When you think ASP, think...
Recent Articles
All Articles
ASP.NET Articles
ASPFAQs.com
Message Board
Related Web Technologies
User Tips!
Coding Tips
Search

Sections:
Book Reviews
Sample Chapters
Commonly Asked Message Board Questions
JavaScript Tutorials
MSDN Communities Hub
Official Docs
Security
Stump the SQL Guru!
Web Hosts
XML
Information:
Advertise
Feedback
Author an Article
Jobs

ASP ASP.NET ASP FAQs Message Board Feedback ASP Jobs
 
Print this Page!
Published: Wednesday, December 22, 2010

Sorting a Grid of Data in ASP.NET MVC

By Scott Mitchell


A Multipart Series on Grids in ASP.NET MVC
Displaying a grid of data is one of the most common tasks faced by web developers. This article series shows how to display grids of data in an ASP.NET MVC application and walks through a myriad of common grid scenarios, including paging, sorting, filtering, and client-side enhancements.

  • Displaying a Grid - this inaugural article walks through building the ASP.NET MVC demo application and retrieving and displaying database data in a simple grid.
  • Sorting a Grid - learn how to display a sortable grid of data.
  • Paging a Grid - shows how to efficiently page through a grid of data N records at a time.
  • Filtering a Grid - see how to create a filtering interface to limit what records are displayed.
  • Sorting and Paging a Grid - learn how to display a grid that is both sortable and can be paged.
  • Sorting, Paging, and Filtering - shows how to display a grid that is sortable, pageable, and filterable.
  • MvcContrib Grid - see how to use the free, open source MvcContrib Grid to display a grid of data.
  • Introduction


    Last week's article, Displaying a Grid of Data in ASP.NET MVC, showed, step-by-step, how to display a grid of data in an ASP.NET MVC application. Last week's article started with creating a new ASP.NET MVC application in Visual Studio, then added the Northwind database to the project and showed how to use Microsoft's Linq-to-SQL tool to access data from the database. The article then looked at creating a Controller and View for displaying a list of product information (the Model).

    This article builds on the demo application created in Displaying a Grid of Data in ASP.NET MVC, enhancing the grid to include bi-directional sorting. If you come from an ASP.NET WebForms background, you know that the GridView control makes implementing sorting as easy as ticking a checkbox. Unfortunately, implementing sorting in ASP.NET MVC involves a bit more work than simply checking a checkbox, but the quantity of work isn't significantly greater and with ASP.NET MVC we have more control over the grid and sorting interface's layout and markup, as well as the mechanism through which sorting is implemented. With the GridView control, sorting is handled through form postbacks with the sorting parameters - what column to sort by and whether to sort in ascending or descending order - being submitted as hidden form fields. In this article we'll use querystring parameters to indicate the sorting parameters, which means a particular sort order can be indexed by search engines, bookmarked, emailed to a colleague, and so on - things that are not possible with the GridView's built-in sorting capabilities.

    Like with its predecessor, this article offers step-by-step instructions and includes a complete, working demo available for download at the end of the article. Read on to learn more!

    - continued -

    Step 0: A Brief Roadmap


    This article walks through displaying a grid of data that offers bi-directional sorting. It is presumed that you have read Displaying a Grid of Data in ASP.NET MVC, in which we created the ASP.NET MVC demo application and saw how to display a grid of data without any features like sorting or paging.

    In Displaying a Grid of Data in ASP.NET MVC the simple grid demo was available at the URL www.yoursite.com/Products/Index (which could be shortened to www.yoursite.com/Products, since Index is the default action name). For this demo let's use the following general URL pattern for the sortable grid: www.yoursite.com/Products/Sortable?sortBy=ColumnName&ascending=true|false. Using this generalized pattern, here is what some concrete URLs might look like:

    • /Products/Sortable - this should display the products sorted in their default order. Let's define the default sort order to be sorted by product name alphabetically (e.g., in ascending order). In other words, if the sortBy parameter is not supplied, sort by ProductName; if the ascending parameter is not supplied, sort in ascending order.
    • /Products/Sortable?sortBy=UnitPrice - displays the products sorted by the UnitPrice column in ascending order.
    • /Products/Sortable?sortBy=UnitPrice&ascending=false - displays the products sorted by the UnitPrice column in descending order (from most expensive to least).
    Like with the GridView control, the sortable grid we'll be creating will be sortable by clicking the column name in the header row. But unlike the GridView, which renders these as LinkButtons that trigger a postback, our grid's headers will contain hyperlinks that, when clicked, send the browser to the appropriate URL. For example, the grid has a Price column that, when clicked for the first time, sends the user to www.yoursite.com/Products/Sortable?sortBy=UnitPrice&ascending=true. Note that the column name displayed in the grid ("Price," in this case), is not necessarily the column name that is passed in the sortBy querystring parameter. The sortBy querystring parameter provides the name of the database column by which the data should be sorted, which may not be the same as the text displayed in the grid column's header.

    Step 1: Creating a View-Specific Model


    The demo we created in Displaying a Grid of Data in ASP.NET MVC used a collection of Product objects as its model (namely, the NorthwindDataContext object's Products property). While this worked fine for the simple grid, for a sortable grid the View needs to know a bit more than just the set of products to display in order to correctly render its user interface. In particular, the View needs to know what column the data is sorted by and whether it's sorted in ascending or descending order. This information is needed for two reasons:
    • If the user opts to sort the grid by a column that is not the one the grid is currently sorted by, then we will sort the grid by that column in ascending order. However, if the user opts to sort the grid by the column that the grid is already sorted by then we want to toggle the ascending parameter so that clicking a column once sorts the grid by that column in ascending order, clicking it twice sorts it in descending order, clicking it thrice sorts it in ascending order, and so on. To accomplish this we need to know what column the grid is sorted by and whether it's sorted in ascending or descending order.
    • We also need to know what column the grid is sorted by (and whether it's sorted in ascending or descending order) if we want to provide the user a visual cue (such as an up or down arrow) to indicate what column the data is sorted by.

    To supply this extra information to the View let's add a new class to our project that will serve as a Model for this particular View. Such classes are referred to as view-specific Models, as they are Models created for a specific View. If you are following along at your computer, open the MVC application created in Displaying a Grid of Data in ASP.NET MVC and add a new class in the Models folder named ProductGridModel.cs with the following code:

    namespace Web.Models
    {
       public class ProductGridModel
       {
          // Data properties
          public IEnumerable<Product> Products { get; set; }
          

          // Sorting-related properties
          public string SortBy { get; set; }
          public bool SortAscending { get; set; }
          public string SortExpression
          {
             get
             {
                return this.SortAscending ? this.SortBy + " asc" : this.SortBy + " desc";
             }
          }
       }
    }

    The ProductGridModel class defines a Products property, which is the collection of Product objects that are to be displayed in the grid. There are also three sorting-related properties:

    • SortBy - the name of the database column by which to sort the data
    • SortAscending - a Boolean value that indicates whether to sort the data in ascending order
    • SortExpression - a read-only property that returns a sorting string constructed from the values of the SortBy and SortAscending properties. For example, if SortBy is assigned "UnitPrice" and SortAscending is true, SortExpression returns "UnitPrice asc". If SortBy is set to "Discontinued" and SortAscending is false, SortExpression returns "Discontinued desc". As we will see shortly, this property is used to apply the actual sorting logic in the Controller.
    With a few more properties, this Model could be extended to also handle paging (and sorting and paging). In fact, if you download the demo available at the end of this article you'll see that the ProductGridModel class has a number of additional properties not noted above. These additional properties will be examined in a future installment.

    Step 2: Creating the Sortable Action


    In Displaying a Grid of Data in ASP.NET MVC we created a Controller named ProductsController with a single action, Index, and a helper property, DataContext, that returns a NorthwindDataContext object. Let's add a new action to this Controller named Sortable, which is the action that will be executed when a request comes in for the URL www.yoursite.com/Products/Sortable.

    ASP.NET MVC automatically performs model binding, which is a mapping of the request's parameters to the input parameters of the executed action. For example, if your action defines an input parameter named sortBy, the MVC framework will search the incoming request's parameters to see if there is any parameter with the same name. (Here, a parameter could be a submitted form field, a querystring parameter, or a route parameter.) If a match is found it is passed into the action's input parameter.

    If that doesn't quite make sense, perhaps a look at the code will help. Here's the complete code for the Sortable action:

    public class ProductsController : Controller
    {
       ...

       // GET: /Products/Sortable?SortColumn=columnName&Ascending=true|false
       public ActionResult Sortable(string sortBy = "ProductName", bool ascending = true)
       {
          var model = new ProductGridModel()
          {
             SortBy = sortBy,
             SortAscending = ascending
          };

          model.Products = this.DataContext.Products.OrderBy(model.SortExpression);

          return View(model);
       }
    }

    Note that the Sortable action accepts two input parameters: sortBy and ascending. These input parameters will be assigned the value of any matching parameter in the incoming request (such as a querystring parameter). If no such match is found - that is, if someone visits www.yoursite.com/Products/Sortable without specifying the querystring parameters - the default values will be used. Here we defined the default values of sortBy and ascending to be "ProductName" and true, respectively. (Note: support for specifying default values for an input parameter in a method was added to C# version 4. See Optional Parameters and Named Arguments in C# 4 for more details.)

    The Sortable action starts by creating a new ProductGridModel instance named model and assigning model's SortBy and SortAscending properties to the values of its sortBy and ascending input parameters. Next, the Sortable action gets the entire collection of Products (this.DataContext.Products), orders it by the ProductGridModel object's SortExpression value, and assigns it to model's Products property. Finally, the action returns a strongly-typed View, passing in model.

    If you are familiar with LINQ you may be perplexed at my use of the OrderBy extension method where I simply pass in a string by which to sort the Products collection (such as "ProductName desc"). The OrderBy extension method you are likely familiar with accepts as input a lambda expression that indicates the property to sort on, such as: this.DataContext.Products.OrderBy(p => p.ProductName).

    The OrderBy method I am using is not part of the standard .NET Framework, but is rather part of Microsoft's Dynamic LINQ library, which is a library that allows querying using string arguments. The demo project available for download includes a file in the HelperClasses folder named Dynamic.cs, which contains the OrderBy extension method signature that accepts a string as input. If you are following along at your computer, either download the demo project and add the Dynamic.cs class file to your project, or download the Dynamic.cs file from Microsoft. Also, take a moment to read Scott Guthrie's blog entry, Dynamic LINQ, as it gives a quick overview of Dynamic LINQ and how and why you might want to use it.

    Step 3: Creating the View


    To create the view for the Sortable action, right-click on the action name in the Controller class file and choose the Add View option. From the Add View dialog box, check the "Create a strongly-typed view" checkbox and then select the Web.Models.ProductGridModel option from the "View data class" drop-down (see the screen shot below). Click OK. This should close the dialog box and create (and open) the new view, Sortable.aspx.

    Add a strongly-typed View for the Sortable action.

    Next, add the following markup and server-side code to the View in the Content control for the MainContent ContentPlaceHolder:

    <table class="grid">
       <tr>
          <th>Product</th>
          <th>Category</th>
          <th>Qty/Unit</th>
          <th>Price</th>
          <th>Discontinued</th>
       </tr>

    <% foreach (var item in Model.Products) { %>
       
       <tr>
          <td class="left"><%: item.ProductName %></td>
          <td class="left"><%: item.Category.CategoryName %></td>
          <td class="left"><%: item.QuantityPerUnit %></td>
          <td class="right"><%: String.Format("{0:C}", item.UnitPrice) %></td>
          <td>
             <% if (item.Discontinued) { %>
                <img src="<%=Url.Content("~/Content/cancel.png") %>" alt="Discontinued" title="Discontinued" />
             <% } %>
          </td>
       </tr>
       
    <% } %>

    </table>

    The above markup is nearly identical to what we added to the Index View in Displaying a Grid of Data in ASP.NET MVC). The only difference, in fact, is in the foreach construct. In Displaying a Grid of Data in ASP.NET MVC the Index View's Model was a collection of Product objects, so our foreach loop looked like so: foreach(var item in Model) { ... }. However, the Sortable View's Model is a ProductGridModel instance whose Products property contains the collection of Product objects to display. Consequently, the foreach loop iterates over Model.Products.

    Take a moment to visit this View through a browser. If you visit Products/Sortable you should see a grid showing all the products ordered by the default ordering - namely, sorted by product name in alphabetical order.

    The products are ordered alphabetically by product name.

    Right now the grid's column heads are rendered as text. In a moment we'll update the markup to emit hyperlinks, but for now if you go to your browser's Address bar and enter in the querystring parameters manually you can see the results. For instance, if you replace the current URL in your Address bar - say, www.yoursite.com/Products/Sortable - with the following - www.yoursite.com/Products/Sortable?sortBy=UnitPrice&ascending=false - you should see the products sorted by price, from the most expensive to the least.

    When you visit www.yoursite.com/Products/Sortable?sortBy=UnitPrice&ascending=false the ASP.NET MVC framework executes the ProductsController class's Sortable method, passing in the values "UnitPrice" and false as the input parameters for the method's sortBy and ascending input parameters. The Dynamic LINQ library is then used to order the set of all products using the sort expression "UnitPrice desc". This ordered collection of Product objects is then passed to the View, which displays them in a grid.

    Step 4: Adding Sorting Links to the Grid's Header Row


    At this point the user viewing the grid cannot sort it unless she types in the querystring parameters by hand in her browser's Address bar. To remedy this we need to have the grid's header row render hyperlinks in each of the header cells that point to the appropriate URL. At first blush this may seem like a straightforward task: the Product row's header would be a URL to Products/Sortable?sortBy=ProductName&ascending=true, the Price row's header would be a URL to Products/Sortable?sortBy=UnitPrice&ascending=true, and so on. This is what the URL should be for the non-sorted columns, but for the column that is already sorted in ascending order, the URL should be Products/Sortable?sortBy=ColumnName&ascending=false. Also, we should add some sort of visual cue to the header row of the column the grid is sorted by so that the user can clearly see how the grid is sorted.

    To accomplish this I decided to create a partial view. A partial view is akin to a User Control in the WebForms model. In short, it's a View that you can use (and reuse) in a View. If you are following along at your computer, create a new partial view named SmartLink.ascx by right-clicking on the Views/Shared folder in the Solution Explorer and choosing Add -> Add View. Name the View SmartLink and check the "Create a partial view (.ascx)" checkbox. Also check the "Create a strongly-typed view" checkbox and then choose the Web.Models.ProductGridModel type from the drop-down.

    Add a strongly-typed, partial View named SmartLink.

    After clicking OK, Visual Studio will create a new partial view file (SmartLink.ascx) in the Views/Shared folder. At this point the partial view contains only the following markup:

    <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Web.Models.ProductGridModel>" %>

    Note how the Model type selected in the Add View dialog box (Web.Models.ProductGridModel) is specified in the Inherits attribute. Like with the Sortable View, we can access the strongly-typed Model using the Model property.

    We'll come back to write the markup and code for this partial view in a moment, but first let's look at how we'll use this partial view to the Sortable View. To add a partial view to a "normal" View, use one of the following Html.RenderParial method overrides:

    <% Html.RenderPartial("partialViewName"); %>

    <% Html.RenderPartial("partialViewName", viewData); %>

    <% Html.RenderPartial("partialViewName", model); %>

    <% Html.RenderPartial("partialViewName", model, viewData); %>

    The optional viewData parameter is a ViewDataDictionary object. If supplied, these values can be accessed from the partial view using the syntax ViewData["name"].

    When rendering the SmartLink partial view we need to pass in the Sortable View's ProductGridModel and some additional information. Specifically, we need to supply the name of the column to show in the grid (such as "Product" or "Category") as well as the name of the database column that provides that data (such as "ProductName" and "Categories.CategoryName"). This additional information is supplied through viewData.

    The following code snippet shows how we'll render the partial view from the Sortable View. In particular, this snippet shows the code for the Product column. Note how the model that's passed into the partial view is the exact same Model object passed to the Sortable View. And the viewData dictionary passed into partial view contains two entries named "ColumnName" and "DisplayName", which in this case are assigned the values "ProductName" and "Product", respectively.

    <% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "ProductName" }, { "DisplayName", "Product" } }); %>

    Take a moment to replace the existing header markup with the following content. Note that each column header's text has been replaced with the SmartLink partial view. The Html.RenderPartial method call in each column header passes in the same partial view name and model, but differs by the values supplied in the viewData's "ColumnName" and "DisplayName" entries.

    <table class="grid">
       <tr>
          <th><% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "ProductName" }, { "DisplayName", "Product" } }); %></th>
          <th><% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "Category.CategoryName" }, { "DisplayName", "Category" } }); %></th>
          <th><% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "QuantityPerUnit" }, { "DisplayName", "Qty/Unit" } }); %></th>
          <th><% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "UnitPrice" }, { "DisplayName", "Price" } }); %></th>
          <th><% Html.RenderPartial("SmartLink", Model, new ViewDataDictionary { { "ColumnName", "Discontinued" }, { "DisplayName" , "Discontinued" } }); %></th>
       </tr>
       
       ...
    </table>

    Now that we've seen how the partial view will be used from the Sortable View, we're ready to write its markup and code. Keep in mind that from this partial view we can access any value of the Sortable View's Model via the Model property; the values in the viewData collection are accessible via ViewData["name"]. Also remember that the purpose of the SmartLink partial view is to generate a hyperlink for one particular column in the grid.

    First things first, we need to determine if the link being rendered needs to indicate that the data should be sorted in ascending or descending order. As we discussed earlier in this article, the only time clicking a sorting link should display data in descending order is if the grid is already sorted by that column in ascending order. The following line of code creates a variable named isDescending that is set to true only if the column that the grid is currently sorted by (Model.SortBy) is the same as the name of the database column this SmartLink is generating a hyperlink for (ViewData["ColumnName"]) and the data is currently sorted in ascending order (Model.SortAscending):

    var isDescending = string.CompareOrdinal(Model.SortBy, ViewData["ColumnName"].ToString()) == 0 && Model.SortAscending;

    It would be nice to provide the user some sort of visual cue indicating the column (and order) by which the data is currently sorted. The CSS class CustomStyles.css defines two CSS classes - sortAsc and sortDesc - that add an up or down arrow to the right of any HTML element that specifies that class name. Therefore, when rendering a hyperlink the SmartLink partial class needs to determine if the data is currently sorted by its ViewData["ColumnName"] value. If so, then it needs to set its hyperlink's class attribute to either sortAsc or sortDesc depending on whether the data is currently sorted in ascending or descending order.

    The following code defines a dictionary of HTML attributes and adds the appropriate class attribute value if column the data is currently sorted by (Model.SortBy) is the same as the database column name for the SmartLink being rendered (ViewData["ColumnName"]):

    if (string.CompareOrdinal(Model.SortBy, ViewData["ColumnName"].ToString()) == 0)
    {
       if (Model.SortAscending)
          htmlAttributes.Add("class", "sortAsc");
       else
          htmlAttributes.Add("class", "sortDesc");
    }

    Putting it all together we end up with the following code and markup in the SmartLink partial view:

    <%
       var isDescending = string.CompareOrdinal(Model.SortBy, ViewData["ColumnName"].ToString()) == 0 && Model.SortAscending;

       var routeData = new RouteValueDictionary { { "sortBy", ViewData["ColumnName"].ToString() }, { "ascending", !isDescending } };

       var htmlAttributes = new Dictionary<string, object>();
       if (string.CompareOrdinal(Model.SortBy, ViewData["ColumnName"].ToString()) == 0)
       {
          if (Model.SortAscending)
             htmlAttributes.Add("class", "sortAsc");
          else
             htmlAttributes.Add("class", "sortDesc");
       }
    %>

    <%: Html.ActionLink(
          ViewData["DisplayName"].ToString(),                         // Link Text
          Html.ViewContext.RouteData.Values["action"].ToString(),     // Action
          Html.ViewContext.RouteData.Values["controller"].ToString(), // Controller
          routeData,                                                  // Route data
          htmlAttributes                                              // HTML attributes to apply to hyperlink
       )
    %>

    The Html.ActionLink method renders the HTML for a hyperlink. It takes in as its first parameter the text to display in the hyperlink - ViewData["DisplayName"].

    Its second and third parameters specify the action and controller used to generate the link. Because we want these links to send the user back to the current action (just with different querystring parameters), we use Html.ViewContext.RouteData.Values["action"].ToString() and Html.ViewContext.RouteData.Values["controller"].ToString() to get the action and controller for the current request, respectively. (The reason I went with this approach rather than hard-coding in "Sortable" and "Products" is so that this partial view can be used by other views.)

    The Html.ActionLink method's fourth input parameter specifies the route data, which is any additional routing information besides the controller and action. This is where we supply the sortBy and ascending values, which are the values of ViewData["ColumnName"] and the negation of isDescending, respectively.

    The fifth and final input parameter to Html.ActionLink is the set of HTML attributes. This will add the up or down arrow to the hyperlink the SmartLink is rendering for the column that the grid is already sorted by.

    With the partial view implemented and used from the Sortable View, take a moment to visit the page through a browser. When visiting Products/Sortable you should see the products sorted by product name in alphabetical order with an up arrow next to the Product text in the header row.

    The products are ordered alphabetically by product name.

    Clicking the Product link in the header row takes you to Products/Sortable?sortBy=ProductName&ascending=False, with the products ordered in reverse alphabetical order and with a down arrow now next to the Product text.

    The products are sorted in reverse alphabetical order.

    Clicking on other column headers sorts the grid as you'd expect.

    Conclusion and Looking Forward...


    This article showed how to use a view-specific Model, a sprinkle of Dynamic LINQ, and a partial view to display a sortable grid of data. At this point, the grid still shows all products. In the next installment we'll see how to implement paging so that only a subset of records are displayed at a time.

    Until then... Happy Programming!

  • By Scott Mitchell


    Attachments:

  • Download the Demo Code Used in this Article

    Further Reading

  • Displaying a Grid of Data in ASP.NET MVC
  • ASP.NET MVC Model Binding
  • 6 Tips for ASP.NET MVC Model Binding
  • Dynamic LINQ


  • ASP.NET [1.x] [2.0] | ASPMessageboard.com | ASPFAQs.com | Advertise | Feedback | Author an Article