Tuesday, August 02, 2011

WebGrid in ASP.NET MVC

Earlier this year Microsoft released ASP.NET MVC version 3 (asp.net/mvc), as well as a new product called WebMatrix (asp.net/webmatrix). The WebMatrix release included a number of productivity helpers to simplify tasks such as rendering charts and tabular data. One of these helpers, WebGrid, lets you render tabular data in a very simple manner with support for custom formatting of columns, paging, sorting and asynchronous updates via AJAX.
In this article, I’ll introduce WebGrid and show how it can be used in ASP.NET MVC 3, then take a look at how to really get the most out of it in an ASP.NET MVC solution. (For an overview of WebMatrix—and the Razor syntax that will be used in this article—see Clark Sell’s article, “Introduction to WebMatrix,” in the April 2011 issue at msdn.microsoft.com/magazine/gg983489).
This article looks at how to fit the WebGrid component into an ASP.NET MVC environment to enable you to be productive when rendering tabular data. I’ll be focusing on WebGrid from an ASP.NET MVC aspect: creating a strongly typed version of WebGrid with full IntelliSense, hooking into the WebGrid support for server-side paging and adding AJAX functionality that degrades gracefully when scripting is disabled. The working samples build on top of a service that provides access to the AdventureWorksLT database via the Entity Framework. If you’re interested in the data-access code, it’s available in the code download, and you might also want to check out Julie Lerman’s article, “Server-Side Paging with the Entity Framework and ASP.NET MVC 3,” in the March 2011 issue (msdn.microsoft.com/magazine/gg650669).

Getting Started with WebGrid

To show a simple example of WebGrid, I’ve set up an ASP.NET MVC action that simply passes an IEnumerable to the view. I’m using the Razor view engine for most of this article, but later I’ll also discuss how the WebForms view engine can be used. My ProductController class has the following action:
  1. public ActionResult List()
  2.   {
  3.     IEnumerable model =
  4.       _productService.GetProducts();
  5.  
  6.     return View(model);
  7.   }
The List view includes the following Razor code, which renders the grid shown in Figure 1:
  1. @model IEnumerable< span="">.Domain.Product>
  2. @{
  3.   ViewBag.Title = "Basic Web Grid";
  4. }
  5. < span="">>Basic Web Grid

  6. < span="">>
  7. @{
  8.   var grid = new WebGrid(Model, defaultSort:"Name");
  9. }
  10. @grid.GetHtml()
A Basic Rendered Web Grid
(click to zoom)

Figure 1 A Basic Rendered Web Grid
The first line of the view specifies the model type (for example, the type of the Model property that we access in the view) to be IEnumerable. Inside the div element I then instantiate a WebGrid, passing in the model data; I do this inside an @{...} code block so that Razor knows not to try to render the result. In the constructor I also set the defaultSort parameter to “Name” so theWebGrid knows that the data passed to it is already sorted by Name. Finally, I use @grid.GetHtml() to generate the HTML for the grid and render it into the response.
This small amount of code provides rich grid functionality. The grid limits the amount of data displayed and includes pager links to move through the data; column headings are rendered as links to enable paging. You can specify a number of options in the WebGrid constructor and the GetHtml method in order to customize this behavior. The options let you disable paging and sorting, change the number of rows per page, change the text in the pager links and much more. Figure 2 shows the WebGrid constructor parameters and Figure 3 the GetHtml parameters.
Figure 2 WebGrid Constructor Parameters
NameTypeNotes
sourceIEnumerableThe data to render.
columnNamesIEnumerableFilters the columns that are rendered.
defaultSortstringSpecifies the default column to sort by.
rowsPerPageintControls how many rows are rendered per page (default is 10).
canPageboolEnables or disables paging of data.
canSortboolEnables or disables sorting of data.
ajaxUpdateContainerIdstringThe ID of the grid’s containing element, which enables AJAX support.
ajaxUpdateCallbackstringThe client-side function to call when the AJAX update is complete.
fieldNamePrefixstringPrefix for query string fields to support multiple grids.
pageFieldNamestringQuery string field name for page number.
selectionFieldNamestringQuery string field name for selected row number.
sortFieldNamestringQuery string field name for sort column.
sortDirectionFieldNamestringQuery string field name for sort direction.
Figure 3 WebGrid.GetHtml Parameters
NameTypeNotes
tableStylestringTable class for styling.
headerStylestringHeader row class for styling.
footerStylestringFooter row class for styling.
rowStylestringRow class for styling (odd rows only).
alternatingRowStylestringRow class for styling (even rows only).
selectedRowStylestringSelected row class for styling.
captionstringThe string displayed as the table caption.
displayHeaderboolIndicates whether the header row should be displayed.
fillEmptyRowsboolIndicates whether the table can add empty rows to ensure the rowsPerPage row count.
emptyRowCellValuestringValue used to populate empty rows; only used when fillEmptyRows is set.
columnsIEnumerableColumn model for customizing column rendering.
exclusionsIEnumerableColumns to exclude when auto-populating columns.
modeWebGridPagerModesModes for pager rendering (default is NextPrevious and Numeric).
firstTextstringText for a link to the first page.
previousTextstringText for a link to the previous page.
nextTextstringText for a link to the next page.
lastTextstringText for a link to the last page.
numericLinksCountintNumber of numeric links to display (default is 5).
htmlAttributesobjectContains the HTML attributes to set for the element.
The previous Razor code will render all of the properties for each row, but you may want to limit which columns are displayed. There are a number of ways to achieve this. The first (and simplest) is to pass the set of columns to the WebGrid constructor. For example, this code renders just the Name and ListPrice properties:
  1. var grid = new WebGrid(Model, columnNames: new[] {"Name""ListPrice"});
You could also specify the columns in the call to GetHtml instead of in the constructor. While this is slightly longer, it has the advantage that you can specify additional information about how to render the columns. In the following example, I specified the header property to make the ListPrice column more user-friendly:
  1. @grid.GetHtml(columns: grid.Columns(
  2.  grid.Column("Name"),
  3.  grid.Column("ListPrice", header:"List Price")
  4.  )
  5. )
Often when you render a list of items, you want to let users click on an item to navigate to the Details view. The format parameter of the Column method allows you to customize the rendering of a data item. The following code shows how to change the rendering of names to output a link to the Details view for an item, and outputs the list price with two decimal places as typically expected for currency values; the resulting output is shown in Figure 4.
  1. @grid.GetHtml(columns: grid.Columns(
  2.  grid.Column("Name", format: @@Html.ActionLink((string)item.Name,
  3.             "Details""Product"new {id=item.ProductId}, null)),
  4.  grid.Column("ListPrice", header:"List Price"
  5.              format: @@item.ListPrice.ToString("0.00"))
  6.  )
  7. )
A Basic Grid with Custom Columns
Figure 4 A Basic Grid with Custom Columns
Although it looks like there’s some magic going on when I specify the format, the format parameter is actually a Func—a delegate that takes a dynamic parameter and returns an object. The Razor engine takes the snippet specified for the format parameter and turns it into a delegate. That delegate takes a dynamic parameter named item, and this is the item variable that’s used in the format snippet. For more information on the way these delegates work, see Phil Haack’s blog post at bit.ly/h0Q0Oz.
Because the item parameter is a dynamic type, you don’t get any IntelliSense or compiler checking when writing your code (see Alexandra Rusina’s article on dynamic types in the February 2011 issue at msdn.microsoft.com/magazine/gg598922). Moreover, invoking extension methods with dynamic parameters isn’t supported. This means that, when calling extension methods, you have to ensure that you’re using static types—this is the reason that item.Name is cast to a string when I call the Html.ActionLink extension method in the previous code. With the range of extension methods used in ASP.NET MVC, this clash between dynamic and extension methods can become tedious (even more so if you use something like T4MVC: bit.ly/9GMoup).

Adding Strong Typing

While dynamic typing is probably a good fit for WebMatrix, there are benefits to strongly typed views. One way to achieve this is to create a derived type WebGrid, as shown in Figure 5. As you can see, it’s a pretty lightweight wrapper!
Figure 5 Creating a Derived WebGrid
  1. public class WebGrid : WebGrid
  2.   {
  3.     public WebGrid(
  4.       IEnumerable source = null,
  5.       ... parameter list omitted for brevity)
  6.     : base(
  7.       source.SafeCast<object>(), 
  8.       ... parameter list omitted for brevity)
  9.     { }
  10.   public WebGridColumn Column(
  11.               string columnName = null
  12.               string header = null
  13.               Funcobject> format = null
  14.               string style = null
  15.               bool canSort = true)
  16.     {
  17.       Funcobject> wrappedFormat = null;
  18.       if (format != null)
  19.       {
  20.         wrappedFormat = o => format((T)o.Value);
  21.       }
  22.       WebGridColumn column = base.Column(
  23.                     columnName, header, 
  24.                     wrappedFormat, style, canSort);
  25.       return column;
  26.     }
  27.     public WebGrid Bind(
  28.             IEnumerable source, 
  29.             IEnumerable<string> columnNames = null
  30.             bool autoSortAndPage = true
  31.             int rowCount = -1)
  32.     {
  33.       base.Bind(
  34.            source.SafeCast<object>(), 
  35.            columnNames, 
  36.            autoSortAndPage, 
  37.            rowCount);
  38.       return this;
  39.     }
  40.   }
  41.  
  42.   public static class WebGridExtensions
  43.   {
  44.     public static WebGrid Grid(
  45.              this HtmlHelper htmlHelper,
  46.              ... parameter list omitted for brevity)
  47.     {
  48.       return new WebGrid(
  49.         source, 
  50.         ... parameter list omitted for brevity);
  51.     }
  52.   }
So what does this give us? With the new WebGrid implementation, I’ve added a new Column method that takes a Func for the format parameter, which means that the cast isn’t required when calling extension methods. Also, you now get IntelliSense and compiler checking (assuming that MvcBuildViews is turned on in the project file; it’s turned off by default).
The Grid extension method allows you to take advantage of the compiler’s type inference for generic parameters. So, in this example, you can write Html.Grid(Model) rather than new WebGrid(Model). In each case, the returned type is WebGrid.

Adding Paging and Sorting

You’ve already seen that WebGrid gives you paging and sorting functionality without any effort on your part. You’ve also seen how to configure the page size via the rowsPerPage parameter (in the constructor or via the Html.Grid helper) so that the grid will automatically show a single page of data and render the paging controls to allow navigation between pages. However, the default behavior may not be quite what you want. To illustrate this, I’ve added code to render the number of items in the data source after the grid is rendered, as shown in Figure 6.
The Number of Items in the Data Source
Figure 6 The Number of Items in the Data Source
As you can see, the data we’re passing contains the full list of products (295 of them in this example, but it’s not hard to imagine scenarios with even more data being retrieved). As the amount of data returned increases, you place more and more load on your services and databases, while still rendering the same single page of data. But there’s a better approach: server-side paging. In this case, you pull back only the data needed to display the current page (for instance, only five rows).
The first step in implementing server-side paging for WebGrid is to limit the data retrieved from the data source. To do this, you need to know which page is being requested so you can retrieve the correct page of data. When WebGrid renders the paging links, it reuses the page URL and attaches a query string parameter with the page number, such as http://localhost:27617/Product/DefaultPagingAndSorting?page=3 (the query string parameter name is configurable via the helper parameters—handy if you want to support pagination of more than one grid on a page). This means you can take a parameter called page on your action method and it will be populated with the query string value.
If you just modify the existing code to pass a single page worth of data to WebGrid, WebGrid will see only a single page of data. Because it has no knowledge that there are more pages, it will no longer render the pager controls. Fortunately, WebGrid has another method, named Bind, that you can use to specify the data. As well as accepting the data, Bind has a parameter that takes the total row count, allowing it to calculate the number of pages. In order to use this method, the List action needs to be updated to retrieve the extra information to pass to the view, as shown in Figure 7.
Figure 7 Updating the List Action
  1. public ActionResult List(int page = 1)
  2. {
  3.   const int pageSize = 5;
  4.  
  5.   int totalRecords;
  6.   IEnumerable products = productService.GetProducts(
  7.     out totalRecords, pageSize:pageSize, pageIndex:page-1);
  8.             
  9.   PagedProductsModel model = new PagedProductsModel
  10.                                  {
  11.                                    PageSize= pageSize,
  12.                                    PageNumber = page,
  13.                                    Products = products,
  14.                                    TotalRows = totalRecords
  15.                                  };
  16.   return View(model);
  17. }
With this additional information, the view can be updated to use the WebGrid Bind method. The call to Bind provides the data to render and the total number of rows, and also sets the autoSortAndPage parameter to false. The autoSortAndPage parameter instructs WebGrid that it doesn’t need to apply paging, because the List action method is taking care of this. This is illustrated in the following code:
  1. < span="">>
  2. @{
  3.   var grid = new WebGrid< span="">>(null, rowsPerPage: Model.PageSize, 
  4.     defaultSort:"Name");
  5.   grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
  6. }
  7. @grid.GetHtml(columns: grid.Columns(
  8.  grid.Column("Name", format: @< span="">>@Html.ActionLink(item.Name, 
  9.    "Details", "Product", new { id = item.ProductId }, null)),
  10.   grid.Column("ListPrice", header: "List Price", 
  11.     format: @< span="">>@item.ListPrice.ToString("0.00"))
  12.   )
  13.  )
  14.  
With these changes in place, WebGrid springs back to life, rendering the paging controls but with the paging happening in the service rather than in the view! However, with autoSortAndPage turned off, the sorting functionality is broken. WebGrid uses query string parameters to pass the sort column and direction, but we instructed it not to perform the sorting. The fix is to add the sort and sortDir parameters to the action method and pass these through to the service so that it can perform the necessary sorting, as shown in Figure 8.
Figure 8 Adding Sorting Parameters to the Action Method
  1. public ActionResult List(
  2.            int page = 1
  3.            string sort = "Name"
  4.            string sortDir = "Ascending" )
  5. {
  6.   const int pageSize = 5;
  7.  
  8.   int totalRecords;
  9.   IEnumerable products =
  10.     _productService.GetProducts(out totalRecords,
  11.                                 pageSize: pageSize,
  12.                                 pageIndex: page - 1,
  13.                                 sort:sort,
  14.                                 sortOrder:GetSortDirection(sortDir)
  15.                                 );
  16.  
  17.   PagedProductsModel model = new PagedProductsModel
  18.   {
  19.     PageSize = pageSize,
  20.     PageNumber = page,
  21.     Products = products,
  22.     TotalRows = totalRecords
  23.   };
  24.   return View(model);
  25. }

AJAX: Client-Side Changes

WebGrid supports asynchronously updating the grid content using AJAX. To take advantage of this, you just have to ensure the div that contains the grid has an id, and then pass this id in the ajaxUpdateContainerId parameter to the grid’s constructor. You also need a reference to jQuery, but that’s already included in the layout view. When the ajaxUpdateContainerId is specified, WebGrid modifies its behavior so that the links for paging and sorting use AJAX for the updates:
  1. < span=""> id="grid">
  2.  
  3. @{
  4.   var grid = new WebGrid< span="">>(null, rowsPerPage: Model.PageSize, 
  5.   defaultSort: "Name", ajaxUpdateContainerId: "grid");
  6.   grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
  7. }
  8. @grid.GetHtml(columns: grid.Columns(
  9.  grid.Column("Name", format: @< span="">>@Html.ActionLink(item.Name, 
  10.    "Details", "Product", new { id = item.ProductId }, null)),
  11.  grid.Column("ListPrice", header: "List Price", 
  12.    format: @< span="">>@item.ListPrice.ToString("0.00"))
  13.  )
  14. )
  15.  
While the built-in functionality for using AJAX is good, the generated output doesn’t function if scripting is disabled. The reason for this is that, in AJAX mode, WebGrid renders anchor tags with the href set to “#,” and injects the AJAX behavior via the onclick handler.
I’m always keen to create pages that degrade gracefully when scripting is disabled, and generally find that the best way to achieve this is through progressive enhancement (basically having a page that functions without scripting that’s enriched with the addition of scripting). To achieve this, you can revert back to the non-AJAX WebGrid and create the script in Figure 9 to reapply the AJAX behavior:
Figure 9 Reapplying the AJAX Behavior
  1. $(document).ready(function () {
  2.  
  3.   function updateGrid(e) {
  4.     e.preventDefault();
  5.     var url = $(this).attr('href');
  6.     var grid = $(this).parents('.ajaxGrid'); 
  7.     var id = grid.attr('id');
  8.     grid.load(url + ' #' + id);
  9.   };
  10.   $('.ajaxGrid table thead tr a').live('click', updateGrid);
  11.   $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
  12.  });
To allow the script to be applied just to a WebGrid, it uses jQuery selectors to identify elements with the ajaxGrid class set. The script establishes click handlers for the sorting and paging links (identified via the table header or footer inside the grid container) using the jQuery live method (api.jquery.com/live). This sets up the event handler for existing and future elements that match the selector, which is handy given the script will be replacing the content.
The updateGrid method is set as the event handler and the first thing it does is to call preventDefault to suppress the default behavior. After that it gets the URL to use (from the href attribute on the anchor tag) and then makes an AJAX call to load the updated content into the container element. To use this approach, ensure that the default WebGrid AJAX behavior is disabled, add the ajaxGrid class to the container div and then include the script from Figure 9.

AJAX: Server-Side Changes

One additional point to call out is that the script uses functionality in the jQuery load method to isolate a fragment from the returned document. Simply calling load(‘http://example.com/someurl’) will load the contents of the URL. However, load(‘http://example.com/someurl #someId’) will load the content from the specified URL and then return the fragment with the id of “someId.” This mirrors the default AJAX behavior of WebGrid and means that you don’t have to update your server code to add partial rendering behavior; WebGrid will load the full page and then strip out the new grid from it.
In terms of quickly getting AJAX functionality this is great, but it means you’re sending more data over the wire than is necessary, and potentially looking up more data on the server than you need to as well. Fortunately, ASP.NET MVC makes dealing with this pretty simple. The basic idea is to extract the rendering that you want to share in the AJAX and non-AJAX requests into a partial view. The List action in the controller can then either render just the partial view for AJAX calls or the full view (which in turn uses the partial view) for the non-AJAX calls.
The approach can be as simple as testing the result of the Request.IsAjaxRequest extension method from inside your action method. This can work well if there are only very minor differences between the AJAX and non-AJAX code paths. However, often there are more significant differences (for example, the full rendering requires more data than the partial rendering). In this scenario you’d probably write an AjaxAttribute so you could write separate methods and then have the MVC framework pick the right method based on whether the request is an AJAX request (in the same way that the HttpGet and HttpPost attributes work). For an example of this, see my blog post at bit.ly/eMlIxU.

WebGrid and the WebForms View Engine

So far, all of the examples outlined have used the Razor view engine. In the simplest case, you don’t need to change anything to use WebGrid with the WebForms view engine (aside from differences in view engine syntax). In the preceding examples, I showed how you can customize the rendering of row data using the format parameter:
  1. grid.Column("Name"
  2.   format: @@Html.ActionLink((string)item.Name, 
  3.   "Details""Product"new { id = item.ProductId }, null)),
The format parameter is actually a Func, but the Razor view engine hides that from us. But you’re free to pass a Func—for example, you could use a lambda expression:
  1. grid.Column("Name"
  2.   format: item => Html.ActionLink((string)item.Name, 
  3.   "Details""Product"new { id = item.ProductId }, null)),
Armed with this simple transition, you can now easily take advantage of WebGrid with the WebForms view engine!

Wrapping Up

In this article I showed how a few simple tweaks let you take advantage of the functionality that WebGrid brings without sacrificing strong typing, IntelliSense or efficient server-side paging. WebGrid has some great functionality to help make you productive when you need to render tabular data. I hope this article gave you a feel for how to make the most of it in an ASP.NET MVC application.

Ref: http://msdn.microsoft.com/en-us/magazine/hh288075.aspx