Click here to Skip to main content
15,881,742 members
Articles / Web Development / HTML

Sorting and Filtering an HTML Table using ASP.NET MVC Technology

Rate me:
Please Sign up or sign in to vote.
4.31/5 (10 votes)
14 Feb 2011CPOL14 min read 101.9K   3.1K   45   18
A simple MVC web app which implements a sortable and filterable table

Background

As you would expect, I used HTML and CSS to define the markup and look and feel for the table. JQuery, YUI and Microsoft Ajax libraries are used for client side manipulation. JQuery 1.4.4 and Microsoft Ajax JavaScript libraries are shipped with ASP.NET MVC 3, so if you have Visual Studio 2010, you don’t need to download these separately; when you create an ASP.NET MVC 3 application, these libraries are included in the project template. Just in case, JQuery can be found here and ASP.NET MVC 3 can be found here. YUI 2 can be found here.

As an inversion of control container, I used Castle Windsor which can be found here.

For unit testing, I used NUnit which can be found here.

I used Moq to mock objects which you can find here.

The Application

This application will be a simple one and, the main focus will be on how to build a simple HTML table that can be sorted and filtered. We’ll have a single aspx page and two ascx user controls. The two user controls will have basically the same functionality, one will post back the page on every action taken by the user, the other will be Ajax enabled, meaning that it will do partial post backs.

app_screenshot.jpg

Let’s summarize how this application should work. First time, no filters or sort expressions are enabled, the first page is shown. Below the table, there is a label that displays how many records correspond to the current filtering, the numbers in the last row of the table indicate how many pages are available. The user can change the page size by selecting a different value from the combo box in the top left corner of the page. Every column has a green arrow that, if clicked, brings up a panel with a check box list. Each check box’s text is a possible filter value for the property the column refers to. If no check box is checked, then no filtering is applied by that property name, it is the same as if all of the check boxes were checked. If the “Filter instantly” check box is checked, then when the user checks/unchecks a filter check box, the page is submitted and the filtering is applied instantly. When “Filter instantly” is unchecked, the user needs to click on the “Filter” button located on the filter panel for the filtering to take effect. If the user clicks on the header text, then sorting is applied first in ascending order. If the same header is clicked twice, then sorting is applied in descending order. The “Clear all filters” link in the top left corner of the page resets all filter values, i.e., removes any filter expressions.

If you open up the VS solution, you’ll see three projects. The DomailModel project contains the data that our table will use. The FakePersonsRepository class implements the IPersonsRepository interface and defines some hypothetical persons with hard-coded values. The MvcTableDemo project is an ASP.NET MVC 2 web application and contains the logic for our solution. The MvcTableDemo.Tests project contains unit tests written for the MvcTableDemo project. Let's start dissecting our main project.

As I said before, we have an Index.aspx page which contains the common script references that our user controls will use and an if clause that checks the web.config file to decide which user control to instantiate:

C#
if (ConfigurationSettings.AppSettings["useAjax"] == "false") 
    Html.RenderPartial ("ItemsList"); 
else 
    Html.RenderPartial ("ItemsListAjax");       

In the web.config file, we have the following element:

XML
<appSettings>
    <add key="useAjax" value="true" />
</appSettings>

If the value is set to true, the ItemsListAjax.ascx user control will be instantiated, otherwise the ItemsList.ascx is used.

The EntitiesController class is our main controller class and has two main methods that handle the requests:

  1. C#
    public ActionResult InitialList
    	(int pageSize, int page, String sortBy, String sortMode)
  2. C#
    public ActionResult MaintainList
    	(int pageSize, int page, String showFilter, String sortBy, 
    	String sortMode, String scrollTop, FormCollection fc)

I used constructor injection to provide an IPersonsRepository implementation to the main controller class. I used Castle Windsor to set up the dependency on the controller class. To make this work correctly, you need to create a class that subclasses DefaultControllerFactory and create a WindsorContainer object and register all components specified in web.config file. In the Global.asax.cs file's Application_Start() method, you need to set the specified controller factory:

C#
...
ControllerBuilder.Current.SetControllerFactory (new WindsorControllerFactory ());

In the web.config file, you need to do the following configuration:

XML
<configSections>
    <section 
        name="castle"
        type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, 
	Castle.Windsor" />
</configSections>

<castle>
    <components>
        <component
            id="PersonsRepository"
            service="DomainModel.IPersonsRepository, DomainModel"
            type="DomainModel.FakePersonsRepository, DomainModel">
        </component>
    </components>
</castle>
...

The InitialList method is a GET method, and it is executed either when the first request to our page is made (i.e. when you type in the URL address in the browser and hit enter) or when the “Clear filter” link is clicked on the ItemsList.ascx user control (in non-Ajax mode). The main difference between the two methods is that the InitialList method filters the data by Request.QueryString value whereas the MaintainList() method filters by the hidden input fields posted back to the server, i.e., by clicking on the controls of the table. Both methods sort the data if the sortBy and sortMode parameters are filled.

First, let’s see what the InitialList method does. For what kind of URLs is this method executed?

  • / => pageSize = 4, page = 1, sortMode = "", sortBy =""

  • 4/2 => pageSize = 4, page = 2, sortMode = "", sortBy = ""

  • /4/2/id/asc?filter=id:1,2 => pageSize = 4, page = 2, sortBy = "id", sortMode = "asc"

  • /6/1/birthdate/desc?filter=name:George, john,emily,ismarried:true => pageSize = 6, page = 1, sortBy = "birthdate", sortMode = "desc"

The URLs above are mapped by the RouteCollection defined in Global.asax.cs file’s RegisterRoutes method. The last two URLs above are matched by the following pattern:

C#
routes.MapRoute (
    null, // Route name
    "{pageSize}/{page}/{sortBy}/{sortMode}", // URL with parameters
    new { controller = "Entities", action = "InitialList" },
    new { pageSize = @"\d+", page = @"\d+" }
);

The InitialList method calls the FilterByQueryString method which receives the query string (e.g. filter=id:1,2,name=john) and returns a FiltersData object. The FiltersData class defines a dictionary which stores each property name of our Person object together with an array of FilterData. The FilterData class has two properties:

C#
public String FilterText{…}

public bool IsActive {…}

The FilterText property stores the value of the property of the Person object and the IsActive property stores whether the filter is active or not.

The FiltersData object is stored in the ViewData[“filters”] dictionary which is used by the user controls to populate the initial state of the filters on the HTML page. The question that arises now is how to store the filter values on our HTML page. In my solution, the generated HTML for the ID filters will be as follows:

XML
<div style="display: none;"><div id="div_Name">
    <input id="filter_Name_George" name="filter_Name_George" 
	type="hidden" value="False" /> 
    <input id="filter_Name_John" name="filter_Name_John" type="hidden" value="False" /></div></div>

This way, the property name (e.g. Name) and the corresponding filter values (e.g. George, John) can be easily extracted with JavaScript code.

Now let’s turn to the discussion of how the user controls are built up. First, I will describe the ItemsList.ascx user control which is easier. A typical generated markup by the user control would look like this:

XML
<table id="table_Persons">
    <thead>
        <tr>
            <th class="header_ID">
                <a id="a_ID" href="#">ID
                    <img class="sortImage" src="/Content/Images/down-arrow.jpg" alt="" />
                    <img class="sortImage" src="/Content/Images/up-arrow.jpg" alt="" />
                </div>
            </th>
        </tr>
    </thead>
    <tbody> 
        <tr class="odd">
            <td>1</td> 
            <td>George</td> 
            <td>12/1/1980 1:14:15 PM</td> 
            <td>True</td>
        </tr>
     </tbody>
     <tfoot>
         <tr>
             <td style="text-align: left;" colspan="4"> 
                 <a href="#" class="selectedPage">1</a> 
                 <a href="#" class="defaultPage">2</a>
                 <a href="#" class="defaultPage">3</a>
             </td>
         </tr>
     </tfoot>
</table>

If you look at the markup of ItemsList.ascx user control, you’ll notice the code that generates the HTML table and below that, we have the code that generates the markup for the filter values (see markup above) and some input fields that are filled with data from the ViewData dictionary. All these hidden input fields are enclosed in a “using (Html.BeginForm(…))” statement, this way all these values are submitted to the server on a post-back. JavaScript code is responsible for filling these values before submitting back to the server.

First, let’s ignore how the filter values get set in JavaScript code, suppose that all the values of the hidden fields are set, the page is submitted and the method MaintainList() is executed. The MaintainList() method has the following signature:

C#
public ActionResult MaintainList (int pageSize, int page, 
String showFilter, String sortBy, String sortMode, String scrollTop FormCollection fc)

The parameters of this method are the same as for the InitialList method except “showFilter” of type String and “fc” which is of type FormCollection. The fc object contains all the HTML controls with their values which are inside the form element that was submitted. Because we declared this method such that it includes all the parameters that correspond to the names of the hidden fields except those that store filter values, we need to extract from the fc object only those hidden field values that contain the text “filter” in their name (note that each hidden field for the filters is named by the code:

XML
<%=Html.Hidden ("filter_" + propertyName + "_" + fData.FilterText, fData.IsActive) %>

Granted that we have these values, we can build up a FiltersData object that corresponds to the actual filter expressions set by the user. Then, the actual filtering can be easily done by looking at the FiltersData object’s dictionary. One last thing to note about this method is that it differentiates whether the request is an Ajax request or not: if it is, then it fills up the ViewData dictionary with the relevant values, otherwise a JsonResult object is returned back to the client which in turn contains all the relevant values needed to update the page.

Now let’s turn our attention to the JavaScript file ItemsListJS.js which is referenced by the ItemsList.ascx user control. As I mentioned, I used JQuery to traverse the DOM and to set values and attributes of elements. As we have seen, the logic to filter, sort and navigate through pages of the table are all located server-side, so our JavaScript code needs to prepare the relevant data for the server-side code as well as managing how to show a filter panel when the user clicks on a filter link.

The status of the "cbk_Instant" check box is stored in a cookie, so we don’t need to carry this information back and forth between requests and responses. It is worth mentioning the role of the "hdn_showFilter" hidden field: if instant filtering is on, it stores the property name to which the filter text belongs. We need that because when a filter check box’s status is changed, the page is submitted, and HTTP being a stateless protocol, the browser forgets that we had a filter panel open. We need to know which filter panel should be opened again, and by looking at the value of this hidden field, we can find that out. If this value is an empty string, then it means that no filter panel should be open.

On each response, we build up a filter panel for each column of the table by calling the createFilterPanel(a, propertyName) function. This function’s parameter "a" is an anchor element with the class value set to "filterButton" and the parameter "propertyName" is the name of the property which the column stores. Notice the HTML markup (in this case the property name is "Name"):

XML
<th class="header_Name">
    <a id="a_ID" href="#">Name</a>
    <div style="float: right; width: 0px;"></div>
    <a href="#" class="filterButton">
        <img src="/Content/Images/up.jpg" alt="" />
     </a></th>

Each filter panel should contain a list of check boxes with their states set based on the values of the hidden fields inside the FORM element:

XML
<form  ><div id="div_Name">
        <input id="filter_Name_George" name="filter_Name_George" 
	type="hidden" value="False" />
        <input id="filter_Name_John" name="filter_Name_John" 
	type="hidden" value="False" /></div></form>

The createFilterPanel function builds up the check box list dynamically based on the hidden fields. The filter panel “pnl_Filter” that gets created is of type YAHOO.widget.Panel. The Panel component suits the job to host our check box list. Note that all filter panels are created ahead when the page is finished loading, and a filter panel is opened only when the user clicks on the respective anchor element. An important thing to notice is that when a filter panel is opened, we loop through the check box list for the property name and insert the attribute “initialValue” for each check box element with the value of the current state of the check box. Why we need that? Imagine the following scenario: instant filtering is off and no filters are applied; the user clicks on a filter anchor, a filter panel is opened and the user checks some check boxes but before he closes the filter panel, he doesn’t push the filter button on the panel. This way no filtering is applied but the check boxes remain checked, so when this same filter panel is opened again the user will see the checked check boxes and will notice that filtering by those check boxes was not applied. We want to eliminate this kind of behavior.

The following code sets the “initialValue” attribute:

JavaScript
$("#pnl_Filter_" + propertyName + " input[type='checkbox']").each(function (i, input) {
    $(input).attr("initialValue", $(input).attr("checked"));
});  

If instant filtering is on, when a filter check box’s status is changed, the page is submitted but we don’t want the user to be able to mess around with the filter check boxes by checking/unchecking them while the filtering is in progress. Because of this, we disable the controls on the filter panel by calling the changePopupStatus function. This function receives the value that indicates whether to enable or disable the controls and the id of the filter panel. Note that we don’t need to enable the filter panel because a full post-back invalidates this setting.

We omitted the discussion of the code that sets up the filter hidden fields. The setupFilterValues (propertyName) function’s role is to loop through the check box list which corresponds to the property name received as a parameter and set the respective hidden fields’ values according to the check boxes’ state:

JavaScript
function setupFilterValues(propertyName) {
    $("#pnl_Filter_" + propertyName + " input").each(function (i, input) {
        var hdn_input =
            $("#div_" + propertyName + " input[name='filter_" + 
		propertyName + "_" + input.value + "'][type='hidden']");

        hdn_input.attr("value", input.checked);
    });
};

Some words about sorting: the hidden field “hdn_sortMode” stores the property name by which the sorting should occur and the hidden field “hdn_sortMode” stores the mode of the sorting (“asc” or “desc”). When the user clicks on the header text of a column, the sorting is applied instantly. The following code handles the click event on the header anchor:

JavaScript
a_HeaderName.click(function () {
    if (hdn_SortBy.attr("value") == propertyName &&
        hdn_SortMode.attr("value") == "asc") {
        hdn_SortMode.attr("value", "desc");
    }
    else {
        hdn_SortBy.attr("value", propertyName);
        hdn_SortMode.attr("value", "asc");
    }

    submit(false);
});

The Ajax Enabled Version

The ItemsListAjax.ascx and ItemsListAjaxJS.js files are similar to the discussed ones, so I will only highlight the major differences between them.

On the ItemsListAjax user control, we have an IMG element ”img_Loader”; this will be shown when an Ajax request is made. The hidden fields inside the FORM element are the same as in the case of the ItemsList user control, as a matter of fact we don’t need the hdn_showFillter control because this time we’ll do partial post-backs, and the filter panels we’ll be created only once, i.e. on the first response from the server. But to use the same controller method MaintainList(), we need to accommodate to its parameter list.

In the case of ItemsListAjaxJS.js file, we’ll have some more differences compared to its non-Ajax version. The first thing you’ll notice is that there is attached an event handler to the submit function of the FORM element. Inside this function, there is attached a custom function to the JQuery.ajax object’s success property. This function gets called if the request succeeds. Inside this function, we need to update the content of our table.

If you look at the MaintainList() method, you’ll notice that it returns a JsonResult object. This object’s Data property contains a custom object that we send back to the client. It is defined as:

C#
var resultData =
    new
    {
        Items = currentItems,
        Pages = (int)Math.Ceiling ((double)_allItems.Count () / pageSize),
        Page = page,
        SortBy = sortBy,
        SortMode = sortMode,
        NumberOfRecords = _allItems.Count ()
    };

    return new JsonResult { Data = resultData };

As you can see, the resultData object contains all the relevant information we need to update our table on the client side. I’ll describe how the TR elements of the table are updated. The following code does this:

JavaScript
$(result.Items).each(function (i, item) {
    var tr = "<tr>";
    $.each(item, function (property, value) {
        if (value.toString().indexOf("Date") != -1) {
            var re = /-?\d+/;
            var m = re.exec(value);
            var d = new Date(parseInt(m[0]));

            value = d.format("m/d/yyyy HH:MM:ss TT");
        }
        tr += "<td>" + value + "</td>";
    });

    tr += "</tr>";

    tbody.append(tr);
});

The code above iterates through all the items of the result object and creates a TR element for each one. To access the property values of each item (e.g. Person object in our case) we iterate through the properties of the item. We could also access the property values by calling item.ID, item.Name, etc. but in that case, the code would depend on the Person class and it won’t work for any other. Note that in the case of the .NET DateTime object, the JSON string representation of it doesn’t look the same as what the .NET ToString() method returns (e.g. it renders like "/Date(545346000000)/" which is clearly not what we expect), so we need to convert it to a JavaScript Date object and then format it according to our needs.

As I mentioned previously, the filter panels will be created only once by the createFilterPanel () function. When instant filtering is on and a filter check box's status is changed, we need to disable the controls on the filter panel to prevent the user from messing around with the other check boxes. In contrast with the non-Ajax version, the filter panel is not recreated on partial post-backs, so after we have disabled it, we need to enable it again. As I mentioned the changePopupStatus() function needs the id of the filter panel which has to be enabled or disabled. Because of this, we store the id of the disabled filter panel in the $.disabledPopupName variable; and when the Ajax response arrives, we know which filter panel has to be enabled.

MvcTableDemo.Tests

If you take a look at the MvcTableDemo.Tests project, you'll notice 3 classes. The PersonsRepositoryCreator class has a static method that creates a mocked IPersonRepository object with the specified number of Person objects. The 2 other classes contain test methods for the ItemsListController's InitialList() method and MaintainList() method, respectively. The naming of the methods are such that they explain what they actually test so I won't go into the details now. Note that in the case of tests written for the MaintainList() method, there are two versions for a test (where it is necessary): one that tests something in non AJAX mode and one that tests something in AJAX mode.

History

  • 17 November 2010: Initial release
  • 27 January 2011: Source code updated to handle NULL values in the data model (e.g. a Person object's properties can be set to NULL).

That was all, I hope you enjoyed it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey21-May-12 3:38
professionalManoj Kumar Choubey21-May-12 3:38 
Generalthanks for sharing Pin
Pranay Rana21-Feb-11 2:50
professionalPranay Rana21-Feb-11 2:50 
GeneralMy vote of 4 Pin
Ramu Sangabathula Original15-Feb-11 11:03
Ramu Sangabathula Original15-Feb-11 11:03 
GeneralCool Pin
shakil03040031-Feb-11 0:06
shakil03040031-Feb-11 0:06 
Generalhelp full Pin
Pranay Rana26-Jan-11 23:16
professionalPranay Rana26-Jan-11 23:16 
GeneralRe: help full Pin
Zoltan Aszalos26-Jan-11 23:20
Zoltan Aszalos26-Jan-11 23:20 
GeneralMy vote of 5 Pin
CarlCarl24-Jan-11 9:00
CarlCarl24-Jan-11 9:00 
QuestionNull data Pin
CarlCarl20-Jan-11 7:10
CarlCarl20-Jan-11 7:10 
AnswerRe: Null data Pin
CarlCarl24-Jan-11 8:54
CarlCarl24-Jan-11 8:54 
GeneralRe: Null data Pin
Zoltan Aszalos25-Jan-11 4:21
Zoltan Aszalos25-Jan-11 4:21 
GeneralRe: Null data Pin
CarlCarl25-Jan-11 4:25
CarlCarl25-Jan-11 4:25 
GeneralRe: Null data Pin
Zoltan Aszalos26-Jan-11 4:20
Zoltan Aszalos26-Jan-11 4:20 
GeneralRe: Null data Pin
CarlCarl26-Jan-11 7:11
CarlCarl26-Jan-11 7:11 
GeneralRe: Null data Pin
Zoltan Aszalos26-Jan-11 7:17
Zoltan Aszalos26-Jan-11 7:17 
GeneralRe: Null data Pin
Zoltan Aszalos26-Jan-11 21:22
Zoltan Aszalos26-Jan-11 21:22 
GeneralMy vote of 4 Pin
L Hills29-Nov-10 6:14
L Hills29-Nov-10 6:14 
GeneralRe: My vote of 4 Pin
Zoltan Aszalos30-Nov-10 1:37
Zoltan Aszalos30-Nov-10 1:37 
Generalthanks for having eksemple in 2008 also Pin
hestol23-Nov-10 20:20
hestol23-Nov-10 20:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.