ASP.NET MVC Paging/Sorting/Filtering using the MVCContrib Grid and Pager
- by rajbk
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. 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. Strongly Typed View Models The views are written to expect strongly typed objects. We suffix these strongly typed objects with ViewModel since they are designed specifically 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 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 = 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 following diagram shows the rest of the key ViewModels in our design.
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 will hold information about the different select lists and the ProductName being searched on. It will also hold 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 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 type ProductListContainerViewModel which we described earlier.
<%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
An example of a request like this: /Products?productName=test&supplierId=29&categoryId=4. 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 sample is attached below. Don’t forget to change your connection string to point to the server containing the Northwind database.
NorthwindSales_MvcContrib.zip
My name is Kobayashi. I work for Keyser Soze.