Efficient Paging with WebGrid Web Helper - ASP.NET MVC 3 RC

Posted by: Malcolm Sheridan , on 12/9/2010, in Category ASP.NET MVC
Views: 154096
Abstract: The following article demonstrates one way of performing efficient paging using the WebGrid WebHelper in ASP.NET MVC 3 RC.

Last week I published an article on how to use the WebGrid WebHelper in ASP.NET MVC 3 RC. This was a beginners article on getting started. The one piece of functionality I didn’t touch on was paging. Out of the box, the WebGrid has a built-in Pager object, which handles the rendering of paging links underneath the WebGrid. The biggest downside of using this paging is smart paging isn’t enabled. What I mean by that is if you have a WebGrid fetches it data from 10,000 records from a database, but it only displays 10 records per page, each time you click on a paging link, you’re not retrieving 10 records you want to display, you’re fetching the 10,000 records each time and only displaying 10. This is terribly inefficient when you hit a large scale system. Well this article you’re reading right now will show you one possible way of creating efficient paging and still using the WebGrid.

Before moving on, you need to download ASP.NET MVC 3 RC. Click here to download and install them using the Microsoft Web Platform Installer.

The Microsoft Web Helpers are added as a reference by default now in ASP.NET MVC 3 RC. I’ve left the instructions on how to add them in this article in case you need to do this as a separate exercise.

Open studio 2010 and create a new ASP.NET MVC 3 Web Application (Razor) project. The new MVC 3 dialog can be seen below:

Empty Template Razor 

Choose Razor as the view engine and click OK.

The next step is to download the Microsoft Web Helpers library through NuGet. Follow these steps to do this:

Step 1. Right click the website and choose Add Package Reference.

Mvc Add Package Reference 

Step 2. The Add Package Reference dialog will open. Search for Microsoft in the search box in the top right corner. Upon completion you'll see microsoft-web-helpers option. Select that option and click Install.

Microsoft Web Helpers

Step 3. Click 'Close' to return to the project

Check the references and you'll see that the library has been added to the project. 

System Web Helpers 

Now it's time to start coding! For this example I’ve created a model called FavouriteGivenName. This will store 10 records. The model would normally be a database, but I’m using a smaller set of data to illustrate the paging examples.

Default Paging in MVC 3 WebGrid

By default, the WebGrid’s paging functionality will retrieve the entire set of data for each paging request. To see this in action, create a Home controller and add an index action. 

Home Controller IndexAction 

The code above is setting model for the view as a collection of FavouriteGivenName’s. Add the following code to the view: 

MVC CollectionView 

The output from the above code is shown below. 

MVC Grid Pager 

The grid.Pager method sets the paging for the WebGrid. The problem is when you page through the data, all the data is returned. If you try to limit the data being returned, the problem you’ll encounter because you’re only returning a subset of the data is the WebGrid thinks there’s only that amount of data to display, so the paging links will disappear! Not good! So to show you what this looks like, I’m going to only return the first 5 records for the WebGrid, because I only want to display 5 records per page. Here’s my updated action: 

MVC ActionResult 

Now because I’m returning only 5 instead of 10 records, the paging has disappeared from the WebGrid! 

MVC Grid NoPaging 

Efficient Paging in MVC 3 WebGrid Web Helper 

The solution I came up with is one possible solution. I’m sure there are many others, but this works well. The issue around the WebGrid’s paging is it needs the total count of data that the grid is supposed to display. If you limit the WebGrid to only the records you want displayed, you limit the total count and the paging doesn’t work. To fix this problem you need to separate the two. One set of data for the grid and a separate piece of data that tells the WebGrid how many paging links to display. Unfortunately the paging object is locked, so I had to make my own.

To incorporate the
DRY (Don’t Repeat Yourself) principal, I’m going to create an action method that returns the WebGrid as one object, and a total record count as another object and wrap both of them in a JSON object. Here’s the action method. 

MVC Json Efficient Paging 

The JSON being returned stores two object.  

  • Data – this stores the data only needed for the grid. It utilises the Skip and Take methods to limit the data being returned. It is returning the WebGrid as HTML. This ensures this code will replicate what the WebGrid would look like if we were declaring it in the Razor mark-up.
  • Count – this stores the total records. This will be used for creating the paging links.

The view for this is simple. All the hard work is in the JavaScript. Here’s the JavaScript below that calls the EfficientPaging action and displays the returned WebGrid and creates the paging links. 

JavaScript Efficient Paging 

The result of this is below. 

MVC Grid Paging 

It looks the same as the previous example, but now if you page through the data, the page you’re requesting will be sent as a parameter to the action method, and only that data will be fetched from the server. 

MVC Json Debug 

Now you have efficient paging and are still using the WebGrid WebHelper! The only difference is you’re creating your own paging links.

Like I mentioned earlier, this is only one possible solution, but for me this works well because I’m still creating only one WebGrid. If you think of other ways to do this, please let me know.

The entire source code of this article can be downloaded over here

Give me a +1 if you think it was a good article. Thanks!
Recommended Articles
Malcolm Sheridan is a Microsoft awarded MVP in ASP.NET, a Telerik Insider and a regular presenter at conferences and user groups throughout Australia and New Zealand. Being an ASP.NET guy, his focus is on web technologies and has been for the past 10 years. He loves working with ASP.NET MVC these days and also loves getting his hands dirty with jQuery and JavaScript. He also writes technical articles on ASP.NET for SitePoint and other various websites. Follow him on twitter @malcolmsheridan


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Canumstag on Thursday, December 9, 2010 4:19 AM
nicely done. what method would you adopt to test if the grid loads data efficiently. also i'm rating this article 5 stars because it deserves it.
Comment posted by Link on Thursday, December 9, 2010 4:30 AM
HTML in the Controller? I'd say that's against the whole MVC pattern. Your controller action is coupled to your View.
Wouldn't it be better to throw the grid into a partial and return it (and also use renderpartial in the "real" view?)
Comment posted by Malcolm Sheridan on Thursday, December 9, 2010 5:26 AM
@Canumstag
Thanks.  I'm glad you liked it.

@Link
Yes I could have done that.  I just didn't think of it!  Good tip though.
Comment posted by Marcin Dobosz on Thursday, December 9, 2010 2:31 PM
Just letting everyone know that this is a known issue in MVC 3 RC and has already been fixed for the next release.
Comment posted by Malcolm Sheridan on Thursday, December 9, 2010 4:26 PM
@Marcin
Great!  Thanks for letting us know.
Comment posted by Nicholas D on Thursday, December 9, 2010 6:16 PM
In MVC 3 what are the different ways to performance test an app? When will MVC 3 final be out?
Comment posted by Malcolm Sheridan on Thursday, December 9, 2010 7:11 PM
@Nicholas
Performance testing is a whole different ball game.  MVC 3's release date hasn't been announced yet.
Comment posted by westham on Wednesday, December 22, 2010 4:19 PM
Where to put eventually any @Html.ActionLink("Edit", "Edit", new { id=item.ID }) in the Grid?
Comment posted by westham on Wednesday, December 22, 2010 5:11 PM
Where to put eventually any @Html.ActionLink("Edit", "Edit", new { id=item.ID }) in the Grid?
Comment posted by ponas on Sunday, December 26, 2010 10:18 AM
it doesn't works in chrome ...
Comment posted by ponas on Sunday, December 26, 2010 10:29 AM
tfoot tag needs tr and td tags, when these tags are present in tfoot, chrome renders page numbers ...
Comment posted by Malcolm Sheridan on Sunday, December 26, 2010 8:21 PM
@ponas
I only tested it in IE and Firefox sorry.

@westham
Sorry I didn't focus on that aspect.  I'd need to look into that side for you if you want.
Comment posted by Rahul on Monday, December 27, 2010 4:31 AM
Thanks Malcolm Sheridan, this is what i expected.I know MVC only very little but i heard this WebGrid Web Helper some where and i searched to know about this but could not find properly. But you gave the proper answer with coding.

http://godwinsblog.cdtech.in/2010/12/requested-page-cannot-be-accessed.html
Comment posted by Thanigainathan on Monday, December 27, 2010 7:26 AM
Hi,

How this will be when we are using secured data ? I think this seems to be a security leak.

Thanks,
Thani
Comment posted by Mohan Gajula on Monday, December 27, 2010 7:28 AM
Seems good.. trying it out..
Comment posted by Mohan Gajula on Monday, December 27, 2010 7:29 AM
Seems good.. trying it out..

Mohan Gajula
<a href="http://iMohanG.blogspot.com">http://iMohanG.blogspot.com</a>
Comment posted by Malcolm Sheridan on Tuesday, December 28, 2010 10:46 PM
@Thanigainathan
What scenario are you thinking of?
Comment posted by Einar Arne on Wednesday, December 29, 2010 7:29 PM
This is no longer necessary. You can use the Bind method to tell the grid to use server side paging.

grdv.Bind(myData, rowCount=10000, autoSortAndPage=False)

Setting autoSortAndPage to false tells the grid that myData is just a segment of the data. It will show all rows of this data regardless of your page size setting. Pager will be built using the rowCount you pass in and not the number of records in myData.


Comment posted by Malcolm Sheridan on Saturday, January 1, 2011 5:49 AM
@Einar
Appartently this is still an issue.

http://forums.asp.net/p/1631392/4232145.aspx
Comment posted by rinku on Wednesday, January 5, 2011 4:31 AM
gridview insert textfile.
Comment posted by Julien Cousineau on Thursday, January 20, 2011 12:48 PM
your code     $("#DataTable thead").after(footer); doesnt work for me I replace it to => $("#DataTable thead").after("<tfoot></tfoot>").html(footer);  .. without the tag in the loop .. hope this help for other
Comment posted by StuTheDog on Sunday, February 20, 2011 7:55 PM
Thank you so much for this Malcolm, I know a lot of posters have been negative but this post has been very educational for me learning MVC 3 and JQuery in general. Because of you I have the grid and paging logic roundtripping splendidly.
Comment posted by Malcolm Sheridan on Thursday, March 10, 2011 3:58 PM
@StuTheDog

Thanks.  I'm glad you learned something new by reading it.
Comment posted by Marek on Tuesday, March 22, 2011 12:13 AM
Did you try ot pass a parameter of type IQueryable to the WebGrid constructor? That ensures efficient paging and sorting.
Comment posted by altaf on Thursday, May 12, 2011 3:17 AM
Sorting doest not work as it redirect to EfficientPaging?sort=Name&amp;sortdir=ASC . How to solve this issue?
Comment posted by Micha Demmers on Thursday, June 9, 2011 2:34 AM
@Einar Arne

You are right you don't have to create an own pager for server side paging.
The reason you will want to do this is that you can create your own template for paging, like digg style paging.

Also creating your own pager like this example will give you the option to send data via json and jquery wich is must faster then using the server side paging with full page postback. An other advantage of creating an own pager is more attractive links in stead of having ?page=2 etc.

I've created a more advanced pager with digg style paging based on this article http://kpumuk.info/asp-net/gridview-with-custom-digg-like-pager/. Here's the javascript:

app.grids.js:

App = {};
App.Grid = {};

App.Grid.Build = function (id, appendTo, route) {

    $.getJSON(route.join("/"), null, function (response) {

        var append = typeof (appendTo) == typeof (undefined)
            ? "body"
            : appendTo;

        $(append).append(response.Data);

        App.Grid.Build.Footer(response.Count, 0, id);

        $("#" + id + " tfoot a").live("click", function (e) {
            e.preventDefault();
            var data = {
                page: $(this).attr("class").replace("page-", "")
            };

            $.getJSON(route.join("/"), data, function (html) {
                // add the data to the table    
                $("#" + id).remove();
                $(append).append(html.Data);

                // re-add the footer
                $('#' + id + ' thead tfoot').remove();

                App.Grid.Build.Footer(html.Count, parseInt(data.page), id);
            });
        });
    });
};
App.Grid.Build.Footer = function (d, customPageIndex, id) {
    var pageCount = Math.ceil(d / 5),
        pageIndex = customPageIndex + 1,
        pageButtonCount = 3,
        row = $("<tr></tr>"),
        cell = $("<td></td>");

    var pager = $("<div></div>"),
            min, max;

    row.append(cell);

    if (pageCount > 1) {

        pager.attr("class", "pagination");

        cell.append(pager);

        min = pageIndex - pageButtonCount;
        max = pageIndex + pageButtonCount;

        if (max > pageCount) {
            min -= (max - pageCount);
        }
        else if (min < 1) {
            max += (1 - min);
        }

        if (pageIndex > 1) {
            pager.append(BuildLinkButton(0, "First"));
            page = BuildLinkButton(pageIndex - 2, "Previous")
        } else {
            page = BuildSpan("Previous", "disabled");
        }

        pager.append(page);

        var needDiv = false;
        for (i = 1; i <= pageCount; i++) {
            if (i <= 2 || i > pageCount - 2 || (min <= i && i <= max)) {
                var text = i;
                page = i == pageIndex
                        ? BuildSpan(text, "current")
                        : BuildLinkButton(i - 1, text);

                pager.append(page);
                needDiv = true;
            }
            else if (needDiv) {
                page = BuildSpan("&hellip;", null);
                pager.append(page);
                needDiv = false;
            }
        };

        if (pageIndex < pageCount) {
            console.log("PageIndex: " + pageIndex);
            page = BuildLinkButton(pageIndex, "Next");
            pager.append(page);

            pager.append(BuildLinkButton(pageCount - 1, "Last"));
        } else {
            page = BuildSpan("Next", "disabled");
            pager.append(page);
        }
    }

    var footer = $("<tfoot></tfoot>");
    var footerRow = $("<tr></tr>");

    footerRow.append(pager);
    footer.append(footerRow);

    $("#" + id + " thead").after(footer);

    return footer;
};

function BuildLinkButton(pageIndex, text)
{
    var pagerLink = $("<a></a>").attr("class", "page-" + pageIndex);
    pagerLink.append(text);

    return pagerLink;
};

function BuildSpan(text, cssClass)
{
    var span = $("<span></span>");
    span.attr("class", cssClass);
    span.html(text);

    return span;
}


Index.cshtml:

<script type="text/javascript" src="@Url.Content("~/Scripts/app.grids.js")"></script>

@{
    ViewBag.Title = "Feeds";
}

<style>
   div.pagination {
      padding: 3px;
      margin: 3px;
   }
   div.pagination a {
      padding: 2px 5px 2px 5px;
      margin: 2px;
      border: 1px solid #AAAADD;
      text-decoration: none; /* no underline */
      color: #000099;
      cursor: pointer;
   }
   div.pagination a:hover, div.pagination a:active {
      border: 1px solid #000099;
      color: #000;
   }
   div.pagination span.current {
      padding: 2px 5px 2px 5px;
      margin: 2px;
      border: 1px solid #000099;
      font-weight: bold;
      background-color: #000099;
      color: #FFF;
   }
   div.pagination span.disabled {
      padding: 2px 5px 2px 5px;
      margin: 2px;
      border: 1px solid #EEE;
      color: #DDD;
   }
</style>

<script type="text/javascript" language="javascript">
    $(function () {
        //ID for the table, append the table to wich element, Array of route values [Controller, Action]
        App.Grid.Build("DataTable", "body", ["Feeds", "Grid"]);
    });    
</script>

Controller:

[HttpGet]
        public JsonResult Grid(int? page)
        {
            int skip = page.HasValue ? page.Value : 0;
            var data = _mostPopular.Skip(skip * 5).Take(5).ToList();
            var grid = new WebGrid();

            var htmlString = grid.GetHtml(tableStyle: "webGrid",
                                          headerStyle: "header",
                                          alternatingRowStyle: "alt",
                                          htmlAttributes: new { id = "DataTable" });

            return Json(new
            {
                Data = htmlString.ToHtmlString(),
                Count = _mostPopular()
            }, JsonRequestBehavior.AllowGet);
        }
Comment posted by Micha Demmers on Thursday, June 9, 2011 2:39 AM
In stead of the controller with action EfficientPaging i used the controller Feeds with jsonresult Grid.
You can just modify these values in the array to match your controller and action. The id of the table in the App.Grid.Build constructor needs to be the same as the id of the table in the controller.
In this way you can have more than 1 table with paging on a page.
Comment posted by Guffa on Friday, June 10, 2011 9:08 PM
Thanks for sharing this tip Micha. Can I use your javascript in my project?
Comment posted by Malcolm Sheridan on Thursday, July 14, 2011 10:28 PM
@Micha

The example I did fetches all of the paged data via jQuery and JSON.  
Comment posted by Micha Demmers on Thursday, August 4, 2011 6:25 AM
@Guffa

offcourse you can!
Comment posted by LambdaCruiser on Thursday, August 11, 2011 11:53 PM
Doesn't this break the sorting functionality of WebGrid? I mean - if it only gets one page of data now then it can only sort within that one page. What should I do if I want the sorting to work on all of the data, before paging?
Comment posted by Stuart Leeks on Monday, August 15, 2011 12:43 PM
@LambdaCruiser - if you want to do server-side paging and have the WebGrid handle the paging links for you then check out my MSDN Magazine article "Get the Most out of WebGrid in ASP.NET MVC": http://msdn.microsoft.com/en-us/magazine/hh288075.aspx

- Stuart
Comment posted by Andrew Wrigley on Thursday, September 8, 2011 8:54 AM
Not exactly separation of concerns, what with the web grid defined in the controller.
Comment posted by Andrew Wrigley on Thursday, September 8, 2011 10:48 AM
I think you ought to update this article, as a lot of what you are doing is way, way too complex, and doesn't take account of Separation of Concerns, nor does it use the functionality built into WebGrid.  You should see the article in MSDN magazine:

http://msdn.microsoft.com/en-us/magazine/hh288075.aspx
Comment posted by a1771550 on Thursday, September 8, 2011 11:47 AM
Sorry, but I found the Grid created by "EfficientWay" came with no footer and unsortable headers at all, but displaying 5 rows there. Any advice would be greatly appreciated. Many thanks in advance.
Comment posted by Ankit on Wednesday, October 12, 2011 6:20 AM
Re:  WebGrid Web Helper - ASP MVC 3
this is great article, thanks for sharing with us. It's help me lot and also this link....   http://www.mindstick.com/Articles/25289a23-a3df-4863-9c0c-a4ec4dae1477/?WebGrid%20control%20in%20ASP.NET%20MVC helped me in completing my projet.

Thanks!!!!
Comment posted by Malcolm Sheridan on Thursday, October 20, 2011 7:58 AM
@Andrew

That MSDN article is different from this.  Out of the box the WebGrid does paging without any optimization.  Hopefully that will be fixed in another version, but I doubt it.

@Ankit

Glad you liked the article.
Comment posted by Malcolm Sheridan on Thursday, October 20, 2011 7:59 AM
@LambdaCruiser

Imagine you had 10000 records.  Would you really want to return all of the records first, then sort them?
Comment posted by red77star on Friday, May 18, 2012 2:10 PM
MVC is pile of crap.
Comment posted by Sandeep on Tuesday, July 17, 2012 7:48 PM
Please provide code to have scroll bar(vertical) and stop the header to to grid
Comment posted by Deva on Thursday, July 19, 2012 10:45 AM
Can i include dropdown selected value as for selecting rows per page property value
Comment posted by Deva on Thursday, July 19, 2012 10:47 AM
If we can include dropdown selected value for rows per page value then give me a sample code for it please ..,
Comment posted by Deva on Thursday, July 19, 2012 11:54 AM
If we can include dropdown selected value for rows per page value then give me a sample code for it please ..,
Comment posted by Yasser Shaikh on Thursday, July 26, 2012 9:31 AM
Hi Malcolm.. its been close to 2 years since you posted this article, has any better method come up for this ?
Comment posted by Deepak on Sunday, August 19, 2012 7:35 AM
Nice article. I have one question

How to show custom header names? I mean "First Name"
  grid.Column("First Name") -> if I give space does it work?

Comment posted by Serge on Thursday, December 6, 2012 7:55 PM
Deepak, the second parameter to grid.Column() is the display name. So you should use grid.Column("first_name_or_whatever_field_name_you_have", "First Name")
Comment posted by Nikunj on Friday, December 7, 2012 3:05 AM
Good one.
Comment posted by Syam .S on Sunday, December 23, 2012 8:11 AM
Great article .

Please tell me how can we format a column . i tried a lot and cannot find any solution ..Is any body find it please let me know ..
Comment posted by Brit on Thursday, January 17, 2013 5:01 AM
Nice Work..
Comment posted by Hari on Thursday, May 16, 2013 11:46 PM
How can I format a column?
Comment posted by JWaters on Thursday, June 6, 2013 9:46 AM
The zip file doesn't include a view with the JSON javascript code??
Comment posted by Naveen on Monday, August 26, 2013 5:40 AM
wanna bing data from database to WebGrid using razor(cs html).. Just now i started to work on MVC.. Am a beginner help me..

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel