Code based ASP.NET MVC GridView

ASP.NET MVC GridViewEarlier this week a colleague of mine asked me if there was such thing as a  DataGrid or GridView or something like that in the ASP.NET MVC framework. My first answer was: "Nope!". I advised him to look for a nice foreach implementation or using ExtJS, Dojo or similar. Which made me think... Why not create a simple GridView extension method which generates a nice looking, plain-HTML grid with all required features like paging, editing, deleting, alternating rows, ...?

The idea was simple: an extension method to the HtmlHelper class would be enough. Required parameters: a header and footer template, item template, edit item template, ... But how to pass in these templates using a simple C# parameter... Luckily,  C# 3.0 introduced lambdas! Why? They are super-flexible and versatile! For instance, take the following code:

[code:c#]

// C# code:
public void RenderPerson(Person p, Action<T> renderMethod) {
    renderMethod(p);
}

// ASP.NET code:
<% RenderPerson(new Person(), person => { %>
    Hello! You are <%=person.Name%>.
<% } %>

[/code]

It translates nicely into:

[code:c#]

Response.Write("Hello! You are Maarten.");

[/code]

Creating a GridView extension method should not be that hard! And it sure isn't.

Live demo

Perhaps I should put this last in my blog posts, but there are always people who are only reading the title and downloading an example:

1. The GridView extension method

Quite short and quite easy:

[code:c#]

public static class GridViewExtensions
{
    public static void GridView<T>(
        this HtmlHelper html,
        GridViewData<T> data,
        Action<GridViewData<T>> headerTemplate,
        Action<T, string> itemTemplate,
        string cssClass,
        string cssAlternatingClass,
        Action<T> editItemTemplate,
        Action<GridViewData<T>> footerTemplate)
    {
        headerTemplate(data);

        int i = 0;
        foreach (var item in data.PagedList)
        {
            if (!item.Equals(data.EditItem))
            {
                itemTemplate(item, (i % 2 == 0 ? cssClass : cssAlternatingClass));
            }
            else
            {
                editItemTemplate(item);
            }

            i++;
        }

        footerTemplate(data);
    }
}

[/code]

2. GridViewData

Of couse, data will have to be displayed. And we'll need a property which sets the current item being edited. Here's my Model I'll be passing to the View:

[code:c#]

public class GridViewData<T>
{
    public PagedList<T> PagedList { get; set; }

    public T EditItem { get; set; }
}

[/code]

By the way, the PagedList<T> I'm using is actually a shameless copy from Rob Conery's blog a while ago.

3. The View

Of course, no rendered HTML without some sort of View. Here's a simplified version in which I pass the GridView<T> extension method the required data, header template, item template, edit item template and footer template. Also noice the alternating rows are simply alternating CSS styles (item and item-alternating).

[code:c#]

<%Html.GridView<Employee>(
    this.ViewData.Model,
    data => { %>
        <table class="grid" cellpadding="0" cellspacing="0">
    <% },
    (item, css) => { %>
        <tr class="<%=css%>">
            <td><%=Html.ActionImage<HomeController>(c => c.Edit(item.Id), "~/Content/edit.gif", "Edit", null)%></td>
            <td><%=Html.ActionImage<HomeController>(c => c.Delete(item.Id), "~/Content/delete.gif", "Delete", null)%></td>
            <td>&nbsp;</td>
            <td><%=item.Name%></td>
            <td><%=item.Email%></td>
        </tr>
    <% },
    "item",
    "item-alternating",
    item => { %>
        <%using (Html.Form<HomeController>(c => c.Save(item.Id), FormMethod.Post, new { id = "editForm" })) {>
            <tr class="item-edit">
                <td><%=Html.SubmitImage("save", "~/Content/ok.gif", new { alt = "Update" })%></td>
                <td><%=Html.ActionImage<HomeController>(c => c.Index(), "~/Content/cancel.gif", "Cancel", null)%></td>
                <td>&nbsp;</td>
                <td><%=Html.TextBox("Name", item.Name)%></td>
                <td><%=Html.TextBox("Email", item.Email)%></td>
            </tr>
        <% } %>
    <% },
    data => { %>
        </table>
<% });%>

[/code]

4. The Controller

The Controller is perhaps the hardest part: it contains all methods that handle actions which are requested by the View. I have a Show action which simply shows the View with current data. Also, I have implemented an Edit and Save action. Make sure to check my example code download for the full example (earlier in this post).

[code:c#]

// ...

public ActionResult Show(int? page)
{
    CurrentPage = page.HasValue ? page.Value : CurrentPage;
    GridViewData<Employee> viewData = new GridViewData<Employee>
    {
        PagedList = Employees.ToPagedList<Employee>(CurrentPage, 4)
    };

    return View("Index", viewData);
}

public ActionResult Edit(int id)
{
    GridViewData<Employee> viewData = new GridViewData<Employee>
    {
        PagedList = Employees.ToPagedList<Employee>(CurrentPage, 4),
        EditItem = Employees.Where( e => e.Id == id).FirstOrDefault()
    };

    return View("Index", viewData);
}

public ActionResult Save(int id)
{
    BindingHelperExtensions.UpdateFrom(
        Employees.Where(e => e.Id == id).FirstOrDefault(),
        Request.Form
    );
    return RedirectToAction("Show");
}

// ...

[/code]

Note: based on ASP.NET MVC preview 3

kick it on DotNetKicks.com 

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

28 responses

  1. Avatar for David
    David June 4th, 2008

    Nice. I had used the same technique to create a DataList like view (though no paging).

    You can probably abstract out the <table>, <tr>, <td> tags using Response.Write(). But yeah, good stuff. Thanks for sharing.

    Here's the code:

    public static void RenderSimpleTable<T>(
    this HtmlHelper helper,
    IEnumerable<T> items,
    int columns,
    Object tableTagAttributes,
    Object trTagAttributes,
    Object tdTagAttributes,
    Action<T> itemRenderHandler)
    {
    helper.RenderSimpleTable(
    items,
    columns,
    tableTagAttributes,
    trTagAttributes,
    tdTagAttributes,
    itemRenderHandler,
    true);
    }

    public static void RenderSimpleTable<T>(
    this HtmlHelper helper,
    IEnumerable<T> items,
    int columns,
    Object tableTagAttributes,
    Object trTagAttributes,
    Object tdTagAttributes,
    Action<T> itemRenderHandler,
    bool evenlySpaceColumns)
    {
    var percent = ((int)(((float)1 / (float)(columns)) * 100)).ToString() + "%";
    var response = helper.ViewContext.HttpContext.Response;

    var tableTagAttributeString
    = tableTagAttributes != null
    ? " " + tableTagAttributes.ToAttributeList()
    : String.Empty;

    var trTagAttributesString
    = trTagAttributes != null
    ? " " + trTagAttributes.ToAttributeList()
    : String.Empty;

    var tdTagAttributesString
    = tdTagAttributes != null
    ? " " + tdTagAttributes.ToAttributeList()
    : String.Empty;

    var evenlySpaceStyleString
    = evenlySpaceColumns
    ? String.Format(@" style=""width:{0}""", percent)
    : String.Empty;

    helper.RenderTable<T>(
    items,
    columns,
    () => { response.Write(String.Format(@"<table{0}>", tableTagAttributeString)); },
    () => { response.Write(String.Format(@"<tr{0}>", trTagAttributesString)); },
    (item) => {
    response.Write(String.Format(@"<td{0}{1}>", evenlySpaceStyleString, tdTagAttributesString));
    itemRenderHandler(item);
    response.Write(@"</td>");
    },
    () => { response.Write(@"</tr>"); },
    () => { response.Write(@"</table>"); },
    () => { response.Write(String.Format(@"<td{0}{1}>&nbsp;</td>", evenlySpaceStyleString, tdTagAttributesString)); });
    }

    public static void RenderTable<T>(
    this HtmlHelper helper,
    IEnumerable<T> items,
    int columns,
    Action headRenderHandler,
    Action groupHeadRenderHandler,
    Action<T> itemRenderHandler,
    Action groupTailRenderHandler,
    Action tailRenderHandler,
    Action emptyRenderHandler)
    {
    headRenderHandler();
    int column = 0;

    foreach (var item in items)
    {
    if (column == 0)
    groupHeadRenderHandler();
    itemRenderHandler(item);
    if (column == columns - 1)
    groupTailRenderHandler();
    column = (column + 1) % columns;
    }

    if (column > 0)
    {
    for (int i = column; i < columns; i++)
    emptyRenderHandler();
    }

    tailRenderHandler();
    }

  2. Avatar for maartenba
    maartenba June 4th, 2008

    I smell a very nice combination here for creating on-the-fly gridview generation from any kind of data set :-)

  3. Avatar for Klaus Graefensteiner
    Klaus Graefensteiner June 4th, 2008

    Nice MVC example!

  4. Avatar for Glenn
    Glenn June 4th, 2008

    Nice stuff. What really needs to occur for Web development to move forward. A couple things though...

    Minor - it looks like the Cancel and Delete icons are mixed up. (Cancel shows to delete line, Delete shows to Cancel Edit.)
    Not so Minor - Sorting would be needed for what I usually use a GridView for.
    Not so Minor - It posts back with each added line or change. Can't it do this using Ajax? This doesn't look much better than a using ViewState Web Page Control.

  5. Avatar for maartenba
    maartenba June 4th, 2008

    That's a consideration to make... I would say: use some of the advanced JavaScript + Ajax grids out there (see start of my post). If you need something similar to the old "postback" method, this would be the way to go. Usage really depends on how and what and when.

  6. Avatar for Rob Conery
    Rob Conery June 5th, 2008

    Maarten - great stuff! I've noodled with this stuff using a ComponentController (as that's what this is...) - check it out. That's why we made the ComponentController - for this very thing!

  7. Avatar for maartenba
    maartenba June 5th, 2008

    Actually this was the first thing that popped up in my mind. I've used the ComponentController before in the Mvc Membership provider (www.codeplex.com/MvcMembers... and it proved quite nice to work with.

    Using a ComponentController for something like a gridview would indeed be an interesting approach, as one could use 1 ComponentController base class, if needed a specific ComponentController, and multiple Views with that.

  8. Avatar for Mohammad Javed
    Mohammad Javed June 9th, 2008

    your are providing good code.

  9. Avatar for Duckie
    Duckie June 23rd, 2008

    Hm, is there any way to convert this to vb.net, i tried and failed :-)

    sp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>Employees</h2>
    <%Html.GridView<Employee>(
    this.ViewData.Model,
    data => { %>
    <table class="grid" cellpadding="0" cellspacing="0">
    <tr>
    ....

  10. Avatar for maartenba
    maartenba June 23rd, 2008

    Affraid I can't help you there. Are multi-line lambda's supported in VB.NET?

  11. Avatar for Duckie
    Duckie June 23rd, 2008

    Does not look like it :-(

  12. Avatar for dkl
    dkl July 10th, 2008

    It seems a little bit too complicated. Too many lambdas just to abstract away one foreach cycle. This is the real meat of this approach:

    [quote]
    headerTemplate(data);

    int i = 0;
    foreach (var item in data.PagedList)
    {
    if (!item.Equals(data.EditItem))
    {
    itemTemplate(item, (i % 2 == 0 ? cssClass : cssAlternatingClass));
    }
    else
    {
    editItemTemplate(item);
    }
    i++;
    }

    footerTemplate(data);
    [/quote]

    Should we sacrifice readability just because one simple foreach cycle?

  13. Avatar for Swordfish
    Swordfish July 12th, 2008

    I second the remark of dlkl (just above). Only why not using the new 3.5 DataList? It's works perfectly with MVC (See Scott Gurthie blog) and it's not too hard to hard to turn it into a datagrid with ajax support. That's reminds me, A few weeks ago I came across a project name Ajax Data Controls from dotnetjunkies. It can do the same as the 'one-liner' function but it has proper template support, it has better maintainability (very important in larger projects where multiple developers are involved) and as dlkl said: readability is also important.

  14. Avatar for Mani
    Mani July 21st, 2008

    Nice & awesome thank u very much fr giving this code...

  15. Avatar for vamsi
    vamsi May 2nd, 2009

    Hi all,

    can any one of you ppl please send me the code to bind gridview in mvc 1.0.

    i am getting the following error when i try to bind data to the gridview.

    [b]Compiler Error Message[/b]: CS1928: 'System.Web.Mvc.HtmlHelper' does not contain a definition for 'GridView' and the best extension method overload 'MvcGridView.Extensions.GridViewExtensions.GridView<T>(System.Web.Mvc.HtmlHelper, MvcGridView.Extensions.GridViewData<T>, System.Action<MvcGridView.Extensions.GridViewData<T>>, System.Action<T,string>, string, string, System.Action<T>, System.Action<MvcGridView.Extensions.GridViewData<T>>)' has some invalid arguments

    Thanks in Advance,
    vamsi.

  16. Avatar for GreatButNotForReleasedVersion
    GreatButNotForReleasedVersion May 16th, 2009

    Unfortunately this no longer works for MVC1

    This is the problem with eatrly adoption I guess, nothing works after the RC ( in Microsoft )

    And I"m too new to help fix it or I would but just a heads up it doesn't work anymore.

  17. Avatar for maartenba
    maartenba May 25th, 2009

    I have uploaded an ASP.NET MVC 1.0 version: http://blog.maartenballiauw...

  18. Avatar for Petey
    Petey June 17th, 2009

    It's a great thing you post the code up-front!! I hate it when bloggers put the download link all the way in the bottom, or even worse, somewhere in the middle.

  19. Avatar for Kishan Hathiwala
    Kishan Hathiwala July 27th, 2010

    Hi, Thanks for the great code. What am trying to do is I am trying to use this gridview in master/details application. I have created 2 tabs in the first tab i see master table fields and in the second tab am trying to setup this gridview with add/edit/delete functionality. I am already done with majority of the task but am getting the following error while at design time: [b]Cannot implicitly convert type 'void'[/b] to 'object' AND at runtime am getting: [b]CS1525: Invalid expression term ')'[/b]. I can surely help with the code if needed.

  20. Avatar for Kishan Hathiwala
    Kishan Hathiwala July 28th, 2010

    Never mind I was able to figure it out. Thanks once again for the great article

  21. Avatar for Alexander Freiria
    Alexander Freiria July 28th, 2010

    Thanks for sharing the code snippet. I've been using a modified version for projects that I have been working on... Thanks Much!

  22. Avatar for Steve
    Steve July 29th, 2010

    Hi David- do you have an example of how this DataList could be used in a nested fashion (such as :

    Category1
    product1
    product2
    Category2
    product3
    product4
    .
    .

    Thanks,

    Steve

  23. Avatar for Greg
    Greg November 3rd, 2010

    Your live demo ...

    http://examples.maartenball...

    FAILS :(

  24. Avatar for Benny
    Benny April 28th, 2011

    This code works great in MVC1 however I&#39m trying to use this in an MVC3 project with Razor in .Net 4. I&#39m having trouble with upgrading the code as the assembly system.web.mvc seems to be a very old version. Any suggestions?

  25. Avatar for Muhammad Sajjad Nawaz
    Muhammad Sajjad Nawaz September 6th, 2013

    Excellent idea and nice way done. I really enjoyed learning this.But there is still an issue for me: I want to use aspx page as partial view in MVC 4. If i do that, I have to inherit it from "System.Web.Mvc.ViewPage<dynamic>" and I can not use the code behind (.cs) file. The real purpose to implement Grid View in MVC dies for me. can u suggest something for this problem ???

  26. Avatar for Nimesh
    Nimesh June 28th, 2014

    Excellent one.... Simple and Speedy. Please give in razor also if possible

  27. Avatar for vijay
    vijay September 4th, 2015

    it will not work with masterpage

  28. Avatar for vijay
    vijay September 4th, 2015

    the same thing i want it in mvc2