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:
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.
To remove a column (for example, the 2nd column):
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.
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);
}
}
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:
- 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. - After filling in the data, the user can submit the update panel (by clicking submit or update button perhaps).
- 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.