Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Category Filter

In this module we are going to add a Category Filter feature and finally wire up those Category buttons on the left side of the UI.

Table Of Contents
  1. Modify the View Model
  2. Update the Home Controller
  3. Fix the Current Unit Tests
  4. Unit Test the Category Filter
  5. Add a Category/Paging Route
    • Modify the Paging Route
    • Create the Category/Paging route
  6. Test the Routes
  7. Wire up the Category buttons
  8. Refactor Home Index & Reseed Categories to Plural
    • Refactor the Home controller's Index method
    • Reseed Categories from Singular to Plural
      • Pluralize categories in the SeedData class
      • Drop and recreate the database
  9. Fix the Page Count
  10. Highlight the selected Category Button
  11. Initialize the ALL button's category to null
  12. Reflect Current Category in Results Header
  13. What's Next

Modify the View Model

The first thing we are going to do is modify the View Model. Whenever we select a new category we are going to need to keep track of what the current category is during paging and sorting. So, we are going to add a CurrentCategory property to the VehiclesListViewModel class.

Modify the VehiclesListViewModel class with the code shown below.

FredsCars\Models\ViewModels\VehiclesListViewModel.cs

namespace FredsCars.Models.ViewModels
{
    public class VehiclesListViewModel
    {
        public List<Vehicle> Vehicles { get; set; } = new();
        
        public PagingInfo PagingInfo { get; set; } = new();
        public SortingInfo SortingInfo { get; set; } = new();
        public string? CurrentCategory { get; set; }
    }
}

Update the Home Controller

Now we need to update the Home Controller to filter the Vehicle results based on a category and set the new CurrentCategory property in the View Model to a value for tracking across paging and sorting.

Modify the Home controller with the code shown below.

FredsCars\Controllers\HomeController.cs

... existing code ...

namespace FredsCars.Controllers
{
    public class HomeController : Controller
    {
        private IVehicleRepository _repo;

        public int PageSize = 5;
        //public int PageSize = 3;

        public HomeController(IVehicleRepository repo)
        {
            _repo = repo;
        }

        public async Task<ViewResult> Index(
            string? category,
            int pageNumber = 1,
            string sortColumn = "make",
            string sortOrder = "asc")
        {
            var vehicles = _repo.Vehicles;

            // Category filter
            vehicles = vehicles.Where(v =>
                category == null || v.VehicleType.Name == category);

            #region Sorting
            ... existing code ...
            #endregion Sorting

            return View(new VehiclesListViewModel
            {
                Vehicles = await vehicles
                    .AsNoTracking()
                    .Skip((pageNumber - 1) * PageSize)
                    .Take(PageSize)
                    .Include(v => v.VehicleType)
                    .ToListAsync(),
                PagingInfo = new PagingInfo
                {
                    PageIndex = pageNumber,
                    PageSize = this.PageSize,
                    TotalItemCount = _repo.Vehicles.Count()
                },
                SortingInfo = new SortingInfo
                {
                    CurrentSortColumn = sortColumn.ToUpper(),
                    CurrentSortOrder = sortOrder.ToUpper()
                },
                CurrentCategory = category
            });
        }
    }
}

In the code above, we modified the Index method of the Home controller to take in a new parameter named category of type nullable string.

Next we modify the vehicles IQueryable to filter by category based on the new parameter value.

vehicles = vehicles.Where(v =>
                category == null || v.VehicleType.Name == category);

In the above statement, we have an OR (||) condition. If the category is null, then for each vehicle the condition is true so all vehicles will be included in the results of the IQueryable. (In this case the right side of the OR condition will be ignored or, short circuited.) OR, if category == null is not true only the vehicles whose VehicleType.Name property is equal to the incoming category parameter’s value will be included in the results.

Notice, we do the filtering first before sorting or paging. We need to filter out the vehicles we want, than sort those results, and then get the current page, in that order.

And finally, we set the new CurrentCategory property of the view model to the value of the incoming category parameter.

CurrentCategory = category

Fix the Current Unit Tests

Because we have added a new parameter to the beginning of the parameter list in the Index method of the Home controller, all of our Home controller unit tests are currently failing.

We need to modify all of our Act statements in the unit tests where we call the Index method of the Home controller with the mocked repo.

Make the following modifications for the Act statement in each unit test.

Can_Access_VehicleList_FromVehicleRepo

VehiclesListViewModel? result =
    (await controller.Index(null)).ViewData.Model
        as VehiclesListViewModel;

Can_Page_Results

// Act
VehiclesListViewModel? result =
    (await controller.Index(null, 2)).ViewData.Model
        as VehiclesListViewModel;

Can_Return_PagingInfo

VehiclesListViewModel? result =
    (await controller.Index(null, 2)).ViewData.Model
        as VehiclesListViewModel;

Can_Sort_ByModel_Asc

VehiclesListViewModel? result =
    (await controller.Index(null, 1, "model", "asc"))
        .ViewData.Model as VehiclesListViewModel;

Can_Sort_ByModel_Desc

VehiclesListViewModel? result =
    (await controller.Index(null, 1, "model", "desc"))
        .ViewData.Model as VehiclesListViewModel;

Now if you do a test run on all of the tests they should all succeed.

Unit Test the Category Filter

Now that we have updated all of our current tests we can go ahead and unit test the category filtering feature.

To completely test that the Index method of the Home controller can filter by category I want to write three unit tests.

  1. Can_Filter_ByCategory: Test that the Index method can filter by category.
  2. Can_Filter_ByCat_AndSort: Test that the Index method can filter by category and sort at the same time.
  3. Can_Filter_ByCat_AndSort_AndPage: Test that the Index method can filter, sort, and page all at the same time together.

NOTE: I am going to put all of the Category Filtering Tests in a region directive so that I can collapse all of those related tests when I don’t need to work with them.

#region Category Filtering Tests
[Fact]
public async Task Can_Filter_ByCategory(){}

[Fact]
public async Task Can_Filter_ByCat_AndSort(){}

[Fact]
public async Task Can_Filter_ByCat_AndSort_AndPage(){}
#endregion

Let’s start with just the category filtering alone.

Add the following unit test to the end of the HomeControllerTests.cs unit tests.

FredsCars.Tests\Controllers\HomeControllerTests.cs

[Fact]
public async Task Can_Filter_ByCategory()
{
    // Arrange
    // 1 - create a List<T> with test items
    var vehicles = new List<Vehicle>
    {
        new Vehicle
        {
            Id = 1,
            Make = "Ford",
            Model = "Mustang",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            },
         },
        new Vehicle
        {
            Id = 2,
            Make = "Chevy",
            Model = "Corvette",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            }
        },
        new Vehicle
        {
            Id = 3,
            Make = "Dodge",
            Model = "Ram",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 4,
            Make = "Chevy",
            Model = "Silverado",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 5,
            Make = "Toyota",
            Model = "Takoma",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 6,
            Make = "Jeep",
            Model = "Cherokee",
            VehicleType = new VehicleType
            {
                Id = 2,
                Name = "Jeep"
            }
        },
    };

    // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
    var mockVehiclesIQueryable = vehicles.BuildMock();

    // 3 - build mock IVehicleRepository
    Mock<IVehicleRepository> mockVehicleRepo =
        new Mock<IVehicleRepository>();
    mockVehicleRepo.Setup(mvr => mvr.Vehicles)
        .Returns(mockVehiclesIQueryable);

    HomeController controller =
        new HomeController(mockVehicleRepo.Object);
    
    // Act
    VehiclesListViewModel? result =
        (await controller.Index("Truck"))
            .ViewData.Model as VehiclesListViewModel;

    Vehicle[] results = result?.Vehicles.ToArray()
        ?? Array.Empty<Vehicle>();

    // Assert
    Assert.Equal(3, results.Length);
    Assert.True(results[0].VehicleType.Name == "Truck");
    Assert.True(results[1].VehicleType.Name == "Truck");
    Assert.True(results[2].VehicleType.Name == "Truck");
}

In the unit test above, the build up in the arrange method should look quite familiar. We create a list of Vehicles as test data, use the MockQueryable.Moq extension to mock the IVehicleRepository, and pass that repo to the Home controller’s constructor when we create an instance of it.

In the act section, we call the Index method of the Home controller and pass “Truck” as the category to filter by and assign the resulting model to a variable called result of type VehiclesListViewModel. Then we convert the Vehicles property of the model to an array making the Vehicle results easier to work with and assign that to a variable of type Vehicle[] (or Vehicle array) names results.

Finally, in the assert section we use the Assert.Equal method to test that there are three Vehicles in the results which should be true if we look at our test data in the arrange section. Then we use the Assert.True method three times to assert that Vehicle result one, two, and three (indexed by 0, 1, and 2 respectively) all have a VehicleType.Name property value of “Truck”.


Next, add a test called Can_Filter_ByCat_AndSort after the previous test in the HomeControllerTests class with the following code.

[Fact]
public async Task Can_Filter_ByCat_AndSort()
{
    // Arrange
    // 1 - create a List<T> with test items
    var vehicles = new List<Vehicle>
    {
        new Vehicle
        {
            Id = 1,
            Make = "Ford",
            Model = "Mustang",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            },
         },
        new Vehicle
        {
            Id = 2,
            Make = "Chevy",
            Model = "Corvette",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            }
        },
        new Vehicle
        {
            Id = 3,
            Make = "Dodge",
            Model = "Ram",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 4,
            Make = "Chevy",
            Model = "Silverado",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 5,
            Make = "Toyota",
            Model = "Takoma",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 6,
            Make = "Jeep",
            Model = "Cherokee",
            VehicleType = new VehicleType
            {
                Id = 2,
                Name = "Jeep"
            }
        },
    };

    // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
    var mockVehiclesIQueryable = vehicles.BuildMock();

    // 3 - build mock IVehicleRepository
    Mock<IVehicleRepository> mockVehicleRepo =
        new Mock<IVehicleRepository>();
    mockVehicleRepo.Setup(mvr => mvr.Vehicles)
        .Returns(mockVehiclesIQueryable);

    HomeController controller =
        new HomeController(mockVehicleRepo.Object);
    
    // Act
    // Default sort by Make
    VehiclesListViewModel? resultByMake =
        (await controller.Index("Truck"))
            .ViewData.Model as VehiclesListViewModel;

    Vehicle[] resultsByMake = resultByMake?.Vehicles.ToArray()
        ?? Array.Empty<Vehicle>();

    // Sort by Model
    VehiclesListViewModel? resultByModel =
        (await controller.Index("Car", sortColumn: "model"))
            .ViewData.Model as VehiclesListViewModel;

    Vehicle[] resultsByModel = resultByModel?.Vehicles.ToArray()
        ?? Array.Empty<Vehicle>();

    // Assert
    // -asserts for cat:truck/sort by make
    Assert.Equal(3, resultsByMake.Length);
    Assert.True(resultsByMake[0].VehicleType.Name == "Truck"
        && resultsByMake[0].Make == "Chevy");
    Assert.True(resultsByMake[1].VehicleType.Name == "Truck"
        && resultsByMake[1].Make == "Dodge");
    Assert.True(resultsByMake[2].VehicleType.Name == "Truck"
        && resultsByMake[2].Make == "Toyota");
    
    // -asserts for cat:car/sort by model
    Assert.Equal(2, resultsByModel.Length);
    Assert.True(resultsByModel[0].VehicleType.Name == "Car"
        && resultsByModel[0].Model == "Corvette");
    Assert.True(resultsByModel[1].VehicleType.Name == "Car"
        && resultsByModel[1].Model == "Mustang");
}

In the above unit test we are testing the sorting feature along side the filtering feature to make sure they can both work in unison.

The arrange section is exactly the same as the previous test including the test data in the List of Vehicles.

In the act section, we are really performing two tests. Number one, we are testing that the default sort by “make” works with category filtering. And, number two, we are testing that we can specify a different column property to sort on than the default and it will still work along side of the filtering feature.

In the first test within the act section where we call the Index method of the Home controller, we only pass the category, “Truck”.

// Default sort by Make
VehiclesListViewModel? resultByMake =
    (await controller.Index("Truck"))
        .ViewData.Model as VehiclesListViewModel;

We rely on the optional parameters in the Home controller’s Index method signature to set the page number and sorting values.

public async Task<ViewResult> Index(
    string? category,
    int pageNumber = 1,
    string sortColumn = "make",
    string sortOrder = "asc")
{
   ...
}

We finish up the first test by assigning the Vehicles property of the returned model holding the Vehicle results array to a variable named resultsByMake of type Vehicle[].

Vehicle[] resultsByMake = resultByMake?.Vehicles.ToArray()
    ?? Array.Empty<Vehicle>();

In the second test commented by the text, “Sort by Model”, we pass the string “Car” as the category to filter by and this time specify “model” as the column property to sort by.

// Sort by Model
VehiclesListViewModel? resultByModel =
    (await controller.Index("Car", sortColumn: "model"))
        .ViewData.Model as VehiclesListViewModel;

Usually, you have to go in order in passing parameter values to a method. But above we were able to skip over the pageNumber parameter (which will use its default value of 1 specified in the Index method signature) and specify the sortColumn parameter value with a named parameter.

sortColumn: "model"

To specify a named parameter we use the parameter’s name followed by a colon and then the value to pass to that parameter.

This time we assign the results of the returned model’s Vehicles property to a variable named resultsByModel again of type Vehicle[].

Vehicle[] resultsByModel = resultByModel?.Vehicles.ToArray()
    ?? Array.Empty<Vehicle>();

Next, we have assert sections for both of our little mini tests. One makes asserts on resultsByMake, and another makes asserts on resultsByModel.

The first section tests the results of filtering by truck and sorting by make.

// -asserts for cat:truck/sort by make
Assert.Equal(3, resultsByMake.Length);
Assert.True(resultsByMake[0].VehicleType.Name == "Truck"
    && resultsByMake[0].Make == "Chevy");
Assert.True(resultsByMake[1].VehicleType.Name == "Truck"
    && resultsByMake[1].Make == "Dodge");
Assert.True(resultsByMake[2].VehicleType.Name == "Truck"
    && resultsByMake[2].Make == "Toyota");

If we look at our test data, there should be three Vehicles returned with a category (or really VehicleType.Name under the hood) of “Truck”.
Then we assert for each of the three results that the category is indeed “Truck” and that the first Vehicle has a Make property value of “Chevy”, the second result has a value of “Dodge”, and the third result’s Make is “Toyota”.

The second section tests the results of filtering by car and sorting by model.

// -asserts for cat:car/sort by model
Assert.Equal(2, resultsByModel.Length);
Assert.True(resultsByModel[0].VehicleType.Name == "Car"
    && resultsByModel[0].Model == "Corvette");
Assert.True(resultsByModel[1].VehicleType.Name == "Car"
    && resultsByModel[1].Model == "Mustang");

Here, we assert there are two Vehicles in the results, both results have a category of “Car” and the results were sorted by Model. The first result has a model of “Corvette” and the second result has a model of “Mustang”.


For our last test outlined at the beginning of this section, add the following unit test at the end of the HomeControllerTests class to test filtering, sorting, and paging all together side by side in unison.

[Fact]
public async Task Can_Filter_ByCat_AndSort_AndPage()
{
    // Arrange
    // 1 - create a List<T> with test items
    var vehicles = new List<Vehicle>
    {
        new Vehicle
        {
            Id = 1,
            Make = "Ford",
            Model = "Mustang",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            },
         },
        new Vehicle
        {
            Id = 2,
            Make = "Chevy",
            Model = "Corvette",
            VehicleType = new VehicleType
            {
                Id = 1,
                Name = "Car"
            }
        },
        new Vehicle
        {
            Id = 3,
            Make = "Dodge",
            Model = "Ram",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 4,
            Make = "Chevy",
            Model = "Silverado",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 5,
            Make = "Toyota",
            Model = "Takoma",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        new Vehicle
        {
            Id = 6,
            Make = "Jeep",
            Model = "Cherokee",
            VehicleType = new VehicleType
            {
                Id = 2,
                Name = "Jeep"
            }
        },
    };

    // 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension 
    var mockVehiclesIQueryable = vehicles.BuildMock();

    // 3 - build mock IVehicleRepository
    Mock<IVehicleRepository> mockVehicleRepo =
        new Mock<IVehicleRepository>();
    mockVehicleRepo.Setup(mvr => mvr.Vehicles)
        .Returns(mockVehiclesIQueryable);

    HomeController controller =
        new HomeController(mockVehicleRepo.Object);
    controller.PageSize = 2;

    // Action
    VehiclesListViewModel? result =
        (await controller.Index("Truck", 2, "desc", "model"))
            .ViewData.Model as VehiclesListViewModel;

    Vehicle[] results = result?.Vehicles.ToArray()
        ?? Array.Empty<Vehicle>();

    // Assert
    Assert.Single(results);
    Assert.True(results[0].VehicleType.Name == "Truck"
        && results[0].Make == "Toyota"
        && results[0].Model == "Takoma");
}

In the unit test above, again the arrange section is exactly the same as the previous two tests except for the fact that we set the Home controller’s PageSize property to 2.

In the act section we once again call the Index method of the Home controller this time passing all four arguments; category of Truck, pageNumber of 2, sortColomn of “model”, and sortOrder of “desc”.

In the assert section we verify there is only one Vehicle in the Vehicle array using the Assert.Single method. And, we assert that the only Vehicle in the second page has a Make property value of “Dodge” and a Model property value of “Ram”.

Add a Category/Paging Route

In module 17 where we added the paging feature, we also added a custom route.
It had the following URL format:
http://localhost:40080/Vehicles/Page2

And this URL pattern is routed to the following custom route named paging in the Program class in the Program.cs file.

app.MapControllerRoute("paging",
    "Vehicles/Page{pageNumber}",
    new { Controller = "Home", action = "Index" });

Modify the Paging Route

For reasons that will become more clear as we move through this module, we need to make a little modification to the paging route.

Right now the literal string “Vehicles” in the paging route is in the URL segment where a category is going to be once we create the Category/Paging route. So a URL such as
https://localhost:40443/Vehicles/Page1
will match the wrong route: Category/Paging rather then just paging.

So we are going to remove the literal string “Vehicles” from the paging route which pages through ALL vehicles.

Make the following change to the paging route in Program.cs.

FredsCars\Program.cs

// removed "Vehicles" from route pattern
app.MapControllerRoute("paging",
    "Page{pageNumber:int}",
    new { Controller = "Home", action = "Index" });

Create the Category/Paging route

To wire up the category buttons and let the user filter results by category we need one more URL format:
http://localhost:40080/Cars/Page1
Here, Cars would represent the category and Page1 would pass 1 to the pageNumber variable in the custom route pattern.

The above URL format will need it’s own custom route. So we need to create a new one.

Make the modifications to the Program class shown below.

FredsCars\Program.cs

... existing code ...
// Add custom routes
app.MapControllerRoute("cat-and-page",
    "{category}/Page{pageNumber:int}",
    new { Controller = "Home", action = "Index" });

app.MapControllerRoute("paging",
    "Page{pageNumber:int}",
    new { Controller = "Home", action = "Index" });

/*** Add endpoints for controller actions and
       the default route ***/
app.MapDefaultControllerRoute();
... existing code ...

Now, we have three routes defined; cat-and-page, paging, and the default route “/”. The cat-and-page endpoint has a category segment variable in its URL pattern in addition to the pageNumber variable in the second URL segment.

NOTE: the pageNumber variable in both custom routes are explicitly marked as integer types with a colon after the variable name and then the datatype as in
pageNumber:int


I am also going to add a piece of middleware to the Program class in the pipeline to make it easier to see which custom route is being used for each request. Make the second modification to the Program class shown below.

FredsCars\Program.cs

... existing code ...
/*** Add endpoints for controller actions and
       the default route ***/
app.MapDefaultControllerRoute();

// Log chosen route to console
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    var routeNameMetadata = endpoint?.Metadata.GetMetadata<RouteNameMetadata>();
    var routeName = routeNameMetadata?.RouteName;
    
    if (endpoint is RouteEndpoint routeEndpoint)
    {
        Console.WriteLine($"Matched route: {routeEndpoint.RoutePattern.RawText}");
        Console.WriteLine($"RouteName: {routeName}");
    }
    await next();
});

/*** Seed the database ***/
... existing code ...

The above middleware function will make it easier for us to see which route is used for each request by logging the route pattern and route name to the console window.

Test the Routes

Let’s test out the three routes we are going to use.

The three routes we need are:

  1. https://localhost:40443 – default route: “/”
  2. https://localhost:40443/Page2 – All Vehicles with a page number.
  3. https://localhost:40443/Cars/Page2 – All Vehicles from a specific category with a page number.

Navigate your browser to https://localhost:40443.

You can now see the route pattern and route name that were used for the request in the console window.

The route pattern is:
{controller=Home}/{action=Index}/{id?}
and the route name is default.

This was set up in the HTTP pipeline configuration in Program.cs with the following statement.

app.MapDefaultControllerRoute();

The first time we enter the application with the URL https://localhost:40443/ it does not go through a custom route because there are no URL segments containing the category or pageNumber variables. The request goes straight to the Index method in the Home controller and the default values of the optional parameters within the Index method are used.


Now navigate to the URL https://localhost:40443/Page2.

Now the route pattern used for the request was
Page{pageNumber:int}
and the route name was paging.
This was our already existing custom route (with our little tweak in the previous section) and the paging and sorting still work correctly as before for. We just haven’t wired up the ALL button yet in the category buttons area.


Lastly, navigate to the URL https://localhost:40443/Cars/Page2.

The route pattern used was
{category}/Page{pageNumber:int}
and the route name was cat-and-page.


The cat-and-page custom route will be used anytime we click the Cars, Trucks, or Jeeps button with the URLs
https://localhost:40443/Cars/Page1
https://localhost:40443/Trucks/Page1
https://localhost:40443/Jeeps/Page1

each setting their respective category to the first page.

The ALL button will use the URL
https://localhost:40443/Page1
setting the results to the first page of the ALL Vehicles category (which really has no category at all or a category of null)

The sort column header links and paging buttons will use the cat-and-page custom route if the CurrentCategory sent down to the view from the controller is not null and the paging custom route otherwise.

Wire up the Category buttons

Now that we have all of our custom routes in place, custom and default, let’s finally wire up those category buttons.

Make the following modifications to the View file of the Home controller’s Index method.

FredsCars\Views\Home\Index.cshtml

... existing code ...
<div class="container-fluid mx-0 row"
    style="margin-top: 20px;">
    <!-- Categories -->
    <div class="col-4 col-md-3 col-lg-2"
         style="border-right: 2px solid black">
        <div class="d-grid gap-2 button-grid">
            <a asp-controller="Home" 
               asp-action="Index"
               asp-route-pageNumber="1"
               class="btn btn-primary button">
                <b>ALL</b></a>
            <a asp-controller="Home"
               asp-action="Index"
               asp-route-category="Cars"
               asp-route-pageNumber="1"
               class="btn btn-outline-primary button">
                <b>CARS</b></a>
            <a asp-controller="Home"
               asp-action="Index"
               asp-route-category="Trucks"
               asp-route-pageNumber="1"
               class="btn btn-outline-primary button">
                <b>TRUCKS</b></a>
            <a asp-controller="Home"
               asp-action="Index"
               asp-route-category="Jeeps"
               asp-route-pageNumber="1"
               class="btn btn-outline-primary button">
                <b>JEEPS</b>
            </a>
        </div>
    </div>
... existing code ...

In the above razor code, we are using four anchor tag helpers to create the Category buttons for the user and generate URLs based on the tag helper’s asp attribute values.

The anchor tag helper for the ALL button looks like this:

<a asp-controller="Home" 
   asp-action="Index"
   asp-route-pageNumber="1"
   class="btn btn-primary button">
    <b>ALL</b></a>

Since it contains an asp-route-pageNumber attribute/value pair and no asp-route-category attribute, it will not not match and skip over the cat-and-page custom route in Program.cs and match the paging route. The generated html for the tag helper will look like this:

<a class="btn btn-primary button" href="/Page1">
	<b>ALL</b></a>

The anchor tag helper for a Category, using Cars as an example, looks like this:

<a asp-controller="Home"
   asp-action="Index"
   asp-route-category="Cars"
   asp-route-pageNumber="1"
   class="btn btn-outline-primary button">
    <b>CARS</b></a>

Since the tag helper code above has asp-route- values for both category and pageNumber, it will match the cat-and-page route in Program.cs
and the resulting generated html will look like this:

<a class="btn btn-outline-primary button" href="/Cars/Page1">
	<b>CARS</b></a>

Refactor Home Index & Reseed Categories to Plural

Right now the ALL vehicles functionality works fine. But if you type in the URL https://localhost:40443/Cars/Page1 or click the Cars button no results will show up.

A couple of things need to happen to get the categories caught up and working as well as the ALL Vehicles feature.

Refactor the Home controller’s Index method

In order for the LINQ query that filters on category (stored in the VehicleType table in the database) to work, we need to include the VehicleType entities for each Vehicle via the VehicleType navigation property. Make the following modification to the HomeController class.

FredsCars\FredsCars\Controllers\HomeController.cs

... existing code ...
public async Task<ViewResult> Index(
    string? category,
    int pageNumber = 1,
    string sortColumn = "make",
    string sortOrder = "asc")
{
    // Category filter
    var vehicles = _repo.Vehicles
        .Include(v => v.VehicleType)
        .Where(v =>
            category == null || v.VehicleType.Name == category);
... existing code ...

Now that we are including the VehicleType entities for Vehicles in category filtering which comes first before sorting and paging, we can remove the Include method for paging in the return statement of the HomeController’s Index method. Make the following modification to the HomeController class and comment out the Include line of the LINQ query.

FredsCars\Controllers\HomeController.cs

... existing code ...
return View(new VehiclesListViewModel
{
    Vehicles = await vehicles
        .AsNoTracking()
        .Skip((pageNumber - 1) * PageSize)
        .Take(PageSize)
        // .Include(v => v.VehicleType)
        .ToListAsync(),
... existing code ...

Reseed Categories from Singular to Plural

At this point if you start the application and navigate to https://localhost:40443/ then the first five vehicles from all vehicles are displayed. But, if you click on the CARS button no cars are displayed. This is because our URL has Cars (plural) for the category:
https://localhost:40443/Cars/Page1
while it is stored in singular form in the database as Car.
The plural form looks more natural in the URL. We are looking at and paging through cars; Not a car.

Pluralize categories in the SeedData class

The best way we can change categories to plural form in the database is to change the categories initialization in the SeedData class, drop the database, run an EF Core update command to recreate the database, and restart the application so it will enter the categories correctly into the VehicleType table. Make the following modifications to the SeedData class.

C:\Development\FredsCars\MVC\Module19\FredsCars\FredsCars\Models\SeedData.cs

... existing code ...
// Add VehicleType records
//   if no VehicleType data exists yet.
if (!context.VehicleTypes.Any())
{
    // Reset Identity seed value to 1
    context.Database
        .ExecuteSqlRaw("DBCC CHECKIDENT ('VehicleType', RESEED, 0)");

    context.VehicleTypes.AddRange(
        new VehicleType
        {
            Name = "Cars"
        },
        new VehicleType
        {
            Name = "Trucks"
        },
        new VehicleType
        {
            Name = "Jeeps"
        }
    );
    context.SaveChanges();
}

// Capture newly generated VehicleType Ids
//   generated by SQL Server when the records
//   are inserted.
var carTypeId = context.VehicleTypes
        .FirstOrDefault(vt => vt.Name == "Cars")!.Id;
var truckTypeId = context.VehicleTypes
    .FirstOrDefault(vt => vt.Name == "Trucks")!.Id;
var jeepTypeId = context.VehicleTypes
    .FirstOrDefault(vt => vt.Name == "Jeeps")!.Id;
... existing code ...

Drop and recreate the database

Now stop the application from running by typing Ctrl-C in the console window and enter the following command to drop the database.

dotnet ef database drop --force

Next run the following command to recreate the database.

dotnet ef database update

Now enter the following command in the console window to restart the application and recreate the database.

 dotnet run --launch-profile "https"

NOTE: Actually no changes were made to the database schema. We are just modifying data. So if you prefer rather then drop and recreate the database, you could just delete all the rows from the VehicleType table and rerun the application in development mode. Because of the way we set up the seeding early on, only the VehicleType table will be filled with the changed values. The VehicleTable will not be touched.

Fix the Page Count

Now when you run the application and click the CARS button, the first five out of six cars show up on the first page.

If you then click the Page 2 button link the six car shows up on the second page.

But there is an extra Page button link being generated for a third page. If you click it you are taken to a page with 0 results.


We need to fix the Total Item Count calculation for categories in the PagingInfo class of the return statement within the Home controller’s Index method.

Make the modification to the HomeController class shown below.

FredsCars\Controllers\HomeController.cs

... existing code ...
return View(new VehiclesListViewModel
{
    Vehicles = await vehicles
        .AsNoTracking()
        .Skip((pageNumber - 1) * PageSize)
        .Take(PageSize)
        // .Include(v => v.VehicleType)
        .ToListAsync(),
    PagingInfo = new PagingInfo
    {
        PageIndex = pageNumber,
        PageSize = this.PageSize,
        TotalItemCount = category == null
            ? _repo.Vehicles.Count()
            : _repo.Vehicles.Where(v =>
                v.VehicleType.Name == category).Count()
    },
    SortingInfo = new SortingInfo
    {
        CurrentSortColumn = sortColumn.ToUpper(),
        CurrentSortOrder = sortOrder.ToUpper()
    },
    CurrentCategory = category
});
... existing code ...

In the code above we have changed the TotalItemCount assignment in the PagingInfo class to a ternary statement where if the incoming category from the model is null, we still get the count of all the vehicles in the repository, else we get the count of all the vehicles in the repository whose VehicleType.Name value is equal to the incoming category from the model.

Now when you click the CARS button, there are appropriately only two Page Link Buttons.

Highlight the selected Category Button

Right now we can click a category button and the correct vehicles will show for that category. But, the chosen category button is still not getting highlighted. Let’s fix that. Make the following modifications to the Home controllers Index View file.

... existing code ...
<!-- Categories -->
<div class="col-4 col-md-3 col-lg-2"
     style="border-right: 2px solid black">
    <div class="d-grid gap-2 button-grid">
        <a asp-controller="Home" 
           asp-action="Index"
           asp-route-pageNumber="1"
           class="btn @(Model.CurrentCategory == null
                        ? "btn-primary"
                        : "btn-outline-primary button")">
            <b>ALL</b></a>
        <a asp-controller="Home"
           asp-action="Index"
           asp-route-category="Cars"
           asp-route-pageNumber="1"
           class="btn @(Model.CurrentCategory == "Cars"
                        ? "btn-primary"
                        : "btn-outline-primary button")">
            <b>CARS</b></a>
        <a asp-controller="Home"
           asp-action="Index"
           asp-route-category="Trucks"
           asp-route-pageNumber="1"
           class="btn @(Model.CurrentCategory == "Trucks"
                        ? "btn-primary"
                        : "btn-outline-primary button")">
            <b>TRUCKS</b></a>
        <a asp-controller="Home"
           asp-action="Index"
           asp-route-category="Jeeps"
           asp-route-pageNumber="1"
           class="btn @(Model.CurrentCategory == "Jeeps"
                        ? "btn-primary" 
                        : "btn-outline-primary button")">
            <b>JEEPS</b>
        </a>
    </div>
</div>
... existing code ...

In the above razor code, for each category button, we use the ‘@’ sign to transition into C# while setting the Bootstrap button styles again using a ternary operator. If the incoming CurrentCategory property from the model equals the appropriate string, “Cars”, “Jeeps”, or “Trucks”, then the button gets a style of btn-primary. Else, the button gets a style of btn-outline-primary. For the ALL button the appropriate value will be null instead of a string.

Now restart the application and the correct category buttons will highlight as you click them.

Initialize the ALL button’s category to null

At this point the paging is working pretty well. But, there is still one little bug we need to fix.

When you first enter the application through the URL https://localhost:40443 and hover over the ALL button, you can see that the button link is correctly pointing to https://localhost:40443/Page1.

But, if you then click any other category button, such as Cars and again hover over the ALL button, the button link now points to https://localhost:40443/Cars/Page1. When the built in anchor tag helper generates the link’s URL, it is matching the cat-and-page route and passing the current category from the incoming model to the category segment of the cat-and-page route. We don’t want it to do this. We want it to match the paging route. To force this behavior make the following modification to the Home controller’s View file.

FredsCars\Views\Home\Index.cshtml

... existing code ...
<!-- Categories -->
<div class="col-4 col-md-3 col-lg-2"
     style="border-right: 2px solid black">
    <div class="d-grid gap-2 button-grid">
        <a asp-controller="Home" 
           asp-action="Index"
           asp-route-category=""
           asp-route-pageNumber="1"
           class="btn @(Model.CurrentCategory == null
                        ? "btn-primary"
                        : "btn-outline-primary button")">
            <b>ALL</b></a>
... existing code ...

In the above code for the ALL button’s button link we are setting the category route value to an empty string forcing the category for the generated URL to be null and match the paging route. Now when you restart the application if you click the CARS button and hover over the ALL button or inspect it through the web dev tools you’ll see that the URL for the ALL button link is now correct. It points to https://localhost:40443/Page1 rather than https://localhost:40443/Cars/Page1.

Reflect Current Category in Results Header

When the user clicks a category button we want that category to be reflected in the header of the results. To make this happen modify the Index View of the Home Controller.

FredsCars\Views\Home\Index.cshtml

@model VehiclesListViewModel

@{
    ViewData["Title"] = "Welcome";
}

<div class="container-fluid my-4 text-center">
    <h1>Welcome to Fred's Cars!</h1>
    Where you'll always find the right car, truck, or jeep.<br />
    Thank you for visiting our site!

    <div class="container-fluid mx-0 row"
        style="margin-top: 20px;">
        <!-- Categories -->
        <div class="col-4 col-md-3 col-lg-2"
             style="border-right: 2px solid black">
            <div class="d-grid gap-2 button-grid">
                <vc:categories-component />
            </div>
        </div>
        <!-- Results -->
        <div class="col">
            <h3 class="bg-dark text-success">
                @(Model.CurrentCategory == null ?
                    "ALL RESULTS" : Model.CurrentCategory.ToUpper())</h3>
            <table class="results table table-striped">
... existing code ...

In the code above we have made a simple change and replaced the static text, “ALL Results” with a C# ternary condition that checks if the CurrentCategory in the Model coming down from the controller is null and if so displays the text “ALL RESULTS”. Otherwise it displays the current category from the model and converts it to all upper case.

What’s Next

In this module we completed the last feature we needed for the main page and the vehicle results. We can now filter vehicles by category, sort those vehicles using a column header, and page through the result set.

In the next few modules we are going to learn the concept of a partial views and view components in ASP.Net Core MVC.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme