Using the WebGrid in MVC correctly

I know the WebGrid is a basic Grid control, but sometimes you are constrained with what you can use with a customer and WebGrid is by default from Microsoft so it is safe to use and does not require any unknown third party tools to be installed.

By default the WebGrid requires all the data to be loaded in order for paging to work.  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!  This is the reason why I’m writing this blog post to over come this issue.

Throughout this example I’ve tried to keep to using interfaces where ever possible as I always think these are easier to understand and keep the application more flexible.

The WebGrid supports dynamic typing, while dynamic typing is probably a good fit for WebMatrix, there are benefits to strongly typed views. One way to achieve this is to create a derived type WebGrid<T>, here it is:

using System;
using System.Collections.Generic;
using System.Web.Helpers;

public class WebGrid<T> : WebGrid
{
    public WebGrid(IEnumerable<T> source = null, IEnumerable<string> columnNames = null, string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true, string ajaxUpdateContainerId = null, string ajaxUpdateCallback = null, string fieldNamePrefix = null, string pageFieldName = null,
    string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
        : base(source.SafeCast<object>(), columnNames, defaultSort, rowsPerPage, canPage, canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix, pageFieldName,
            selectionFieldName, sortFieldName, sortDirectionFieldName)
    {
    }
    public WebGridColumn _Column(string columnName = null, string header = null, Func<T, object> format = null, string style = null, bool canSort = true)
    {
        Func<object, object> wrappedFormat = null;
        if (format != null)
        {
            wrappedFormat = o => format((T)o);
        }
        var _scolumn = base.Column(columnName, header, wrappedFormat, style, canSort);
        return _scolumn;
    }
    public WebGrid<T> _Bind(IEnumerable<T> source, IEnumerable<string> columnNames = null, bool autoSortAndPage = true, int rowCount = -1)
    {
        base.Bind(source.SafeCast<object>(), columnNames, autoSortAndPage, rowCount);
        return this;
    }
}

And to extend the exisiting WebGrid we require a static extension:

using System.Web.Mvc;
using System.Collections.Generic;

public static class WebGridExtensions
{
    public static WebGrid<T> Grid<T>(this HtmlHelper htmlHelper, IEnumerable<T> source, IEnumerable<string> columnNames = null, string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true, string ajaxUpdateContainerId = null, string ajaxUpdateCallback = null, string fieldNamePrefix = null,
    string pageFieldName = null, string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
    {
        return new WebGrid<T>(source, columnNames, defaultSort, rowsPerPage, canPage, canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix, pageFieldName,
        selectionFieldName, sortFieldName, sortDirectionFieldName);
    }

    public static WebGrid<T> ServerPagedGrid<T>(this HtmlHelper htmlHelper, IEnumerable<T> source, int totalRows, IEnumerable<string> columnNames = null, string defaultSort = null, int rowsPerPage = 10, bool canPage = true, bool canSort = true, string ajaxUpdateContainerId = null, string ajaxUpdateCallback = null,
    string fieldNamePrefix = null, string pageFieldName = null, string selectionFieldName = null, string sortFieldName = null, string sortDirectionFieldName = null)
    {
        dynamic webGrid = new WebGrid<T>(null, columnNames, defaultSort, rowsPerPage, canPage, canSort, ajaxUpdateContainerId, ajaxUpdateCallback, fieldNamePrefix, pageFieldName,
        selectionFieldName, sortFieldName, sortDirectionFieldName);
        return webGrid.Bind(source, rowCount: totalRows, autoSortAndPage: false);
    }
}

One further method we will need is a SafeCast to extend the Enumerable

using System.Collections;
using System.Collections.Generic;
using System.Linq;

public static class EnumerableExtensions
{
    public static IEnumerable<TTarget> SafeCast<TTarget>(this IEnumerable source)
    {
        return source == null ? null : source.Cast<TTarget>();
    }
}

Okay so this is it for the infrastructure, now lets get down to using the new WebGrid, so first the domain objects or interfaces we want to display

using System;
    
public interface IDocument
{
    global::System.Guid Id { get; set; }
    DateTime? Timestamp { get; set; }
    global::System.Boolean Inactive { get; set; }
}

Now for the important worker, the Service interface:

using System.Collections.Generic;

public interface IDocumentService
{
    IEnumerable<IDocument> GetDocuments(out int totalRecords, int pageSize = -1, int pageIndex = -1, string sort = "Id", SortDirection sortOrder = SortDirection.Ascending);
}

and the impmentation looks something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public class EfDocumentService : IDocumentService
{
    private readonly IDictionary<string, Func<IQueryable<IDocument>, bool, IOrderedQueryable<IDocument>>>
        _documentOrderings
            = new Dictionary<string, Func<IQueryable<IDocument>, bool, IOrderedQueryable<IDocument>>>
                    {
                        {
                            "Id",
                            CreateOrderingFunc<IDocument, Guid>(p => p.Id)
                            },
                        {
                            "Inactive",
                            CreateOrderingFunc<IDocument, bool?>(p => p.Inactive )
                            },
                        {
                            "Timestamp",
                            CreateOrderingFunc<IDocument, DateTime?>(p => p.Timestamp )
                            }
                    };


    private static Func<IQueryable<T>, bool, IOrderedQueryable<T>> CreateOrderingFunc<T, TKey>(Expression<Func<T, TKey>> keySelector)
    {
        return (source, @ascending) => @ascending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector);
    }

    public IEnumerable<IDocument> GetDocuments(out int totalRecords, int pageSize, int pageIndex, string sort, SortDirection sortOrder)
    {
        using (var context = new EDM())
        {
            IQueryable<IDocument> documents = context.Documents;

            totalRecords = documents.Count();

            Func<IQueryable<IDocument>, bool, IOrderedQueryable<IDocument>> applyOrdering;
            _documentOrderings.TryGetValue(sort, out applyOrdering);

            documents = applyOrdering(documents, sortOrder == SortDirection.Ascending);

            if (pageSize > 0 && pageIndex >= 0)
            {
                documents = documents.Skip(pageIndex * pageSize).Take(pageSize);
            }

            return documents.ToList();
        }
    }
}

Now for the Controller to process the data:

using System;
    using System.Web.Mvc;
    using Domain;
    using Models;

    public class HomeController : Controller
    {
        private readonly IDocumentService _documentService;

        public HomeController()
        {
            _documentService = new EfDocumentService();
        }

        public ActionResult index(int page = 1, string sort = "Id", string sortDir = "Ascending")
        {
            const int pageSize = 5;
            int totalRecords;

            var documents = _documentService.GetDocuments(out totalRecords, pageSize: pageSize, pageIndex: page - 1, sort: sort, sortOrder: GetSortDirection(sortDir));
            
            var model = new PagedDocumentsModel
            {
                PageSize = pageSize,
                PageNumber = page,
                Documents = documents,
                TotalRows = totalRecords
            };

            return View(model);
        }

        private SortDirection GetSortDirection(string sortDirection)
        {
            if (sortDirection != null)
            {
                if (sortDirection.Equals("DESC", StringComparison.OrdinalIgnoreCase) || sortDirection.Equals("DESCENDING", StringComparison.OrdinalIgnoreCase))
                {
                    return SortDirection.Ascending;
                }
            }
            return SortDirection.Descending;
        }
    }

The model to display all the information to the screen:

using System.Collections.Generic;
using Domain;

public class PagedDocumentsModel
{
    public int PageSize { get; set; }
    public int PageNumber { get; set; }
    public IEnumerable<IDocument> Documents { get; set; }
    public int TotalRows { get; set; }
}

And finally the view to display the information:

@model WebGridPaging.Models.PagedDocumentsModel
@using WebGridPaging.Infrastructure
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <script src="../../Scripts/jquery-2.0.3.min.js" type="text/javascript"></script>
    <title>List</title>
</head> 
<body>
    <div id="grid">
        @{
            var grid = new WebGrid<WebGridPaging.Domain.IDocument>(null, rowsPerPage: Model.PageSize, defaultSort: "Id", ajaxUpdateContainerId:"grid");
            grid.Bind(Model.Documents, rowCount: Model.TotalRows, autoSortAndPage: false);
        }
        @grid.GetHtml()
    </div>
    <p>
	Model.Documents.Count() = @Model.Documents.Count()
</p>
<p>
	Model.TotalRows = @Model.TotalRows
</p>
<p>
	Time = @System.DateTime.Now
</p>
</body>
</html>

I’ve included a project with code samples, but please note this is talking to an external database with a table called Documents, so either generate a sample database with this table or point it to one you already have.

WebGridPaging.zip (3.37 mb)

Reference links

Get the Most out of WebGrid in ASP.NET MVC