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

Sorting

In the last module we added the paging feature using a custom tag helper. We also created a View Model to hold a PagingInfo class and the Vehicle results. We passed the View Model from the controller to the view so that the view could render the results and page link buttons.

In this module we will add a sorting feature. We will start off by passing the sorting information from the controller to the view via the ViewData dictionary to see an alternative way from using a View Model.

Table Of Contents
  1. Style House Cleaning
    • Fix the table width
  2. Add the sorting feature to the Home Controller
    • The #region directive
    • The ViewData dictionary
    • AsNoTracking()
  3. Modify the View
    • The Bootstrap Display property & breakpoints
  4. Refactor the Controller: pass sorting info via View Model
    • Create the SortingInfo class
    • Add Sorting Info to the View Model
    • Modify the controller
  5. Refactor the View
  6. Create a SortColumn tag helper
  7. Apply the SortColumn tag helper to the View
  8. Unit Test Sorting
    • Unit Test the SortColumn tag helper
    • Unit Test Sorting in the Home Controller
  9. What's Next

Style House Cleaning

Before starting on the sorting feature, we have a house cleaning item to perform with our styling.

Fix the table width

Right now the results table header columns and rows do not take up the whole width of the parent table div element.

Modify the width of the tbody and thead elements from 90 to 100 percent in the site.css file.

C:\Development\FredsCars\MVC\Module18\FredsCars\FredsCars\wwwroot\css\site.css

... existing code ...
table.results thead, table tbody tr {
    display: table;
    width: 100%;
    table-layout: fixed;
}

Restart the application and the table results and column headers should span across the full designated area.

You might have to hard reload and empty the cache for the change to take affect. To do this you can open the web dev tools by hitting F12 on your keyboard. Then right click the refresh button in the top toolbar of the browser and select Empty Cache and Hard Reload.

Add the sorting feature to the Home Controller

Modify the home controller with the code below.

FredsCars\Controllers\HomeController.cs

using FredsCars.Models.Repositories;
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

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(
            int pageNumber = 1,
            string sortColumn = "make",
            string sortOrder = "asc")
        {
            var vehicles = _repo.Vehicles;

            #region Sorting
            ViewData["CurrentSortColumn"] = sortColumn.ToUpper();
            ViewData["CurrentSortOrder"] = sortOrder.ToUpper();

            if (sortOrder.ToUpper() == "ASC")
            {
                switch (sortColumn.ToUpper()) {
                    case "MAKE":
                        vehicles = vehicles.OrderBy(v => v.Make);
                        break;
                    case "MODEL":
                        vehicles = vehicles.OrderBy(v => v.Model);
                        break;
                    case "STATUS":
                        vehicles = vehicles.OrderBy(v => v.Status);
                        break;
                    case "YEAR":
                        vehicles = vehicles.OrderBy(v => v.Year);
                        break;
                    case "COLOR":
                        vehicles = vehicles.OrderBy(v => v.Color);
                        break;
                    case "PRICE":
                        vehicles = vehicles.OrderBy(v => v.Price);
                        break;
                    case "VEHICLETYPE":
                        vehicles = vehicles.OrderBy(v => v.VehicleType.Name);
                        break;
                }
            }
            if (sortOrder.ToUpper() == "DESC")
            {
                switch (sortColumn.ToUpper())
                {
                    case "MAKE":
                        vehicles = vehicles.OrderByDescending(v => v.Make);
                        break;
                    case "MODEL":
                        vehicles = vehicles.OrderByDescending(v => v.Model);
                        break;
                    case "STATUS":
                        vehicles = vehicles.OrderByDescending(v => v.Status);
                        break;
                    case "YEAR":
                        vehicles = vehicles.OrderByDescending(v => v.Year);
                        break;
                    case "COLOR":
                        vehicles = vehicles.OrderByDescending(v => v.Color);
                        break;
                    case "PRICE":
                        vehicles = vehicles.OrderByDescending(v => v.Price);
                        break;
                    case "VEHICLETYPE":
                        vehicles = vehicles.OrderByDescending(v => v.VehicleType.Name);
                        break;
                }
            }
            #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()
                }
            });
        }
    }
}

In the home controller’s code above, we have changed the PageSize property from a value of 5 to 3 so we can watch the behavior of the paging action with the sorting more clearly. So we have commented out the line that sets the value to five for now.

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

We also add two parameters to the Index method named sortColumn and sortOrder. We gave them both default values making them optional parameters. The default sort column will be make and the default sort order will be ascending.

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

NOTE: We went back to the squiggly syntax for the body of the Index method from the method bodied expression syntax since we are going to have a lot more logic now.

Next we capture the repository’s Vehicles IQueryable in a variable named vehicles (with a lower case ‘v’) so that we can modify the IQueryable for sorting and paging (and later a category filter).

var vehicles = _repo.Vehicles;

The #region directive

I have put all of the sorting logic into a #region directive so that the sorting logic can be collapsed and expanded as needed. So you don’t have to look at all of that sorting logic if you are working in another part of the controller code.

#region Sorting
/* sorting code */
#endregion Sorting

Just click on the down arrow on the left of the #region directive to collapse that section.

And then click on the right arrow button on the left of the collapsed directive to expand it again.

The ViewData dictionary

Within the Sorting region the first thing we do is add two items to the ViewData Dictionary.

  • CurrentSortColumn: captures the incoming sortColumn parameter value to be sent to the view in order for the view to track the current sort column.
  • CurrentSortOrder: captures the incoming sortOrder parameter value to be sent to the view in order for the view to track the current sort order.

Notice we convert both items to upper case before storing in the Data Dictionary. This is a common method so that when we compare to other values we can also convert the other values to compare to upper case making the comparison case-insensitve.

The ViewData Dictionary is an alternative way from a View Model to pass information from the controller to the view. It is kind of a quick and dirty way to get small pieces of information from the view to the controller when you don’t feel that information warrants taking the time to set up a strongly typed View Model.


Next, we have the bulk of the sorting logic in two if-statements both of which contain a switch statement with case statements for each column we want to sort by.

The first if-statement checks if the incoming sortOrder parameter converted to upper-case equals the literal string “ASC”. This checks if we want to order the results in ascending order.

if (sortOrder.ToUpper() == "ASC")
{
   /* switch statement */
}

The switch statement then bases it’s case statements on it’s expression which is sortColumn.ToUpper(). In the body of the switch statement we have a case for each column name we want to sort by. If a case’s label matches the incoming sort column in upper case then we modify the vehicles IQueryable to order the results of vehicles by the property matching the case label. We do this using the LINQ OrderBy method on the vehicles IQueryable. Notice we use a break statement after each case statement in order to break out of the case block and continue on with the rest of the code.

switch (sortColumn.ToUpper()) {
    case "MAKE":
        vehicles = vehicles.OrderBy(v => v.Make);
        break;
    case "MODEL":
        vehicles = vehicles.OrderBy(v => v.Model);
        break;
    case "STATUS":
        vehicles = vehicles.OrderBy(v => v.Status);
        break;
    case "YEAR":
        vehicles = vehicles.OrderBy(v => v.Year);
        break;
    case "COLOR":
        vehicles = vehicles.OrderBy(v => v.Color);
        break;
    case "PRICE":
        vehicles = vehicles.OrderBy(v => v.Price);
        break;
    case "VEHICLETYPE":
        vehicles = vehicles.OrderBy(v => v.VehicleType.Name);
        break;
}

The second if statement containing a switch block is similar to the first. Except here we check if the incoming sortOrder parameter is DESC for descending rather than ascending.

If so, the switch statement runs through all of its cases and if it finds a match it orders the vehicles in descending order by the property matching that case using the LINQ OrderByDescending method against the vehicles IQueryable.

if (sortOrder.ToUpper() == "DESC")
{
    switch (sortColumn.ToUpper())
    {
        case "MAKE":
            vehicles = vehicles.OrderByDescending(v => v.Make);
            break;
        case "MODEL":
            vehicles = vehicles.OrderByDescending(v => v.Model);
            break;
        case "STATUS":
            vehicles = vehicles.OrderByDescending(v => v.Status);
            break;
        case "YEAR":
            vehicles = vehicles.OrderByDescending(v => v.Year);
            break;
        case "COLOR":
            vehicles = vehicles.OrderByDescending(v => v.Color);
            break;
        case "PRICE":
            vehicles = vehicles.OrderByDescending(v => v.Price);
            break;
        case "VEHICLETYPE":
            vehicles = vehicles.OrderByDescending(v => v.VehicleType.Name);
            break;
    }
}

The last part of the code does not change much. We still return a VehiclesListViewModel with its Vehicles property set to an IQueryable and implement the paging on that IQueryable inline with the Skip and Take LINQ methods. But now we use our local vehicles variable (which is already modified with the sorting logic) rather then the repo’s Vehicles property. So we are paging in sorted order.

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()
    }
});

AsNoTracking()

Also notice above I have added the AsNoTracking method on to the vehicles IQueryable.

This can improve performance because the EF Core change tracker will not track any of the changes for the entity set such as adding a vehicle, updating a vehicle, or deleting a vehicle. So, AsNoTracking is good to include on a results page like the one we are working on (typically called a list page or the index page) where the overhead of tracking is not required. But you wouldn’t want to include it on other CRUD pages like an update or delete page.


The PagingInfo property of the View Model stays the same. We still set the PageIndex to the incoming pageNumber parameter value, PageSize to the class PageSize value, and TotalItemCount to the repo’s Vehicle count.

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()
    }
});

Modify the View

Now we can apply the changes to the view. Modify the Index view with the code below.

FredsCars\Views\Home\Index.cshtml

@model VehiclesListViewModel

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

    string currentSortColumn = ViewBag.CurrentSortColumn;
    string currentSortOrder = ViewBag.CurrentSortOrder;
}

<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">
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-primary button">
                    <b>ALL</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>CARS</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>TRUCKS</b></a>
                <a asp-controller="Vehicles" 
                   asp-action="Index"
                   class="btn btn-outline-primary button">
                    <b>JEEPS</b>
                </a>
            </div>
        </div>
        <!-- Results -->
        <div class="col">
            <h3 class="bg-dark text-success">ALL Results</h3>
            <table class="results table table-striped">
                <thead>
                    <tr>
                        <th></th>
                        <th>
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="status"
                                asp-route-sortOrder="@((currentSortColumn == "STATUS" && currentSortOrder == "ASC")
                                    ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Status)</a>
                        </th>
                        <th>
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="year"
                                asp-route-sortOrder="@((currentSortColumn == "YEAR" && currentSortOrder == "ASC")
                                    ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Year)</a>
                        </th>
                        <th>
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="make"
                                asp-route-sortOrder="@((currentSortColumn == "MAKE" && currentSortOrder == "ASC")
                                    ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Make)</a>
                        </th>
                        <th>
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="model"
                                asp-route-sortOrder="@((currentSortColumn == "MODEL" && currentSortOrder == "ASC")
                                ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Model)</a>
                        </th>
                        <th class="d-none d-lg-table-cell">
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="color"
                                asp-route-sortOrder="@((currentSortColumn == "COLOR" && currentSortOrder == "ASC")
                                ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Color)</a>
                        </th>
                        <th class="d-none d-md-table-cell">
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="price"
                                asp-route-sortOrder="@((currentSortColumn == "PRICE" && currentSortOrder == "ASC")
                                ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].Price)</a>
                        </th>
                        <th class="d-none d-md-table-cell">
                            <a asp-action="Index"
                                asp-route-pageNumber="1"
                                asp-route-sortColumn="vehicleType"
                                asp-route-sortOrder="@((currentSortColumn == "VEHICLETYPE" && currentSortOrder == "ASC")
                                ? "desc" : "asc")">
                                @Html.DisplayNameFor(model => model.Vehicles[0].VehicleType)</a>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var v in Model.Vehicles)
                    {
                        <tr>
                            <td>
                                <img src="@v.ImagePath"
                                        class="result-image" />
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Status)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Year)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Make)
                            </td>
                            <td>
                                @Html.DisplayFor(modelItem => v.Model)
                            </td>
                            <td class="d-none d-lg-table-cell">
                                @Html.DisplayFor(modelItem => v.Color)
                            </td>
                            <td class="d-none d-md-table-cell">
                                @Html.DisplayFor(modelItem => v.Price)
                            </td>
                            <td class="d-none d-md-table-cell">
                                @Html.DisplayFor(modelItem => v.VehicleType.Name)
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
    
             <div id="page-links"
                  page-model="@Model.PagingInfo"
                  page-action="Index"
                  paging-classes-enabled="true"
                  paging-btn-class="btn"
                  paging-btn-class-normal="btn-outline-dark"
                  paging-btn-class-selected="btn-primary"
                  sort-column="@currentSortColumn"
                  sort-order="@(currentSortOrder)"
                  class="btn-group mt-3">
             </div>
        </div>
    </div>  
</div>

In the Index view code above the first change we made was to capture the current sort column and current sort order from the ViewBag object in a C# code block at the top of the file into two string variables named currentSortColumn and currentSortOrder.

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

    string currentSortColumn = ViewBag.CurrentSortColumn;
    string currentSortOrder = ViewBag.CurrentSortOrder;
}

Recall in the controller we used the ViewData object to pass this information down to the view.

ViewData["CurrentSortColumn"] = sortColumn.ToUpper();
ViewData["CurrentSortOrder"] = sortOrder.ToUpper();

But we are capturing these two pieces of information from the ViewBag object in the View.

Both the ViewData dictionary and the ViewBag object can be used to pass information from a controller to a view.
The ViewData dictionary holds key-value pairs where the key is a string and the value is an object.
The ViewBag object uses the dynamic keyword under the hood, meaning you don’t need to define its properties beforehand. This makes it a flexible option for sending data to the view.

Both the ViewData dictionary and the ViewBag object seem to use the same object under the hood. That is why we were able to assign information to the ViewData dictionary in the controller and fetch it back out from the ViewBag object in the View.

We could have fetched it back out in the View using the ViewData dictionary.

string currentSortColumn = ViewData["CurrentSortColumn"]?.ToString() ?? "";
string currentSortOrder = ViewData["CurrentSortOrder"]?.ToString() ?? "";

With the ViewBag object method we do not need to convert the dynamic currentSortColumn and currentSortOrder properties to strings. C# can infer they are strings because we are assigning them to string variables.
However we do have to convert the two values to string when using the ViewData dictionary method. And we need to use the null conditional operator and the null coalescing operator to get rid of warnings about possible null values.

Let’s stick to the ViewBag method in the View for now.


The next change comes in the column headers section of the results table.

This is where we actually make the results sortable by column header. We do this by applying an anchor tag helper to each column header name and use the captured currentSortColumn and currentSortOrder values from above to set up route values. We also need to set up a pageNumber route value to keep track of the page number we are on.

Let’s look at the Model column header as an example.

<a asp-action="Index"
    asp-route-pageNumber="1"
    asp-route-sortColumn="model"
    asp-route-sortOrder="@((currentSortColumn == "MODEL" && currentSortOrder == "ASC")
    ? "desc" : "asc")">
    @Html.DisplayNameFor(model => model.Vehicles[0].Model)</a>

In the code snippet above, we always set the pageNumber to 1 as a route value when we sort because if we sort by a column property we are reordering the results so we need to reset the page number to one.
asp-route-pageNumber="1"

Then we set the sortColumn route value to a literal string with a value of the Vehicle property we want to sort by, in this case “model”.
asp-route-sortColumn="model"

Setting the last route value for the sort order is a little more complicated. We apply a C# expression using Razor syntax which says if the current sort order is equal to “MODEL” (we set to upper case when assigning in the controller) and the current sort order is ASC (again set to upper case in the controller), then set the route value to “desc” else set the route value to “asc”.
asp-route-sortOrder="@((currentSortColumn == "MODEL" && currentSortOrder == "ASC")
? "desc" : "asc")"

Also notice that in the last three th elements in the column headers section I have added the d-none and d-md-table-cell classes to the last two th elements and the d-none and d-lg-table-cell classes to the third from the last th element. I will explain this in the next section where I cover the changes in the td elements for the vehicle results data because I apply the same classes for the last three td elements there. For now, just know that these are display styles from Bootstrap.

The Bootstrap Display property & breakpoints

The last small change is where I add the same Bootstrap classes to the last three td elements in the results as I mentioned above for the last three th elements.

<td class="d-none d-lg-table-cell">
    @Html.DisplayFor(modelItem => v.Color)
</td>
<td class="d-none d-md-table-cell">
    @Html.DisplayFor(modelItem => v.Price)
</td>
<td class="d-none d-md-table-cell">
    @Html.DisplayFor(modelItem => v.VehicleType.Name)
</td>

In CSS we can use the display property to make items not visible and visible by setting the value to ‘none’ or ‘block’.

.myElement {
   display: none
}
.myElement {
   display: block
}

There are many more values for the CSS display property then just none and block. And, you can read about them here at W3schools.

The Bootstrap classes I am applying to the last three th and td elements in the results table are built on the CSS display property but also incorporate Bootstrap breakpoints.

The Bootstrap d-none class initializes the last three td and th elements to be invisible and not take up any space. (There is another CSS property called visibility with hidden and visible values. But, the element takes up space when hidden with this property.)

For the last two td and th elements I then apply the d-md-table-cell class. This BS class will make the table cells for that column visible at the medium break point when the browser window reaches a width of greater than or equal to 768 pixels.

For the third to last td and th elements I apply the d-lg-table-cell class. This BS class will make the table cells for that column visible at the large break point when the browser window reaches a width of greater than or equal to 992 pixels.


The reason I applied the Bootstrap display classes in this way is because there is not enough room in the table for all the columns I want to show so the results content collapses on itself when the width of the browser becomes too small.

With the new BS styles applied, only the Status, Year, Make, and Model columns will show while the browser width is less then 768 pixels wide. The Color, Price, and Category columns will be invisible and not take up any room in the table.

Once the browser reaches a width of greater than or equal to 768 pixels, the Price and Category columns will become visible.

Finally, once the browser window reaches a width of greater than or equal to 992 pixels, the Color column will be displayed.

Normally, we could apply the table-responsive Bootstrap class to an outer div element containing the results table and the columns could overflow horizontally to the right and the browser would apply a horizontal scroll bar to the table. But the behavior of this class doesn’t work properly with our layout so we went with the Bootstrap display classes approach.

Refactor the Controller: pass sorting info via View Model

Right now we are storing the CurrentSortColumn and CurrentSortOrder information in the ViewData dictionary in the controller and pulling it back out from the ViewBag object in the view. This is a quick and handy approach when you just want to pass a piece of information down to the view in a convenient manor.

But, using a strongly typed view model is still always the preferred method for sending information from the controller to the view. For one thing it gives you compile time checking and for another it makes the code more testable.

Since we are already using a view model to pass the vehicle results and the paging information to the view, let’s also incorporate the sorting information into the view model.

Create the SortingInfo class

Create a class called SortingInfo in the ViewModels folder of the FredsCars project and modify its contents with the code below.

FredsCars\Models\ViewModels\SortingInfo.cs

namespace FredsCars.Models.ViewModels
{
    public class SortingInfo
    {
        public string CurrentSortColumn { get; set; } = string.Empty;
        public string CurrentSortOrder { get; set; } = string.Empty;
    }
}

Here, we are following the same pattern we did for the paging information in the PagingInfo class. We are going to move the storage of the current sort column and sort order from the ViewData dictionary to this class.

Add Sorting Info to the View Model

Modify the VehiclesListViewModel class with the code 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();
    }
}

Here, we have added a property called SortingInfo of our new type of the same name, SortingInfo, to the View Model.

Modify the controller

Modify the home controller with the code changes below.

FredsCars\Controllers\HomeController.cs

... existing code ...
#region Sorting
//ViewData["CurrentSortColumn"] = sortColumn.ToUpper();
//ViewData["CurrentSortOrder"] = sortOrder.ToUpper();
... existing code ...

... 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 = _repo.Vehicles.Count()
    },
    SortingInfo = new SortingInfo
    {
        CurrentSortColumn = sortColumn.ToUpper(),
        CurrentSortOrder = sortOrder.ToUpper()
    }
});
... existing code ...

At the top of the home controller file we remove or comment out the two statements that assign the incoming sort parameter values to the ViewData dictionary.

At the bottom of the home controller code we add an instance of the SortingInfo class to the VehiclesListViewModel instance we are returning as the View Model. And we apply to the two SortingInfo properties the two incoming sorting parameter values just as we had before with the ViewData dictionary and remembering to convert the two values to upper case.

Refactor the View

Now the only change we need to make to the view is at the top of the file in the C# code block. Modify the Home controller’s Index view with the code below.

FredsCars\Views\Home\Index.cshtml

@model VehiclesListViewModel

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

    // string currentSortColumn = ViewBag.CurrentSortColumn;
    // string currentSortOrder = ViewBag.CurrentSortOrder;

    string currentSortColumn = Model.SortingInfo.CurrentSortColumn;
    string currentSortOrder = Model.SortingInfo.CurrentSortOrder;
}
... existing code ...

In the code above we have commented out the original two statements that read the sorting information from the ViewBag object and replaced them with two statements that read it from the View Model instead.


At this point if you restart the application the results should be the same. But now the code is more strongly typed and testable.

Create a SortColumn tag helper

Our code and application are starting to look and function pretty good.

But, in the view we are repeating the same code seven times to set up sortable column headers using the built-in anchor tag helper. And there is quite a bit of C# logic interspersed into each instance to set up the sorting route values.

It would be nice if there were some sort of ASP.Net Core component that could lay out this logic neatly and concisely for each of these seven instances without having to repeat the same code each time, keeping our code more DRY.

Let’s see if we can accomplish this using a tag helper since we already learned how to create one for paging when we created the PageLink tag helper.


Create a class in the TagHelpers folder of the FredsCars project named SortColumnTagHelper and modify it with the code below.

FredsCars\TagHelpers\SortColumnTagHelper.cs

using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace FredsCars.TagHelpers
{
    [HtmlTargetElement("sort-column-anchor",
        Attributes = "sort-model")]
    public class SortColumnTagHelper : TagHelper
    {
        private IUrlHelperFactory _urlHelperFactory;

        public SortColumnTagHelper(IUrlHelperFactory urlHelperFactory)
        {
            _urlHelperFactory = urlHelperFactory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext? ViewContext { get; set; }

        public SortingInfo? SortModel { get; set; }
        public required string ColumnText { get; set; }
        public required string SortProperty { get; set; }
        public required string PageAction { get; set; }
        
        public override void Process(TagHelperContext context,
            TagHelperOutput output)
        {
            // already in upper case from the controller
            string? currentSortColumn = SortModel?.CurrentSortColumn;
            string? currentSortOrder = SortModel?.CurrentSortOrder;

            if (ViewContext != null && SortModel != null)
            {
                IUrlHelper urlHelper
                    = _urlHelperFactory.GetUrlHelper(ViewContext);

                output.TagName = "a";

                var url = urlHelper.Action(PageAction,
                    new
                    {
                        pageNumber = 1,
                        sortColumn = SortProperty.ToLower(),
                        sortOrder = 
                            (currentSortColumn == SortProperty.ToUpper()
                                && currentSortOrder == "ASC")
                                    ? "desc" : "asc"
                    });

                output.Attributes.SetAttribute("href", url);

                output.Content.SetContent(ColumnText);
            }
        }
    }
} 

The tag helper code above for sorting is quite similar to the PageLink tag helper we saw for paging.

The SortColumnTagHelper class inherits from the base class TagHelper and is marked with the HtmlTargetElement attribute that specifies to target an html element named sort-column-anchor with an html attribute named sort-model.

[HtmlTargetElement("sort-column-anchor",
    Attributes = "sort-model")]
public class SortColumnTagHelper : TagHelper
{
   ... tag helper code ...
}

We then set up our private instance of an IUrlHelperFactory using dependency injection and declare our ViewContext property just as with the PageLink tag helper.

private IUrlHelperFactory _urlHelperFactory;

public SortColumnTagHelper(IUrlHelperFactory urlHelperFactory)
{
    _urlHelperFactory = urlHelperFactory;
}

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }

Next, we declare four more class properties.

  • SortModel: of type SortingInfo, our new class we added to the VehiclesListViewModel class. This serves much the same function the PageModel property of type PagingInfo did in the paging tag helper. It will contain the current sort column and sort order passed in by the sort-column-anchor html tag in the view.
  • ColumnText: a string property which specifies the text for column header.
  • SortProperty: a string property specifying what property of the Vehicle class to sort Vehicle results by.
  • PageAction: just as in the paging tag helper specifies the controller action to include in the generated URL using the IUrlHelper.

The last three properties above are marked by the required keyword to get rid of nullable warnings so we don’t have to initialize them right off the bat or in a constructor.

public required string ColumnText { get; set; }
public required string SortProperty { get; set; }
public required string PageAction { get; set; }

Next we override the Process method of the base TagHelper class. In the body of the Process method we capture the current sort column and sort order from the SortModel passed in from the custom tag helper’s html element property named sort-model.

public override void Process(TagHelperContext context,
    TagHelperOutput output)
{
    // already in upper case from the controller
    string? currentSortColumn = SortModel?.CurrentSortColumn;
    string? currentSortOrder = SortModel?.CurrentSortOrder;
   ... 
}

Next, we capture an instance of an IUrlHelper into a variable named urlHelper and set the tag helper’s tag name to “a” for anchor since we are creating a dynamic anchor tag.

public override void Process(TagHelperContext context,
    TagHelperOutput output)
{
    ...

    if (ViewContext != null && SortModel != null)
    {
        IUrlHelper urlHelper
            = _urlHelperFactory.GetUrlHelper(ViewContext);

        output.TagName = "a";
        
    ...
}

Next, we define a URL using the Action method of the IUrlHelper instance and assign it to a variable called url.

var url = urlHelper.Action(PageAction,
    new
    {
        pageNumber = 1,
        sortColumn = SortProperty.ToLower(),
        sortOrder = 
            (currentSortColumn == SortProperty.ToUpper()
                && currentSortOrder == "ASC")
                    ? "desc" : "asc"
    });

We provide two arguments to the parameters in the Action method in the above snippet. The first is the controller action to include in the generated URL, PageAction, which is one of our class properties defined earlier with the string value of the controller action for the anchor link to navigate to passed up from the page-action html attribute of the targeted html element, sort-column-anchor.

The second argument is an anonymous object setting the route values needed in the generated URL;
* pageNumber hardcoded to 1 since we want to start on the first page each time we sort vehicle results.
* sortColumn assigned the value from the class SortProperty property passed in by the targeted html element’s html sort-property attribute and converted to lower case.
* sortOrder – the C# logic and expression here assigned to the sortOrder route value is what makes turning sortable column anchor links into a custom tag helper worth it in the first place.

sortOrder = 
    (currentSortColumn == SortProperty.ToUpper()
        && currentSortOrder == "ASC")
            ? "desc" : "asc"

This expression says if the current sort column is equal to the Vehicle sort property we are sorting on, and the current sort order is equal to “ASC” then assign the string “desc” to sortOrder. Otherwise assign “asc” to sortOrder.

This was quite ugly code logic to include in each of the seven instances of the built in anchor tag helper in the view. Now we will be able to replace each of the built in anchor tag helper instances with our custom sort-column-anchor tag helper and this piece of logic will be tucked away into one place, DRY and testable.

Finally, we set the href attribute of the anchor tag we are creating to the URL we just generated above and set the inner text of the anchor element to the ColumnText property of the SortColumnTagHelper class whose value is passed in from the column-text html attribute of the targeted html element, sort-column-anchor.

output.Attributes.SetAttribute("href", url);

output.Content.SetContent(ColumnText);

Apply the SortColumn tag helper to the View

Modify the Index view with the following code.

FredsCars\Views\Home\Index.cshtml

@model VehiclesListViewModel

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

    // string currentSortColumn = ViewBag.CurrentSortColumn;
    // string currentSortOrder = ViewBag.CurrentSortOrder;
    // string currentSortColumn = Model.SortingInfo.CurrentSortColumn;
    // string currentSortOrder = Model.SortingInfo.CurrentSortOrder;
}

<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 -->
        
        ... existing code ...
        
        <!-- Results -->
        <div class="col">
            <h3 class="bg-dark text-success">ALL Results</h3>
            <table class="results table table-striped">
                <thead>
                    <tr>
                        <th></th>
                        <th>
                            <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Status"
                                sort-property="Status"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th>
                            <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Year"
                                sort-property="Year"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th>
                           <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Make"
                                sort-property="Make"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th>
                           <sort-column-anchor
                                sort-model="@Model.SortingInfo"
                                column-text="Model"
                                sort-property="Model"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th class="d-none d-lg-table-cell">
                            <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Color"
                                sort-property="Color"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th class="d-none d-md-table-cell">
                            <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Price"
                                sort-property="Price"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                        <th class="d-none d-md-table-cell">
                            <sort-column-anchor 
                                sort-model="@Model.SortingInfo"
                                column-text="Category"
                                sort-property="VehicleType"
                                page-action="Index">
                            </sort-column-anchor>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var v in Model.Vehicles)
                    {
                       ... existing code ...
                    }
                </tbody>
            </table>
    
             <div id="page-links"
                  page-model="@Model.PagingInfo"
                  page-action="Index"
                  paging-classes-enabled="true"
                  paging-btn-class="btn"
                  paging-btn-class-normal="btn-outline-dark"
                  paging-btn-class-selected="btn-primary"
                  sort-column="@Model.SortingInfo.CurrentSortColumn"
                  sort-order="@Model.SortingInfo.CurrentSortOrder"
                  class="btn-group mt-3">
             </div>
        </div>
    </div>  
</div>

In the Index view’s code above we have replaced seven instances of the built in anchor tag helper with our custom sort-column-anchor tag helper.

To remind ourselves of what the built-in anchor tag helper looked like before take a look at the code below using the Make column header as an example.

<a asp-action="Index"
	asp-route-pageNumber="1"
	asp-route-sortColumn="make"
	asp-route-sortOrder="@((currentSortColumn == "MAKE" && currentSortOrder == "ASC")
	    ? "desc" : "asc")">
	@Html.DisplayNameFor(model => model.Vehicles[0].Make)</a>

I have highlighted in the above snippet in bold brown font the C# expression marked in Razor syntax to set the value of the asp-route-sortOrder route value. This looks kind of tortured especially when laid out and repeated seven times in a row.

The new version looks like this.

<sort-column-anchor 
     sort-model="@Model.SortingInfo"
     column-text="Make"
     sort-property="Make"
     page-action="Index">
 </sort-column-anchor>

No more logic in the html! Isn’t that nice?

Each attribute on the tag helper element feeds a value to its corresponding property in the tag helper class.

sort-model passes in the SortingInfo property from the View Model (VehiclesListViewModel) which contains the CurrentSortColumn and CurrentSortOrder properties.

And, we should be familiar with the rest of the properties by now from inspecting the code and properties in the tag helper class code.

I also commented out the two lines in the C# code block at the top of the view file that captures currentSortColumn and currentSortOrder into variables.

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

    // string currentSortColumn = ViewBag.CurrentSortColumn;
    // string currentSortOrder = ViewBag.CurrentSortOrder;
    // string currentSortColumn = Model.SortingInfo.CurrentSortColumn;
    // string currentSortOrder = Model.SortingInfo.CurrentSortOrder;
}

And modified the pager to access those two properties directly from the Model.

<div id="page-links"
     page-model="@Model.PagingInfo"
     page-action="Index"
     paging-classes-enabled="true"
     paging-btn-class="btn"
     paging-btn-class-normal="btn-outline-dark"
     paging-btn-class-selected="btn-primary"
     sort-column="@Model.SortingInfo.CurrentSortColumn"
     sort-order="@Model.SortingInfo.CurrentSortOrder"
     class="btn-group mt-3">
</div>

If you restart the application the results should be the same as far as paging and sorting goes.

Unit Test Sorting

The first thing we should do after all of this work is make sure all of our current tests still pass. If you do a test run on all tests you will see that the Can_Access_VehicleList_FromVehicleRepo test is failing. Hmm… Did something we changed by programming in new requirements break one of our tests?

Let’s check it out and try to fix it.

It turns out the error is coming from the assertion in the test that there are four Vehicles being returned from the repository and then from the controller.

Assert.True(vehicleArray.Length == 4);

This error crept in because while we were developing paging we changed the default page size from five to three because it was easier to see the functionality if there were more pages. Let’s set the PageSize property back to 5 in the Home controller.

... existing code ...
public class HomeController : Controller
{
    private IVehicleRepository _repo;

    public int PageSize = 5;
... existing code ...

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

Unit Test the SortColumn tag helper

Now that we’ve made sure all of our current tests pass we can unit test the new SortColumn tag helper. Create a class file named SortColumnTagHelperTests.cs in the TagHelpers folder of the FredsCars.Tests project and modify its contents with the code below.

FredsCars.Tests\TagHelpers\SortColumnTagHelperTests.cs

using FredsCars.Models.ViewModels;
using FredsCars.TagHelpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Moq;
using System;
using System.Text.Encodings.Web;

namespace FredsCars.Tests.TagHelpers
{
    public class SortColumnTagHelperTests
    {
        [Fact]
        public void Can_Generate_SortColumnHeader_AnchorLink()
        {
            // Arrange
            SortingInfo SortModel = new SortingInfo
            {
                CurrentSortColumn = "MAKE",
                CurrentSortOrder = "ASC"
            };
            string ColumnText = "Make";
            string SortProperty = "Make";
            string PageAction = "Index";

            string sortOrder =
                (SortModel.CurrentSortColumn == SortProperty.ToUpper()
                                && SortModel.CurrentSortOrder == "ASC")
                                    ? "desc" : "asc";

            var urlHelper = new Mock<IUrlHelper>();
            urlHelper.Setup(x =>
                x.Action(It.IsAny<UrlActionContext>()))
                .Returns($"<a href=\"/Vehicles/Page1"
                    + $"?sortColumn={SortProperty.ToLower()}"
                    + $"&amp;sortOrder={sortOrder}\">"
                    + $"{ColumnText}</a>");
            var urlHelperFactory = new Mock<IUrlHelperFactory>();
            urlHelperFactory.Setup(f =>
                f.GetUrlHelper(It.IsAny<ActionContext>()))
                    .Returns(urlHelper.Object);

            var viewContext = new Mock<ViewContext>();

            SortColumnTagHelper sortColumnTagHelper =
                new SortColumnTagHelper(urlHelperFactory.Object)
                {
                    ViewContext = viewContext.Object,
                    // test run doesn't use these properties.
                    //   Corresponding properties are fed into 
                    //   mock UrlHelper.
                    SortModel = SortModel,
                    ColumnText = "Make",
                    SortProperty = "Make",
                    PageAction = PageAction
                };

            TagHelperContext tagHelperCtx = new TagHelperContext(
                new TagHelperAttributeList(),
                new Dictionary<object, object>(), "");

            var content = new Mock<TagHelperContent>();
            TagHelperOutput tagHelperoutput = new TagHelperOutput("div",
                new TagHelperAttributeList(),
                (cache, encoder) => Task.FromResult(content.Object));

            // Act
            sortColumnTagHelper.Process(tagHelperCtx, tagHelperoutput);

            // Assert
            // Get the full rendered HTML
            using var writer = new StringWriter();
            tagHelperoutput.WriteTo(writer, HtmlEncoder.Default);
            string fullHtml = writer.ToString();

            Assert.Equal(@"<a href=""/Vehicles/Page1?sortColumn=make&amp;sortOrder=desc"">Make</a>",
                tagHelperoutput.Attributes[0].Value);
        }
    }
}

In the unit test code above, we create a test called Can_Generate_SortColumnHeader_AnchorLink. We want to test that the tag helper can indeed generate a sorting link for a column header.

This test is very similar to the unit test we wrote for the PagingTagHelper unit test.

In the Arrange section we again mock an IUrlHelper and IUrlHelperFactory.

We also did this in the unit test for the PagingTagHelper but in that test we used the SetUpSequence method of the mocked IUrlHelper to specify what the Action method should return as PageLink anchors. However we just defined static anchor links with no logic.

urlHelper.SetupSequence(x =>
    x.Action(It.IsAny<UrlActionContext>()))
        .Returns("Test/Page1")
        .Returns("Test/Page2")
        .Returns("Test/Page3");

This does not really prove that the meat of the PageLink tag helper, the build up of the links in the IUrlHelper Action method, can do its job.

aTag.Attributes["href"] = urlHelper.Action(PageAction,
    new { pageNumber = i, sortColumn = SortColumn,
          sortOrder = SortOrder});

This does not invalidate the test. The test is trying to prove that if in fact the dependency of the tag helper, the IUrlHelperFactory, works correctly, then the tag helper can generate page links. And it does. But, I was just not happy that we are bypassing the testing of the actual logic that does the bulk of the work.

In this unit test for the SortColumn tag helper, I tried to mock the IUrlHelper in a more dynamic way.

I did this by first arranging variables to hold the values I know are going to be dynamically fed into the IUrlHelper.Action method to build up a column sort link in the Arrange section.

Then I define the Action method to do a dynamic build up in a similar fashion to the code in the actual tag helper.

urlHelper.Setup(x =>
    x.Action(It.IsAny<UrlActionContext>()))
    .Returns($"<a href=\"/Vehicles/Page1"
        + $"?sortColumn={SortProperty.ToLower()}"
        + $"&amp;sortOrder={sortOrder}\">"
        + $"{ColumnText}</a>");

Then we instantiate an instance of the SortColumnTagHelper and pass to its constructor the mocked IUrlHelperFactory containing the mocked IUrlHelper and Action method to dynamically build up a sort column header link. I think this more naturally reflects the dynamic buildup of a sort link in the tag helper.

SortColumnTagHelper sortColumnTagHelper =
    new SortColumnTagHelper(urlHelperFactory.Object)
    {
        ViewContext = viewContext.Object,
        // test run doesn't use these properties.
        //   Corresponding properties are fed into 
        //   mock UrlHelper.
        SortModel = SortModel,
        ColumnText = "Make",
        SortProperty = "Make",
        PageAction = PageAction
    };

In the Act section we call the Process method of the SortColumnTagHelper instance and the output is captured in the tagHelperoutput variable of type TagHelperoutput .

// Act
sortColumnTagHelper.Process(tagHelperCtx, tagHelperoutput);

In the Assert section we use a C# StringWriter to capture the actual results of the tag helper output. Then we use an assert statement to check that the expected html results are equal to the actual results.

Assert.Equal(@"<a href=""/Vehicles/Page1?sortColumn=make&amp;sortOrder=desc"">Make</a>",
    tagHelperoutput.Attributes[0].Value);

You are free to choose which style suits you better between the two tag helper unit tests. Or create your own.


Unit Test Sorting in the Home Controller

Lastly, we need to unit test that the Home controller can sort Vehicle results.

Add the following test called Can_Sort_ByModel_Desc to the end of the tests in the HomeControllerTests.cs file .

[Fact]
public async Task Can_Sort_ByModel_Asc()
{
    // 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 = "Dodge",
            Model = "Dakatoa",
            VehicleType = new VehicleType
            {
                Id = 3,
                Name = "Truck"
            }
        },
        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 = 10;

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

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

    Assert.Equal("Cherokee", vehicleArray[0].Model);
    Assert.Equal("Takoma", vehicleArray[5].Model);
}

The unit test code above is similar to the tests we have written in previous sections and modules.

This test ensures the Home controller can sort in ascending order. First, as in previous tests, we build up our mocked IVehicleRepository but this time using a List<Vehicle> with test data more suited for testing sorting.

We instantiate a HomeController instance and pass it the mocked repo and set the PageSize to 10. We set the PageSize to 10 so that all of the results will come back in one page because we are not testing paging here, only sorting.

In the Act section we call the index method as usual passing 1 as the pageNumber argument, but also pass “model” as the sortColumn argument and “asc” for the sortOrder.

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

In the Assert section we convert the List<Vehicle> to an array as in earlier sections to make it easier to work with and check that the first Vehicle in the array has a model type of “Cherokee” and the last Vehicle in the array has a model type of “Takoma” as we would expect from the test data.

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

Assert.Equal("Cherokee", vehicleArray[0].Model);
Assert.Equal("Takoma", vehicleArray[5].Model);

And finally, we need to test sorting in descending order. Add the following test called Can_Sort_ByModel_Desc after Can_Sort_ByModel_Asc.

 [Fact]
 public async Task Can_Sort_ByModel_Desc()
 {
     // 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 = "Dodge",
             Model = "Dakatoa",
             VehicleType = new VehicleType
             {
                 Id = 3,
                 Name = "Truck"
             }
         },
         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 = 10;

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

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

     Assert.Equal("Takoma", vehicleArray[0].Model);
     Assert.Equal("Cherokee", vehicleArray[5].Model);
 }

This test is exactly the same as the ascending test except that we pass “desc” in the Act section to the Index method as the sortOrder argument and reverse the two assertions.

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

...

Assert.Equal("Takoma", vehicleArray[0].Model);
Assert.Equal("Cherokee", vehicleArray[5].Model);

What’s Next

In this section we added the sorting feature to our Vehicle results on the list page. We looked at multiple ways to go about it.

First we learned about the ViewData dictionary and used it to pass sorting information down to the view. Then we refactored and put the sorting information into the ViewModel making it strongly typed.

On the View side of things, first we used the built-in anchor tag helper to accomplish sorting by a column header. Then we refactored and built a custom tag helper to incorporate the needed C# logic to make a sortable column header in one place making it testable and resusable.

In the next module we are finally going to wire up those category buttons on the left side of the UI (User Interface) and let the user choose which type of Vehicle results they want to look at; Cars, Trucks, Jeeps, or All of them.

< 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