ASP.NET MVC Paging/Sorting/Filtering using the MVCContrib Grid and Pager

This post walks you through creating a UI for paging, sorting and filtering a list of data items. It makes use of the excellent MVCContrib Grid and Pager Html UI helpers. A sample project is attached at the bottom.

Our UI will eventually look like this. The application will make use of the Northwind database.

image
The top portion of the page has a filter area region. The filter region is enclosed in a form tag. The select lists are wired up with jQuery to auto post back the form.

The page has a pager region at the top and bottom of the product list.

The product list has a link to display more details about a given product. The column headings are clickable for sorting and an icon shows the sort direction.

A request with this UI will look similar to this : /Products?productName=chai&supplierId=29&categoryId=4&Column=CategoryName&Direction=Ascending. This request translates into a request for products that contain the text “chai” and supplierid=29 and categoryid=29. The results are sorted in ascending order by categoryname.

Strongly Typed View Models

The views are written to expect strongly typed objects. We suffix these strongly typed objects with ViewModel and will be used for passing data down to the view. 

The following listing shows the ProductViewModel. This class will be used to hold information about a Product. We use attributes to specify if the property should be hidden and what its heading in the table should be. This metadata will be used by the MvcContrib Grid to render the table. Some of the properties are hidden from the UI ([ScaffoldColumn(false)) but are needed because we will be using those for filtering when writing our LINQ query.
public class ProductViewModel
{
    [ScaffoldColumn(false)]
    public int? ProductID { get; set; }
 
    public string ProductName { get; set; }
 
    [DisplayName("Unit Price")]
    [DisplayFormat(DataFormatString = "{0:c}")] 
    public System.Nullable<decimal> UnitPrice { get; set; }
 
    [DisplayName("Category Name")]
    public string CategoryName { get; set; }
 
    [ScaffoldColumn(false)]
    public int? CategoryID { get; set; }
 
    [ScaffoldColumn(false)]
    public int? SupplierID { get; set; }
 
    public bool Discontinued { get; set; }
}

The following diagram shows the rest of the key ViewModels in our design.

image
We have a container class called ProductListContainerViewModel which has nested classes.

The ProductPagedList is of type IPagination<ProductViewModel>. The MvcContrib expects the IPagination<T> interface to determine the page number and page size of the collection we are working with. You convert any IEnumerable<T> into an IPagination<T> by calling the AsPagination extension method in the MvcContrib library. It also creates a paged set of type ProductViewModel.

The ProductFilterViewModel class holds information about the different select lists and the ProductName being searched on. It also holds the state of any previously selected item in the lists and the previous search criteria (you will recall that this type of state information was stored in Viewstate when working with WebForms). With MVC there is no default state storage and so all state has to be fetched and passed back to the view.

The GridSortOptions is a type defined in the MvcContrib library and is used by the Grid to determine the current column being sorted on and the current sort direction.


The following shows the view and partial views used to render our UI. The Index view expects a Model of type ProductListContainerViewModel.

image
Index.aspx code:

<%Html.RenderPartial("SearchFilters", Model.ProductFilterViewModel); %>
<% Html.RenderPartial("Pager", Model.ProductPagedList); %>
<% Html.RenderPartial("SearchResults", Model); %>
<% Html.RenderPartial("Pager", Model.ProductPagedList); %>

The View contains a partial view “SearchFilters” and passes it the ProductViewFilterContainer. The SearchFilter uses this Model to render all the search lists and textbox.

The partial view “Pager” uses the ProductPageList which implements the interface IPagination. The “Pager” view contains the MvcContrib Pager helper used to render the paging information. This view is repeated twice since we want the pager UI to be available at the top and bottom of the product list. The Pager partial view is located in the Shared directory so that it can be reused across Views.

The partial view “SearchResults” uses the ProductListContainer model. This partial view contains the MvcContrib Grid which needs both the ProdctPagedList and GridSortOptions to render itself.

The Controller Action 
The application receives this GET request and maps it to the Index method of the ProductController.
Within the action we create an IQueryable<ProductViewModel> by calling the GetProductsProjected() method.

/// <summary>
/// This method takes in a filter list, paging/sort options and applies
/// them to an IQueryable of type ProductViewModel
/// </summary>
/// <returns>
/// The return object is a container that holds the sorted/paged list,
/// state for the fiters and state about the current sorted column
/// </returns>
public ActionResult Index(
    string productName, 
    int? supplierID, 
    int? categoryID, 
    GridSortOptions gridSortOptions, 
    int? page)
{
 
    var productList = productRepository.GetProductsProjected();
 
    // Set default sort column
    if (string.IsNullOrWhiteSpace(gridSortOptions.Column))
    {
        gridSortOptions.Column = "ProductID";
    }
 
    // Filter on SupplierID 
    if (supplierID.HasValue)
    {
        productList.Where(a => a.SupplierID == supplierID);
    }
 
    // Filter on CategoryID 
    if (categoryID.HasValue)
    {
        productList = productList.Where(a => a.CategoryID == categoryID);
    }
 
    // Filter on ProductName
    if (!string.IsNullOrWhiteSpace(productName))
    {
        productList = productList.Where(a => a.ProductName.Contains(productName));
    }
 
    // Create all filter data and set current values if any
    // These values will be used to set the state of the select list and textbox
    // by sending it back to the view.
    var productFilterViewModel = new ProductFilterViewModel();
    productFilterViewModel.SelectedCategoryID = categoryID ?? -1;
    productFilterViewModel.SelectedSupplierID = supplierID ?? -1;
    productFilterViewModel.Fill();
 
    // Order and page the product list
    var productPagedList = productList
           .OrderBy(gridSortOptions.Column, gridSortOptions.Direction)
           .AsPagination(page ?? 1, 10);
 
 
    var productListContainer = new ProductListContainerViewModel
    {
        ProductPagedList = productPagedList,
        ProductFilterViewModel = productFilterViewModel,
        GridSortOptions = gridSortOptions
    };
 
    return View(productListContainer);
}

The supplier, category and productname filters are applied to this IQueryable if any are present in the request. The ProductPagedList class is created by applying a sort order and calling the AsPagination method. Finally, the ProductListContainerViewModel class is created and returned to the view.

You have seen how to use strongly typed views with the MvcContrib Grid and Pager to render a clean lightweight UI with strongly typed views. You also saw how to use partial views to get data from the strongly typed model passed to it from the parent view. The code also shows you how to use jQuery to auto post back.

The nice thing about the MvcContrib Pager and Grid is that it is aware of the original query string and keeps it intact when creating links for sorting and paging.


The sample is attached below. Don’t forget to change your connection string to point to the server containing the Northwind database.


Related Posts:

My name is Kobayashi. I work for Keyser Soze.

2 Comments

Comments have been disabled for this content.