This post looks at how to control paging, sorting and filtering when displaying a list of data by specifying attributes in your Model using the ASP.NET MVC framework and the excellent MVCContrib library. It also shows how to hide/show columns and control the formatting of data using attributes. This uses the Northwind database. A sample project is attached at the end of this post. Let’s start by looking at a class called ProductViewModel. The properties in the class are decorated with attributes. The OrderBy attribute tells the system that the Model can be sorted using that property. The SearchFilter attribute tells the system that filtering is allowed on that property. Filtering type is set by the FilterType enum which currently supports Equals and Contains. The ScaffoldColumn property specifies if a column is hidden or not The DisplayFormat specifies how the data is formatted. public class ProductViewModel
{
[OrderBy(IsDefault = true)]
[ScaffoldColumn(false)]
public int? ProductID { get; set; }
[SearchFilter(FilterType.Contains)]
[OrderBy]
[DisplayName("Product Name")]
public string ProductName { get; set; }
[OrderBy]
[DisplayName("Unit Price")]
[DisplayFormat(DataFormatString = "{0:c}")]
public System.Nullable<decimal> UnitPrice { get; set; }
[DisplayName("Category Name")]
public string CategoryName { get; set; }
[SearchFilter]
[ScaffoldColumn(false)]
public int? CategoryID { get; set; }
[SearchFilter]
[ScaffoldColumn(false)]
public int? SupplierID { get; set; }
[OrderBy]
public bool Discontinued { get; set; }
}
Before we explore the code further, lets look at the UI.
The UI has a section for filtering the data. The column headers with links are sortable. Paging is also supported with the help of a pager row. The pager is rendered using the MVCContrib Pager component. The data is displayed using a customized version of the MVCContrib Grid component. The customization was done in order for the Grid to be aware of the attributes mentioned above.
Now, let’s look at what happens when we perform actions on this page. The diagram below shows the process:
The form on the page has its method set to “GET” therefore we see all the parameters in the query string. The query string is shown in blue above. This query gets routed to an action called Index with parameters of type ProductViewModel and PageSortOptions.
The parameters in the query string get mapped to the input parameters using model binding. The ProductView object created has the information needed to filter data while the PageAndSorting object is used for paging and sorting the data.
The last block in the figure above shows how the filtered and paged list is created.
We receive a product list from our product repository (which is of type IQueryable) and first filter it by calliing the AsFiltered extension method passing in the productFilters object and then call the AsPagination extension method passing in the pageSort object.
The AsFiltered extension method looks at the type of the filter instance passed in. It skips properties in the instance that do not have the SearchFilter attribute. For properties that have the SearchFilter attribute, it adds filter expression trees to filter against the IQueryable data.
The AsPagination extension method looks at the type of the IQueryable and ensures that the column being sorted on has the OrderBy attribute. If it does not find one, it looks for the default sort field [OrderBy(IsDefault = true)]. It is required that at least one attribute in your model has the [OrderBy(IsDefault = true)]. This because a person could be performing paging without specifying an order by column. As you may recall the LINQ Skip method now requires that you call an OrderBy method before it. Therefore we need a default order by column to perform paging. The extension method adds a order expressoin tree to the IQueryable and calls the MVCContrib AsPagination extension method to page the data.
Implementation Notes
Auto Postback
The search filter region auto performs a get request anytime the dropdown selection is changed. This is implemented using the following jQuery snippet
$(document).ready(function () {
$("#productSearch").change(function () {
this.submit();
});
});
Strongly Typed View
The code used in the Action method is shown below:
public ActionResult Index(ProductViewModel productFilters, PageSortOptions pageSortOptions)
{
var productPagedList = productRepository.GetProductsProjected().AsFiltered(productFilters).AsPagination(pageSortOptions);
var productViewFilterContainer = new ProductViewFilterContainer();
productViewFilterContainer.Fill(productFilters.CategoryID, productFilters.SupplierID, productFilters.ProductName);
var gridSortOptions = new GridSortOptions { Column = pageSortOptions.Column, Direction = pageSortOptions.Direction };
var productListContainer = new ProductListContainerModel
{
ProductPagedList = productPagedList,
ProductViewFilterContainer = productViewFilterContainer,
GridSortOptions = gridSortOptions
};
return View(productListContainer);
}
As you see above, the object that is returned to the view is of type ProductListContainerModel. This contains all the information need for the view to render the Search filter section (including dropdowns), the Html.Pager (MVCContrib) and the Html.Grid (from MVCContrib). It also stores the state of the search filters so that they can recreate themselves when the page reloads (Viewstate, I miss you! :0)
The class diagram for the container class is shown below.
Custom MVCContrib Grid
The MVCContrib grid default behavior was overridden so that it would auto generate the columns and format the columns based on the metadata and also make it aware of our custom attributes (see MetaDataGridModel in the sample code).
The Grid ensures that the ShowForDisplay on the column is set to true
This can also be set by the ScaffoldColumn attribute ref: http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-2-modelmetadata.html)
Column headers are set using the DisplayName attribute
Column sorting is set using the OrderBy attribute.
The data is formatted using the DisplayFormat attribute.
Generic Extension methods for Sorting and Filtering
The extension method AsFiltered takes in an IQueryable<T> and uses expression trees to query against the IQueryable data. The query is constructed using the Model metadata and the properties of the T filter (productFilters in our case). Properties in the Model that do not have the SearchFilter attribute are skipped when creating the filter expression tree. It returns an IQueryable<T>.
The extension method AsPagination takes in an IQuerable<T> and first ensures that the column being sorted on has the OrderBy attribute. If not, we look for the default OrderBy column ([OrderBy(IsDefault = true)]). We then build an expression tree to sort on this column. We finally hand off the call to the MVCContrib AsPagination which returns an IPagination<T>.
This type as you can see in the class diagram above is passed to the view and used by the MVCContrib Grid and Pager components.
Custom Provider
To get the system to recognize our custom attributes, we create our MetadataProvider as mentioned in this article (http://bradwilson.typepad.com/blog/2010/01/why-you-dont-need-modelmetadataattributes.html)
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
SearchFilterAttribute searchFilterAttribute = attributes.OfType<SearchFilterAttribute>().FirstOrDefault();
if (searchFilterAttribute != null)
{
metadata.AdditionalValues.Add(Globals.SearchFilterAttributeKey, searchFilterAttribute);
}
OrderByAttribute orderByAttribute = attributes.OfType<OrderByAttribute>().FirstOrDefault();
if (orderByAttribute != null)
{
metadata.AdditionalValues.Add(Globals.OrderByAttributeKey, orderByAttribute);
}
return metadata;
}
We register our MetadataProvider in Global.asax.cs.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new MvcFlan.QueryModelMetaDataProvider();
}
Bugs, Comments and Suggestions are welcome!
You can download the sample code below.
This code is purely experimental. Use at your own risk.
Download Sample Code (VS 2010 RTM)
MVCNorthwindSales.zip