Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Create Your Own GridView Control - the ALT.Net's Way

4.83/5 (10 votes)
26 Mar 2012CPOL9 min read 76.3K   4.5K  
A GridView user control that you can modify and customise to produce the specific grid you want.

Background

ASP.NET provides the very powerful and flexible GridView control, but even with its 100+ properties and events that you can use to customise the grid, you are bound to find some use cases that the GridView cannot satisfy. One example, the paging template auto appears when you have more than one page and you cannot customise this behaviour, e.g. make it always appear even you only have one page.

The inability to customise the look and feel of the grid in any way you want is just an annoyance, but no one will blame Microsoft because there are literally unlimited number of ways to customise a grid and no grid control can satisfy everyone's needs.

However, the Archillies' heel of GridView (at least from the viewpoint of ALT.Net guys like me) is its data source model. GridView's advanced functionality, e.g. sorting, paging, caching and updating only works with data sources that follow the ASP.NET Provider Model, e.g., SqlDataSource, ObjectDataSource, and LinqDataSource which you have to wire up to a data source (SQL table, collection of objects, etc) in your page UI or code behind.

Often a properly multi-tiered web application handles (therefore have a dependency on) the data source in the data access layer (DAL) and DAL only. The UI and business logic layer merely deals with domain model objects returned by the DAL often using an ORM framework (e.g., NHibernate, Entity Framework) and the repository pattern. This design is the current best practice, and ASP.NET MVC framework also abandoned data source provider model to facilitate proper separation of concern in application design.

The GridView can take a collection of domain model objects (returned by DAL) as the data source and auto generate the columns based on the object's properties. But that is about it, you have to write your own code to do paging, sorting, caching and updating.

Introduction

Unable to get the desired behaviour from the ASP.NET built-in GridView control, I decided to write a grid control (by wrapping up GridView) myself for a client of mine.

The grid control I developed (I call it GenericGridView) overcomes the aforementioned problems of ASP.NET built-in GridView control and also satisfies all the requirements of my client, but it may not meet your needs. However, I hope that my code will provide you a base model that you can modify (hopefully slightly only) to create your very own grid control that makes your client happy.

I believe that by wrapping the GridView control, you can easily create your own grid control that can look, feel and behave anyway you want, no matter how absurd the requirement is. If you cannot find a solution, post your problem in the comment, and I will see whether I can offer some ideas.

Using the code

If the link on the top doesn't work, try this link: www.codeproject.com/KB/aspnet/349142/GenericGridViewDemo.zip.

The zip file is a Visual Studio 2010 solution, but 99% of the code will still run in .NET 3.5. You can manually convert the solution to Visual Studio 2008 if you have to. Look for GenericGridView.ascx file and its code behind. They will lead you to everything else.

The unit tests are written in a Behaviour Driven Development style using xUnit with xunit.bdd extension. To run the tests, you need to have xunit test runner installed.

Note that the code is not thoroughly unit tested, because the purpose of the code is not to give you an off-the-shelf control that you can use, but to provide you an example of how to create your own grid control. I only include enough unit tests to show you some worthy techniques, e.g., control mocking, light weight code behind, etc.

GenericGridView Design Rationale

GenericGridView is a wrapper of GridView control. Internally, the GridView's ViewState is disabled because I prefer to actively manage the state of the GridView myself. It is like driving a manual car vs auto one. If you prefer efficiency and control, don't use ViewState.

The code behind contains virtually no logic. All the processing is in the helper classes which are then unit tested.

Data Source

The GenericGridView takes an IEnumerable object (therefore you can use LINQ query results) as its data source, and internally convert it to a list because GridView cannot use LINQ query results that rely on deferred execution for its sorting and paging operation. Take a look at DataSourceHelper class, and you will see how I did this.

Sadly GridView cannot take generic collection as its datasource. As a result, I have to treat the datasource as a collection of objects and use reflection in some places to get the property values.

User Control vs Server Control

The GenericGridView is a user control (an ASCX page) rather than server control (DLL). I did it this way because it is much easier to add JavaScript to user control than to a server control. It is also a lot more straightforward to test the JavaScript in user control than in server control.

Adjusting the Number of Columns

The columns are auto generated based on the properties of the data source object. For example, if you have a domain object - Product - with Name, Description, Price as its properties, the following

myGenericGridView.DataSource = _listOfProducts 

will produce a grid with three columns - Name, Description, Price. You can handle the OnHeaderFormatting event to change the column header to any words any format.

To add or reduce the number of columns, the easiest way is to use the LINQ and anonymous types:

myGenericGridView.DataSource = _listOfProducts.Select(p=> new
                                         {
                                             p.Name, 
                                             p.Description, 
                                             p.Price, 
                                             MyNewColumn = p.Price * 100
                                         });

this will give you a grid of 4 columns - Name, Description, Price, and MyNewColumn.

However, convenient as it is, the anonymous type has some drawbacks - it cannot inherit from another class and you cannot apply attributes to it (therefore you cannot cache them in the session state which can only cache serializeable objects).

So if you want caching and inheritance, you can create a new class with the properties exactly matching the grid you want. Don't worry about creating a new class just for a grid. Your new class will just be like the view model in ASP.Net MVC. It is an acceptable practice and you just need to put all your view models in a logic place.

Another way to add/remove column is to handle the RowDataBound event in your code:

In your page, you supply a hander to RowDataBound event: GridView_RowDataBound

<%@ Register TagPrefix="my" TagName="GenericGridView" Src="~/GenericGridView.ascx" %>

<my:GenericGridView runat="server" ID="myGV" AllowPaging="true" 
          AllowSorting="true" OnRowDataBound="GridView_RowDataBound" />

then in your code behind, you do this:

C#
protected void GridView_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.Header)
    {
        var thc = new TableHeaderCell();
        thc.Text = "Click Me";
        e.Row.Cells.Add(thc);
    }

    if ( e.Row.RowType == DataControlRowType.DataRow)
    {
        var tc = new TableCell();
        tc.Controls.Add(new CheckBox());
        e.Row.Cells.Add(tc);
    }
}

this will add a new column containing a check box for each row.

Image 1

To remove a column (for example, the 2nd column):

C#
protected void GridView_RowDataBound(object sender, GridViewRowEventArgs e)
{
   e.Row.Cells.Remove(e.Row.Cells[1]);
} 

Paging

The PagerTemplate only appears when there are more than one pages, and this behaviour cannot be customised. Also the PagerTemplate may not be flexible enough to provide the layout and behaviour you want. So in my GenenricGridView, I use a table below the GridView as the pager control and because it is external to the GridView, it is not restricted in anyway. You can customise the pager control anyway you want.

To get the paging to work:

  • set the GridView.AllowPaging property to true,
  • calculate page size and total number of pages (NavigationHelper.PreparePaging())
  • associate an event handler with the paging controls that will cause a postback (NavigationHelper.SetUpNavigationBar())
  • handle the page navigation in the event handler (GridView_Navigate())

Please note that using an external pager control requires a lot of more code than using the GridView PagerTemplate. If you are happy with the PagerTemplate's behaviour, stick to PagerTemplate.

Sorting

When the data source is IEnumerable, the GridViewSortEventArgs won't contain the correct SortDirection. So I use SortingHelper.PrepareGridViewSortEventArgs() to correct the SortingDirection.

The GenericGridView also expose an event - OnSorting which let you customise the sorting behaviour. For example, if you have a column of Rating containing High, Low and Medium. You can sort the column as High-Medium-Low. I included an example of such sorting using this technique.

Formatting

It exposes two events - OnHeaderFormatting and OnRowDataBound to let you customise each cell of the grid. You can, for example, replace some cell content with a dropdown box or radio button; or change the font or colour of some text inside the grid; or attach a client side JavaScript event or server side event to a cell or a row.

I included some examples of using OnHeaderFormatting and OnRowDataBound in the code which show you that creating a nested grid is a breeze comparing to other techniques I found in codeproject.

C#
protected void GridView_RowDataBound(object sender, GridViewRowEventArgs e)
{
   if (e.Row.RowType == DataControlRowType.DataRow)
   {
       var gv = LoadControl("GenericGridView.ascx") as GenericGridView;
       gv.DataSource = new[] { new { Currency = "$", ((Product)e.Row.DataItem).Price } };
       e.Row.Cells[3].Controls.Add(gv);
   }
}

Image 2

Apart from the two events, the GenericGridView doesn't expose many properties to let you customise the look and feel of the grid, e.g. row style, alternative row style, but you can easily do so if you want to.

Caching

The GenericGridView doesn't provide built-in support for caching because it shouldn't. If you want to use GenericGridView, you need to supply a list of objects. If you want caching, you can cache the source data yourself in your page rather than in the grid. However, all this boils down to your requirement. If all your grids should cache the same way, then putting the caching in the grid is fine. Typically objects would be cached in the session state.

Update, Insert, and Delete

Sorry I didn't provide any example of update, insert and delete in the code. But it is really simple to do so. You would typically follow these steps:

  1. Handle the GenericGridView OnRowDataBound event and attach either a client side JavaScript event or server side event to a cell or row, and when the user clicks the cell or row, you display a update panel of your choice. You can design the update panel anyway you like.
  2. After filling in the data, the user can submit the update panel (by clicking submit or update button perhaps).
  3. You handle the postback, extract the data posted by the update panel and update your data source accordingly.

AJAX

Since the GenericGridView is a user control. You can wrap it up inside a UpdatePanel, then you will have the very nice AJAX effect. Note that if you do use UpdatePanel and you have some JavaScript code you want to run each time the page loads, you have to put your js function inside: Sys.WebForms.PageRequestManager.getInstance().add_endRequest();

Conclusion

Perhaps I haven't provided enough examples to convince you what the GenericGridView or its variations are capable of. Neither do I take the time to explain the basic concepts thoroughly. I feel compelled to reiterate that the GenericGridView is not an off-the-shelf control that you can use, but an example of how you can create your own grid control easily and swiftly. And the grid can be fully unit tested using mocking (HttpContext and HttpRequest can be easily mocked even though I haven't got examples of such mocking in my code here).

If you have some peculiar requirements that you don't know how to translate into a grid control, post your problem in the comment section, I will try to help. Or I am sure some of the readers will be able to help.

License

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