ASP.NET MVC GridView

August 3rd, 2009 by erin

Update! – Check out Part 2 of the GridView series in which we detail out how to handle and maintain your GridView’s state with fancy Redirect URLs if you leave and return to the GridView page (or any page for that matter).

It’s been several months since BrightMix first took Microsoft’s new MVC framework for ASP.NET out for a test drive, and overall, it’s been a great learning experience.  We started out in Preview 3 of the framework, and thanks to the pre-release status of the framework, we were left largely to our own devices for figuring things out.  Especially when it came to common UI controls, like grids.

We new exactly what we needed in a grid control – our project was for an existing client, and we knew what was expected: paging, sorting, alternating row colors, and the ability to maintain state across requests.  Our first step was to get the basic grid to display, and we went with a basic html table approach, looping through the collection of entities to generate each row.

<table class="Grid" cellpadding="0" cellspacing="0">
    <thead class = "GridHeader">
        <tr>
            <th>Make</th>
            <th>Model</th>
            <th>Year</th>
            <th>Color</th>
            <th>Miles</th>
            <th>Doors</th>
            <th><span>Sunroof?</span></th>
        </tr>
    </thead>
    <% foreach (MvcPagingGrid.Models.Car c in Model.Cars)
       { %>
        <tr>
            <td><span><%= c.Make %></span></td>
            <td><span><%= c.Model %></span></td>
            <td><span><%= c.Year %></span></td>
            <td><span><%= c.Color %></span></td>
            <td><span><%= c.Miles %></span></td>
            <td><span><%= c.Doors %></span></td>
            <td><span><%= c.Sunroof ? "Yes" : "No" %></span></td>
        </tr>
    <% } %>
</table>

Once we had the basic grid lined up, we needed to tackle the sorting. Luckily, we had already written some code that would manage swapping out query string parameters for us. All we needed was a helper method to create the links in the grid headers.

 public static string SortLink(this HtmlHelper htmlHelper, GridSortingLinkProperties properties)
        {
            string url = GenerateSortUrl(properties);

            string currentSortExpression = QueryString.GetString(properties.SortExpressionKey);
            string currentSortDirection = QueryString.GetString(properties.SortDirectionKey);
            string arrow = "";

            if (properties.SortExpression == currentSortExpression)
            {
                if (currentSortDirection == "ASC")
                    arrow = "asc";
                else
                    arrow = "desc";
            }

            TagBuilder tag = new TagBuilder("a")
            {
                InnerHtml = HttpUtility.HtmlEncode(properties.Text)
            };
            tag.MergeAttributes(new Dictionary<string, string> { { "href", url }, { "class", arrow } });

            return tag.ToString();
        }

We then modified our view to take advantage of our new helper. Our gridview, with sorting, looks something like this:

<table class="Grid" cellpadding="0" cellspacing="0">
    <thead class = "GridHeader">
        <tr>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = true, Text="Make", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Make", SortExpressionKey="SortExpr" })%></th>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = false, Text="Model", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Model", SortExpressionKey="SortExpr" })%></th>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = false, Text="Year", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Year", SortExpressionKey="SortExpr" })%></th>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = false, Text="Color", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Color", SortExpressionKey="SortExpr" })%></th>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = false, Text="Miles", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Miles", SortExpressionKey="SortExpr" })%></th>
            <th><%=Html.SortLink(new GridSortingLinkProperties { IsDefaultSortColumn = false, Text="Doors", LinkSortDirection=SortDirection.Ascending,
                SortDirectionKey="SortDir", SortExpression="Doors", SortExpressionKey="SortExpr" })%></th>
            <th><span>Sunroof?</span></th>
        </tr>
    </thead>
    <% foreach (MvcPagingGrid.Models.Car c in Model.Cars)
       { %>
        <tr>
            <td><span><%= c.Make %></span></td>
            <td><span><%= c.Model %></span></td>
            <td><span><%= c.Year %></span></td>
            <td><span><%= c.Color %></span></td>
            <td><span><%= c.Miles %></span></td>
            <td><span><%= c.Doors %></span></td>
            <td><span><%= c.Sunroof ? "Yes" : "No" %></span></td>
        </tr>
    <% } %>
</table>

With sorting in place, we needed paging. Again, we were able to leverage some of our existing code to get our query string based paging control working with ASP.NET MVC.  The paging functionality is it’s own control, and can be applied to much more than grids. All it needs to know is the size of the number of records, the size of the page, the current page index, and where to find more data.

private static string GenerateLinks(GridPagerProperties properties)
        {
            //StringBuilder pagerString = new StringBuilder();

            TagBuilder pagerDiv = new TagBuilder("div") { };
            pagerDiv.MergeAttributes(new Dictionary<string, string> { { "class", "pager" } });

            if (properties.PageCount > 1)
            {
                //create the previous tag
                if (properties.CurrentPageIndex > 0)
                {
                    string prevUrl = GeneratePostBackUrl(properties.CurrentPageIndex - 1, properties.PageKey);
                    TagBuilder prevTag = new TagBuilder("a")
                    {
                        InnerHtml = "<span>" + HttpUtility.HtmlEncode("<") + "</span>"
                    };
                    prevTag.MergeAttributes(new Dictionary<string, string> { { "href", prevUrl } });

                    pagerDiv.InnerHtml += prevTag.ToString(); ;
                    //pagerString.Append(prevTag.ToString());
                }

                for (int page = 0; page < properties.PageCount; page++)
                {
                    string url = GeneratePostBackUrl(page, properties.PageKey);
                    TagBuilder tag = new TagBuilder("a")
                    {
                        InnerHtml = "<span>" + HttpUtility.HtmlEncode((page + 1).ToString()) + "</span>"
                    };
                    tag.MergeAttributes(new Dictionary<string, string> { { "href", url } });

                    if (HttpContext.Current.Request.Url.PathAndQuery == url)
                        tag.Attributes.Add("class", "selected");
                    else if ((HttpContext.Current.Request.QueryString.Count == 0 || QueryString.GetInt(properties.PageKey) == 0) && page == 0)
                        tag.Attributes.Add("class", "selected");

                    TagBuilder label = new TagBuilder("label")
                    {
                        InnerHtml = HttpUtility.HtmlEncode("...")
                    };
                    label.MergeAttributes(new Dictionary<string, string> { { "text", "..." } });

                    if (properties.PageCount > 11)
                    {
                        if (properties.CurrentPageIndex < 7)
                        {
                            //beginning
                            if (page < 9 || ((properties.PageCount - page) < 3))
                                pagerDiv.InnerHtml += tag.ToString();
                            else if (page == 10)
                                pagerDiv.InnerHtml += label.ToString();
                        }
                        else if (properties.CurrentPageIndex >= (properties.PageCount - 7))
                        {
                            //end
                            if (page < 2 || (page >= properties.PageCount - 9))
                                pagerDiv.InnerHtml += tag.ToString();
                            else if (page == 2)
                                pagerDiv.InnerHtml += label.ToString();
                        }
                        else
                        {
                            //middle
                            if (page < 2)
                                pagerDiv.InnerHtml += tag.ToString();
                            else if (page == 2)
                                pagerDiv.InnerHtml += label.ToString();
                            else if ((page >= (properties.CurrentPageIndex - 4)) && (page <= (properties.CurrentPageIndex + 4)))
                                pagerDiv.InnerHtml += tag.ToString();
                            else if (page == properties.CurrentPageIndex + 5)
                                pagerDiv.InnerHtml += label.ToString();
                            else if (page >= properties.PageCount - 2)
                                pagerDiv.InnerHtml += tag.ToString();
                        }
                    }
                    else
                        pagerDiv.InnerHtml += tag.ToString();
                }

                //create the next tag
                if (properties.CurrentPageIndex < properties.PageCount - 1)
                {
                    string nextUrl = GeneratePostBackUrl(properties.CurrentPageIndex + 1, properties.PageKey);
                    TagBuilder nextTag = new TagBuilder("a")
                    {
                        InnerHtml = "<span>" + HttpUtility.HtmlEncode(">") + "</span>"
                    };
                    nextTag.MergeAttributes(new Dictionary<string, string> { { "href", nextUrl } });

                    pagerDiv.InnerHtml += nextTag.ToString();
                }
            }

            // create the total items tag
            TagBuilder countDiv = new TagBuilder("div")
            {
                InnerHtml = String.Format("{0} items", properties.RecordCount)
            };
            countDiv.MergeAttributes(new Dictionary<string, string> { { "class", "totalrecords" } });

            pagerDiv.InnerHtml += countDiv.ToString();

            TagBuilder clearDiv = new TagBuilder("div") { };
            clearDiv.MergeAttributes(new Dictionary<string, string> { { "class", "clear" } });

            pagerDiv.InnerHtml += clearDiv.ToString();

            return pagerDiv.ToString();
        }

At this point, we had a workable grid in place.  All we needed was a little style, and the alternating grid row colors.  For the alternating rows, we let jQuery do the work.

 <script type="text/javascript">
        $(document).ready(function() {
            $('table.Grid tbody tr:odd').addClass('Row');
            $('table.Grid tbody tr:even').addClass('AlternatingRow');
            $('table.Grid tbody tr').bind('mouseover', function() { $(this).addClass('HighlightRow'); });
            $('table.Grid tbody tr').bind('mouseout', function() { $(this).removeClass('HighlightRow'); });
        });MvcPagingGrid

    </script>

That should be enough to get everyone started. In my next post, I’ll add the ability to maintain state across requests.

Click here to Download the Code