In Part 1 of this article series (called Use the MVVM Design Pattern in MVC Core: Part 1, you started using the Model-View-View-Model (MVVM) design pattern in MVC Core applications. In that article, you created an MVC Core application using VS Code and a few class library projects in which to put your entity, repository, and view model classes. Using the AdventureWorksLT database, you created a page to display product data in an HTML table. In addition, you wrote the appropriate code to search for products based on user input.

In this article, you're going to add on to the sample from the last article to sort the database when the user clicks on any of the column headers in the HTML table. You're going to learn how to add a pager to your HTML table so only a specified number of rows are displayed on the page. Finally, you learn to cache the product data in the Session object to improve performance.

Sort the Product Table

In most Web applications, when you have an HTML table of data, the user can click on the header above each column and sort the data within that column. If the user clicks on the same column header twice in a row, it first sorts the data in ascending, then in descending order. If the user clicks on a different column header, the sort direction should go back to ascending and the table should be sorted on the new column's data.

Add Sort Properties to ViewModel Base Class

The ViewModelBase class you created in the previous article is designed to be the base class for any view models you add to your project. As many pages you design might need the sorting functionality, add three new properties to the ViewModelBase class, as shown in the following code snippet:

public string SortDirection { get; set; }
public string SortExpression { get; set; }
public string PreviousSortExpression { get; set; }

Initialize the three properties you just added by adding code into the constructor, as shown below.

public ViewModelBase() 
{  
    EventCommand = string.Empty;  
    SortDirection = "asc";  
    SortExpression = string.Empty;  
    PreviousSortExpression = string.Empty;
}

You put either “asc” or “desc” to specify the sort direction into the SortDirection property. The SortExpression property holds the name of the column the user just clicked upon. The PreviousSortExpression property holds the name of the last column the user clicked upon. If the SortExpression property is equal to the PreviousSortExpression, change the SortDirection to the desc if its current value is asc, or to asc if its value is desc. After you change the SortDirection, put the SortExpression value into the PreviousSortExpression property. Open the ViewModelBase.cs file and add a new method named SetSortDirection() to perform this logic.

protected virtual void SetSortDirection() 
{
    if (SortExpression == PreviousSortExpression) 
    {
        // Toggle the sort direction if the field name is the same    
        SortDirection = (SortDirection == "asc" ? "desc" : "asc");  
    }  
    else 
    {
        SortDirection = "asc";  
    }
    // Set previous sort expression to new column  
    PreviousSortExpression = SortExpression;
}

Create Hidden Input Fields for New Properties

In the last article, you added a partial page with hidden input fields to hold the values of each property added to the ViewModelBase class. The EventCommand property is the only hidden input field in this partial page at this point. Open the _StandardViewModelHidden.cshtml file and add three more hidden input fields to hold the values of each of the three new properties you added to the ViewModelBase class. Modify the partial page to look like the following code snippet:

@model MVVMViewModelLayer.ViewModelBase
<input type="hidden" asp-for="EventCommand" />
<input type="hidden" asp-for="SortDirection" />
<input type="hidden" asp-for="SortExpression" />
<input type="hidden" asp-for="PreviousSortExpression" />

Create Clickable Column Headers

You need to change each column header to hyperlinks so the user can click on them. To do this, use an anchor tag with a couple of data- attributes added to pass data to the view model. Add an attribute named data-custom-cmd and set its value equal to sort. This tells the view model what operation to perform. Add another attribute named data-custom-arg and set its value to the property name to sort upon. This value will be different for each column the user clicks on. Open the partial page named _ProductList.cshtml and add the links to each of the column headers, as shown in Listing 1.

<th>
    <a href="#" data-custom-cmd="sort" data-custom-arg="Name">Product Name</a>
</th>
<th>
    <a href="#" data-custom-cmd="sort" data-custom-arg="ProductNumber">Product Number</a>
</th>
<th class="text-right">
    <a href="#" data-custom-cmd="sort" data-custom-arg="StandardCost">Cost</a>
</th>
<th class="text-right">
    <a href="#" data-custom-cmd="sort" data-custom-arg="ListPrice">Price</a>
</th>

Set the SortExpression Property Using jQuery

In the last article, you added a $(document).ready() function to connect up a click event to any HTML element that has a data-custom-cmd attribute. Within that code, you retrieve the value in the data-custom-cmd attribute and put it into the hidden input field that is bound to the EventCommand property in your view model. If the EventCommand property is set to “sort”, retrieve the value in the data-custom-arg attribute and put that value into the hidden input field bound to the SortExpression property. Open the Products.cshtml and locate the $(document).ready() function at the bottom of the file. After the line of code that updates the $("#EventCommand") hidden input field add an If statement and the code within it as shown below.

// $(document).ready()
// Fill in command to post back to view model
$("#EventCommand").val($(this).data("custom-cmd"));

// Only set sort variables if command was "sort"      
if($("#EventCommand").val() == "sort") {  
    // Get the new sort expression  
    $("#SortExpression").val($(this).data("custom-arg"));
}

Add SortProducts Method

Now that you have the EventCommand and SortExpression properties filled in with the proper values, and you have code written in the SetSortDirection() method to set the SortDirection and PreviousSortExpression properties, it's time to do the actual sorting. Open the ProductViewModel.cs file and add a SortProducts() method, as shown in Listing 2.

Listing 2: For each column in your table you need code to sort data based on that field.

protected virtual void SortProducts() 
{  
    // Search for Products  
    SearchProducts();
    
    if (EventCommand == "sort") 
    {
        // Set sort direction
        SetSortDirection();
    }
    
    // Determine sort direction  
    bool isAscending = SortDirection == "asc";    
    
    // What field should we sort on?  
    switch (SortExpression.ToLower()) 
    {
        case "name":
            if (isAscending) 
            {
                Products = Products.OrderBy(p => p.Name).ToList();
            } 
            else 
            {
                Products = Products.OrderByDescending(p => p.Name).ToList();
            }
            break;
        
        case "productnumber":
            if (isAscending) 
            {
                Products = Products.OrderBy(p => p.ProductNumber).ToList();
            } 
            else 
            {
                Products = Products.OrderByDescending(p => p.ProductNumber).ToList();      
            }
            break;
            
        case "standardcost":
            if (isAscending) 
            {
                Products = Products.OrderBy(p => p.StandardCost).ToList();
            } 
            else 
            {
                Products = Products.OrderByDescending(p => p.StandardCost).ToList();
            }
            break;
        
        case "listprice":
            if (isAscending) 
            {
                Products = Products.OrderBy(p => p.ListPrice).ToList();
            } 
            else 
            {
                Products = Products.OrderByDescending(p => p.ListPrice).ToList();
            }      
            break;
    }
}

The first thing the SortProducts() method does is to retrieve the product data from the data repository by calling the SearchProducts() method. Once the Products collection has been filled in with the product data, call the SetSortDirection() method. Check the SortDirection property to see if it is set to “asc” or “desc”. Set a Boolean variable appropriately. In the switch() expression, compare the value in the SortExpression property against each case statement. Once you have a match on the column name, check the Boolean variable to see if you should apply the OrderBy() or OrderByDescending() methods to the Products collection.

Add Sort Command to HandleRequest() Method

The HandleRequest() method is the public method called from the controller. This method is where you check the EventCommand property to decide what method(s) to call in the view model to set the view model properties for displaying on the screen. You just passed in a new command with a value of “sort”, so you need to add a new case statement just above the “search” case. Modify the method called from SearchProducts() to SortProducts().

case "sort":
case "search":
  SortProducts();
  break;

Set Sort Order in Controller

When the user first enters your product list page, show them the products sorted by the Name property. Open the ProductsController.cs file and add two lines of code to the Products() method, as shown in the code snippet below.

public IActionResult Products()
{
    // Load products  
    _viewModel.SortExpression = "Name";  
    _viewModel.EventCommand = "sort";  
    _viewModel.HandleRequest();
    return View(_viewModel);
}

The first line of code sets the SortExpression property to “Name”. The second line of code sets the EventCommand property to “sort”. When the HandleRequest() method is called, the SortProducts() method is called because of the value in the EventCommand property. Because the value in the PreviousSortExpression property is blank and the value in the SortExpression property is “Name”, the two values are not equal. This causes the SortDirection property to be set to “asc”. Because of the SortExpression and SortDirection, the switch statement causes the Products collection to be ordered by the Name property in an ascending order.

Reset Hidden Fields After Postback

When the user clicks on another column header and forces a post-back with new values to sort by, the [HttpPost] Products() method is called. After the HandleRequest() method is called, there are new values in the SortExpression, PreviousSortExpression and SortDirection properties. To update the hidden input fields, call the ModelState.Clear() method as shown in the code snippet below.

[HttpPost]
public IActionResult Products(ProductViewModel vm)
{
    vm.Repository = _repo;  
    vm.HandleRequest();
    ModelState.Clear();
    return View(vm);
}

Try It Out

Now that you have code written to perform sorting, it is time to try it out. Run the Web application and click on the different column headers. Try clicking on different headers to see the data sort in ascending order for each column. Then try clicking on the same column header to see the data swap between ascending and descending order for that column of data.

Create a Common Library Project

The next step is to add paging to your table. In order to do this, you need some classes to help with the paging. These classes are in the download for this article under a folder called PagerClasses. Please download the samples now so you can continue following along.

These pager classes you downloaded belong in another class library that you are naming CommonLibrary. This class library will be for any generic classes that can be used in any type of application. To create the CommonLibrary project, open a terminal window by clicking on the Terminal > New Terminal menu. Go to your development directory (in my case, that was D:\Samples). Type in the following commands to create a new folder named CommonLibrary.

MD CommonLibrary
CD CommonLibrary
dotnet new classlib

The dotnet new classlib command creates a class library project with the minimum number of references to .NET Core libraries. Now that you have created this new project, add it to your Visual Studio Code workspace by clicking the File > Add Folder to Workspace... menu. Select the CommonLibrary folder and click on the Add button. You should see the CommonLibrary project added to your VS Code workspace. Delete the Class1.cs file, as you're not going to need that. Copy the \PagerClasses folder into the CommonLibrary folder so it now appears in your VS Code workspace.

Add References to the Common Library

This CommonLibrary needs to be referenced from both the MVVMViewModelLayer and the MVVMSample projects. Click on the Terminal > New Terminal menu and select the MVVMSample folder. Set a reference to the CommonLibrary project using the following command.

dotnet add . reference ../CommonLibrary/CommonLibrary.csproj

Change the directory to the MVVMViewModelLayer folder and execute the following command to reference the CommonLibrary in this project as well.

dotnet add . reference ../CommonLibrary/CommonLibrary.csproj

Try It Out

To ensure that you've typed in everything correctly, run a build task to compile the projects. Because you have a reference from the MVVMSample to the CommonLibrary, if you run a build task on the MVVMSample project, it builds all of the other projects, including the CommonLibrary. Select the Terminal > Run Build Task... menu and select the MVVMSample project. Watch the output in the terminal window and you should see that it compiles all five of your projects.

Paging

Instead of displaying hundreds of product rows in a table and forcing the user to scroll down on your Web page, add a pager to your table like the one shown at the bottom of Figure 1. There are two pieces to adding a pager to your table: the first is the actual pager UI, and the second is the calculation and selection of which rows are on each page. All the code to do the calculations and create a set of pager items used to display the pager are contained in the classes in the \PageClasses folder you just added to the CommonLibrary project. I'm not going to cover how these classes work, as that is beyond the scope of this article. I am going to show you how to use them to create the pager shown in Figure 1.

Figure 1: Add a pager below your table to keep the number of records displayed at one time to a minimum.
Figure 1: Add a pager below your table to keep the number of records displayed at one time to a minimum.

To create a pager that looks like what you see in Figure 1, use Bootstrap and the HTML shown in Listing 3. Add the data-custom-cmd and data-custom-arg attributes to each anchor tag to set properties in the view model class. The data-custom-arg attribute is going to be used to set a property named EventArgument in the ViewModelBase class. In the previous section on sorting, you used the data-custom-arg attribute to set the SortExpression property. For paging, you are going to set the EventArgument property from the data-custom-arg attribute and use that to tell the pager how to page through the data.

Listing 3: Create a pager using HTML and Bootstrap

<ul class="pagination">  
    <li class="page-item disabled">    
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="first">&laquo;</a>
    </li>
    <li class="page-item disabled">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="prev">&lsaquo;</a>
    </li>
    <li class="page-item active">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="0">1</a>
    </li>
    <li class="page-item">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="1">2</a>
    </li>
    <li class="page-item">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="10">...</a>
    </li>
    <li class="page-item">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="next">&rsaquo;</a>
    </li>
    <li class="page-item">
        <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="last">&raquo;</a>
    </li>
</ul>

Add Paging Properties to View Model Base Class

Open the ViewModelBase.cs file and add a few new properties to help with paging. The first one, EventArgument, receives the action to perform when paging. For example, this can be set to “next”, “previous”, “first”, “last”, or a specific page number, as you can see by looking at the “data-custom-arg” attributes in Listing 3. The next property, Pager, is of the type Pager, which is one of the classes you downloaded and added to the CommonLibrary project from the PagerClasses folder. This class contains properties such as PageSize, VisiblePagesToDisplay, PageIndex, StartingRow, TotalPages, and TotalRecords. The last property to add to the view model, Pages, is a collection of PagerItem objects. A PagerItem object represents a single visible anchor tag in the pager displayed at the bottom of the table. Each PagerItem object contains properties such as Text, Tooltip, Argument, and CssClass.

public string EventArgument { get; set; }
public Pager Pager { get; set; }
public PagerItemCollection Pages { get; set; }

After adding these new properties, initialize the EventArgument and Pager properties in the constructor of the ViewModelBase class using the code shown in the following code snippet:

public ViewModelBase() 
{
    EventCommand = string.Empty;  
    EventArgument = string.Empty;  
    Pager = new Pager();  
    SortDirection = "asc";  
    SortExpression = string.Empty;  
    PreviousSortExpression = string.Empty;
}

Add a new method named SetPagerObject() to the ViewModelBase class. This method accepts the total amount of records in the Product table. This parameter is passed to the Pager.TotalRecords property. The TotalPages property in the Pager object is calculated from the TotalRecords and the PageSize properties. PageSize has a default size of ten. If the TotalRecords is set to 50, the total pager objects to show on the UI is five.

protected virtual void SetPagerObject(int totalRecords) 
{
    // Set Pager Information  
    Pager.TotalRecords = totalRecords;
    
    // Set Pager Properties  
    Pager.SetPagerProperties(EventArgument);
    
    // Build paging collection  
    Pages = new PagerItemCollection(Pager);
}

Next, the SetPagerProperties() method is called and passed the EventArgument property. Remember the EventArgument is set to “next”, “previous”, or similar commands depending on which anchor tag the user clicked upon. From this command, the PageIndex property in the Pager object is set.

Now that you have the properties in the Pager object calculated, pass this Pager object to the PagerItemCollection object. By reading the TotalRecords, the TotalPages and the PageIndex properties in the Pager object, the PagerItemCollection builds the collection of pages used to display the anchor tags.

Create Pager UI on the Product List Page

To build the pager that the user sees on the list page, open the _ProductList.cshtml file and add a using statement at the top of the file to reference the CommonLibrary.PagerClasses namespace. It's in this namespace that the Pager classes are located.

@using CommonLibrary.PagerClasses

Move to the bottom of this file and add the code in this next snippet. This code is what takes the Pages property you just created in the SetPagerObject() method and builds each hyperlink you see in the pager at the bottom of Figure 1. Feel free to step through the code in the PagerItemCollection object to see how this pager is built.

<ul class="pagination">  
    @foreach (PagerItem item in Model.Pages) 
    {
        <li class="page-item @item.CssClass">
            <a class="page-link" href="#" data-custom-cmd="page" data-custom-arg="@item.Argument" title="@item.Tooltip">@Html.Raw(item.Text)</a>
        </li>  
    }
</ul>

Get One Page of Data from the Products Collection

Before you can try out the paging, you have a few more pieces of code to write. First, open the ProductViewModel.cs file and add a using statement at the top of the file.

using CommonLibrary.PagerClasses;

Within the ProductViewModel class, add a new property to hold onto the total amount of product records you read from the table. The TotalProducts property is used to display how many records the user selected and display it on the page.

public int TotalProducts { get; set; }

Add a new method named PageProducts() that sets the TotalProducts property, then calls the SetPagerObject() method in the base class. Once the Pager object has been set up, you now need to get just the data for the current page of data. The starting row in the Products collection can be calculated by taking the current PageIndex property and multiplying that number by the PageSize property. Pass this result to the LINQ Skip() method on the Products collection. Then take the next PageSize number of records from the collection.

protected virtual void PageProducts() 
{
    TotalProducts = Products.Count;
    base.SetPagerObject(TotalProducts);
    Products = Products.Skip(base.Pager.PageIndex * base.Pager.PageSize).Take(base.Pager.PageSize).ToList();
}

Locate the HandleRequest() method and modify the switch statement to handle the different commands. Modify the search command to reset the PageIndex property. Add a page command for when the user clicks on one of the pager anchor tags. Call the PageProducts() method after the call to the SortProducts() method in both the search and page case statements.

case "search":
    Pager.PageIndex = 0;
    SortProducts();
    PageProducts();
    break;
case "sort":
case "page"
    SortProducts();
    PageProducts();
    break;

Add Hidden Input Fields for Paging

Open the _StandardViewModelHidden.cshtml file and add two new hidden input fields. You need to post-back both the EventArgument and the current PageIndex values. The EventArgument is going to have the “data-custom-arg” value from the pager and the PageIndex is the page the user is currently viewing.

<input type="hidden" asp-for="EventArgument" />
<input type="hidden" asp-for="Pager.PageIndex" />

Add Total Records to Products Search Page

You added the TotalProducts property to the ProductViewModel class. It's now time to display that on the page. Put the following code within the card-footer in the _ProductSearch.cshtml file.

<div class="card-footer bg-primary text-light">
    <div class="row">
        <div class="col-8">
            <button type="button" data-custom-cmd="search" class="btn btn-success">Search</button>
        </div>
        <div class="col-4">
            <p class="text-right">Total Records: @Model.TotalProducts</p>
        </div>
    </div>
</div>

Set the EventArgument Property Using jQuery

The last thing you need to do before trying out the paging is to take the data-custom-arg attribute value and place it into the new hidden input field you just added. Open the Products.cshtml file and locate the $(document).ready() function and after the line of code that updates the $("#EventCommand") hidden input field, add a new line of code to set the EventArgument as shown in the code snippet below:

// The $(document).ready() function
// Fill in command to post back to view model
$("#EventCommand").val($(this).data("custom-cmd"));

// Fill in arguments to post back to view model
$("#EventArgument").val($(this).data("custom-arg"));

Try It Out

You're now ready to try out the paging to ensure that you typed everything in correctly. Run the application and try clicking on the various anchor tags on the pager UI.

Changing the Page Size

By default, the PageSize property in the Pager object is set to 10. If you wish to adjust the size, modify the PageSize property in the controller. Open the ProductsController.cs file and, in the Products() method, add the following line of code just before the call to the HandleRequest() method.

_viewModel.Pager.PageSize = 5;

Also, in the [HttpPost] Products() method, add the following line of code just before the call to the HandleRequest() method.

vm.Pager.PageSize = 5;

Try It Out

Run the application again and you should see only five rows of data appear on each page in the table.

Cache the Products List

As you may have noticed, the Products are read from the database each time you sort or page through the data. Having to retrieve the data every time from the data is very inefficient and this process can be fixed easily. If your product data does not change that often, cache the data in your Web server's memory. There are many methods you may employ for caching; for this article, let's use the Session object.

Add Property to Hold all the Product Data

In the Product view model class, there is a Products collection to hold the data for the one page of data to display. To avoid a round-trip to your SQL Server to get all your product data, add another property to hold the original list of product data. The list of all product data is placed into your cache after retrieving the data the first time. The data is retrieved from the cache on each post-back to populate the Products collection when the user requests the next page of data to display. Open the ProductViewModel.cs file and add a new property named AllProducts.

public List<Product> AllProducts { get; set; }

Each time the user posts back to the controller to either search, sort, or page, the SearchProducts() method is called to retrieve the data from the Repository class. In this method, modify the code to use the data in the AllProducts collection if that collection has been retrieved from the cache.

Locate the SearchProducts() method and modify it to look like Listing 4. In SearchProducts(), check if this is the first time the user has hit the product list page. If the AllProducts collection is null, then call the Search() method on the Repository class to get all of the product data from the database server and store that data into the AllProducts property.

Listing 4: Modify the SearchProducts() method to use the cached data

public virtual void SearchProducts() 
{
    if (AllProducts == null) 
    {
        if (Repository == null) 
        {
            throw new ApplicationException("Must set the Repository property.");
        } 
        else 
        {
            // Get data from Repository
            AllProducts = Repository.Search(SearchEntity).OrderBy(p => p.Name).ToList();
        }  
    }  
    
    // Get data for the Products collection  
    Products = AllProducts.Where(p => (SearchEntity.Name == null || p.Name.StartsWith(SearchEntity.Name)) && p.ListPrice >= SearchEntity.ListPrice).ToList();
}

In the Products() method in the ProductsController class, you're going to store the AllProducts collection into the Session object. You're then going to retrieve the data from the Session object in the [HttpPost] Products() method and put that data back into the AllProducts collection. When the SearchProducts() method is called the second time, the AllProducts collection is not null and thus the Products collection is built by querying the AllProducts collection instead of going to the database server.

Add Session State to MVC Core

As mentioned, you are going to use the Session state object that's built-in to MVC Core. In order to use Session, you must configure it in the Startup class. Open the Startup.cs file and locate the InjectAppServices() method you created in the last article. After the line of code services.AddDbContext(), add the following two lines of code to add services to support session state.

// The next two lines are for session state
services.AddDistributedMemoryCache();
services.AddSession();

Next, locate the Configure() method and find the line of code app.UseRouting(). After this line of code, add the following line of code to turn on session state.

app.UseSession();

Add Newtonsoft Package

The Session object in MVC Core can only store strings or integers. However, you need to put in a collection of Product objects. In order to do this, you need to serialize the collection into JSON. This can be done using the Newtonsoft package. Open a terminal window in the MVVMSample folder and execute the following command to bring in the Newtonsoft library.

dotnet add package Newtonsoft.Json

Store AllProducts Collection into Session

It's now time to cache the data you placed into the AllProducts collection. Open the ProductsController.cs file and add some Using statements to the top of the file.

using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using MVVMEntityLayer;
using NewtonSoft.Json;

Locate the Products() method and after the call to HandleRequest() has been called, get the data from the AllProducts collection, serialize it, and place it into the Session object using the SetString() method as shown in the code below.

HttpContext.Session.SetString("Products",  JsonConvert.SerializeObject(_viewModel.AllProducts));

When the user clicks on any of the links that post back to the controller, the [HttpPost] Products() method is called. Place the following line of code just before the call to the HandleRequest() method to populate the AllProducts collection with the data you stored into the Session object.

vm.AllProducts = JsonConvert.DeserializeObject<List<Product>>(HttpContext.Session.GetString("Products"));

When the HandleRequest() method is called, the AllProducts collection has data in it, and thus in the SearchProducts() method, the Products collection is built from the data in this collection instead of calling the database server.

Try It Out

Run the application and try out the various features of the page. Try searching for data, try sorting the data, and try paging through the data. If you set a breakpoint, in the SearchProducts() method, you can see that it only fetches the data from the Repository class the first time into the page. After that, the data is always coming from the cached data.

Summary

In this article, you learned how to add hyperlinks to each column header in a table to allow sorting of product data. Next, you added paging capabilities to the table, so the user only sees a limited amount of data at a time. This is a better user experience than having a large scrollable table. Finally, you learned how to use the Session object in order to avoid a round-trip to the database server each time the user clicks on something on the page. This saves time and resources and makes updating your page much quicker. The great thing about using the MVVM design pattern is the code in your controller is still very simple, and your view model class need only expose one public method. In the next article (Use the MVVM Design Pattern in MVC Core: Part 3) you'll learn to add, edit, and delete product data using the MVVM design pattern.