Adding A Footer To The Razor WebGrid

Whether you are using the Razor WebGrid in an ASP.NET Web Pages site, or an MVC application, you may well want to display tabular data at some point in which case you are likely to turn to the Razor WebGrid helper. And if you want to do that, you might also want to add a footer to the rendered table. Here are some ways that you can accomplish that task.
Server Side

The vast majority of samples that feature the construction and display of a WebGrid follow the same pattern. The WebGrid is declared and has data passed into it, then the GetHtml() method is used to render the grid's HTML in the Razor page or MVC view. If paging is enabled, a tfoot element is added to the grid and the paging links are displayed there. However, if paging is disabled, neither the WebGrid constructor nor the GetHtml method expose any means by which a tfoot element can be added to the grid, or its content displayed. However, there is another way to generate HTML from a WebGrid instance, and that is via the much overlooked WebGrid.Table method. Here is its definition from MSDN:

public IHtmlString Table(
	string tableStyle,
	string headerStyle,
	string footerStyle,
	string rowStyle,
	string alternatingRowStyle,
	string selectedRowStyle,
	string caption,
	bool displayHeader,
	bool fillEmptyRows,
	string emptyRowCellValue,
	IEnumerable<WebGridColumn> columns,
	IEnumerable<string> exclusions,
	Func<Object, Object> footer,
	Object htmlAttributes
)

Notice the penultimate parameter? That allows you to create a footer which will be rendered as a tfoot element. It works in exactly the same way as the format parameter in the WebGrid Column constructor and expects a templated Razor delegate, allowing you to pass HTML in. The Web Pages framework will create a tfoot element if you pass anything into the footer argument. The generated tfoot will have a tr containing one td element with a colspan value equivalent to the number of columns in the WebGrid.Columns collection. The code generated as a result of the Razor template will be placed in that td element.

Here is a valid value for the footer parameter:

footer: @<text>Hello World!</text>

If you were to pass this in to a table that has five columns, the resulting generatedHTML for the footer would be:

<tfoot><tr><td colspan="5">Hello World!</td></tr></tfoot>

Here is the code block from a small Razor page that displays order details from the Northwind database:

@{
    Layout = "~/_Layout.cshtml";
    Page.Title = "WebGrid.Table method";

    var db = Database.Open("Northwind");
    var sql = "SELECT OrderId FROM Orders";
    var orders = db.Query(sql).Select(o => new SelectListItem {
        Value = o.OrderId.ToString(), 
        Text = o.OrderID.ToString(),
        Selected = o.OrderID.ToString() == Request["OrderID"]
    });

    WebGrid grid = null;
    var orderTotal = 0f;

    if(IsPost){
        sql = @"SELECT p.ProductName, o.UnitPrice, o.Quantity, 
                (o.UnitPrice * o.Quantity) - (o.UnitPrice * o.Quantity * o.Discount) As TotalCost 
                FROM OrderDetails o INNER JOIN Products p ON o.ProductID = p.ProductID 
                WHERE o.OrderID = @0";
        
        var orderDetails = db.Query(sql, Request["OrderID"]);
        orderTotal = orderDetails.Sum(o => (float)o.TotalCost);

        grid = new WebGrid(orderDetails, canPage: false, canSort: false);
    }
}

The first section of code obtains the order numbers from the database and transforms them into a collection of SelectListItems which is used to populate a DropDownList helper. A WebGrid variable is declared so that it is available at page scope. Then the code checks to see if the page has been posted back. If it has, details of the selected order are obtained from the database including the total cost of each item ordered. The total cost of all items is generated via the LINQ Sum operator. Then the WebGrid is instantiated.

Up until this point, the code will be familiar to most developers who are used to seeing WebGrid samples on various blog posts or in the official Web Pages tutorials. Here is the HTML section of the page:

<h1>@Page.Title</h1>
<form method="post">
    @Html.DropDownList("OrderID", orders)
    <input type="submit" />
</form>

@if(grid != null){
    @grid.Table(
            columns: grid.Columns(
                grid.Column("ProductName", "Product", style: "_220"),
                grid.Column("UnitPrice", "Price", style: "_60", format: @<text>@item.UnitPrice.ToString("c")</text>),
                grid.Column("Quantity", style: "_90"),
                grid.Column("TotalCost", "Total Cost", style: "_90", format: @<text>@item.TotalCost.ToString("c")</text>)
            ), 
            footer: @<table class="footer">
                         <tr>
                             <td class="_220">Total</td>
                             <td colspan="2" class="_150">&nbsp;</td>
                             <td class="_90">@orderTotal.ToString("c")</td>
                         </tr>
                    </table>);
}

At first glance, this will look familiar to most other examples that show a WebGrid being rendered to HTML. However, as mentioned earlier, the Table method is used instead of the GetHtml method to render the grid. Many of the parameters are the same except that the Table method exposes a footer parameter instead of the pager-related parameters of the GetHtml method. I have passed a table into the footer which allows for slightly easier alignment of footer values with their corresponding columns, although of course any HTML that can go into a td element is appropriate.

Client Side

The second way in which a footer can be added is by using client-side code. jQuery is the most popular library for DOM manipulation, so this example features its use. The server-side code is identical to the previous example so it will not be repeated, but here is the client-side code illustrating the use of jQuery to add a footer:

<h1>@Page.Title</h1>
<form method="post">
    @Html.DropDownList("OrderID", orders)
    <input type="submit" />
</form>

<div>
@if(grid != null){
    @grid.GetHtml(
        columns: grid.Columns(
            grid.Column("ProductName", "Product", style: "_220"),
            grid.Column("UnitPrice", "Price", style: "_60", format: @<text>@item.UnitPrice.ToString("c")</text>),
            grid.Column("Quantity", style: "_90"),
            grid.Column("TotalCost", "Total Cost", style: "_90", format: @<text>@item.TotalCost.ToString("c")</text>)
         )
     )
}
</div>

<script>
    $(function () {
        var tfoot = '<tfoot><tr><td class="_220">Total</td><td colspan="2" class="_150">&nbsp;</td>';
        tfoot += '<td class="_90">@orderTotal.ToString("c")</td></tr></tfoot>';
        $('table').append(tfoot);
    })

</script>

This time the GetHtml method has been used, although the Table method could just as easily have been used with nothing being passed to the footer parameter. A JavaScript script block contains code that instantiates a string representing the HTML for a tfoot element containing three cells; the first cell holds the text "Total", and the last one contains the orderTotal variable value. Then the string is appended to the table element.

This might appear to be a more "hacky" way to achieve the same effect as the first approach using the Table method. However, the resulting foot isn't wrapped in a table cell that has a preset colspan. It should be noted that the jQuery approach obviously relies on JavaScript being enabled in the browser.

Server Side Hack

Both the GetHtml and Table methods of the WebGrid return an IHtmlString. That means that if you convert it using ToString(), it is open to manipulation using standard string methods and/or Regex. The next method illustrates how to do this to add some additional HTML to the result of a call to GetHtml:

@using System.Text;
@{
    Layout = "~/_Layout.cshtml";
    Page.Title = "Hack method";

    var db = Database.Open("Northwind");
    var sql = "SELECT OrderId FROM Orders";
    var orders = db.Query(sql).Select(o => new SelectListItem {
        Value = o.OrderId.ToString(), 
        Text = o.OrderID.ToString(),
        Selected = o.OrderID.ToString() == Request["OrderID"]
    });

    WebGrid grid = null;
    var orderTotal = 0f;
    
    var tableHtml = string.Empty;
    
    if(IsPost){
        sql = @"SELECT p.ProductName, o.UnitPrice, o.Quantity, 
                (o.UnitPrice * o.Quantity) - (o.UnitPrice * o.Quantity * o.Discount) As TotalCost 
                FROM OrderDetails o INNER JOIN Products p ON o.ProductID = p.ProductID 
                WHERE o.OrderID = @0";
        
        var orderDetails = db.Query(sql, Request["OrderID"]);
        orderTotal = orderDetails.Sum(o => (float)o.TotalCost);
        
        grid = new WebGrid(orderDetails, canPage: false, canSort: false);
        
        var sb = new StringBuilder();

        sb.Append(grid.GetHtml(
            columns: grid.Columns(
                grid.Column("ProductName", "Product", style: "_220"),
                grid.Column("UnitPrice", "Price", style: "_60", format: @<text>@item.UnitPrice.ToString("c")</text>),
                grid.Column("Quantity", style: "_90"),
                grid.Column("TotalCost", "Total Cost", style: "_90", format: @<text>@item.TotalCost.ToString("c")</text>)
            )
        ).ToString().Replace("</table>", string.Empty));
        sb.Append("<tfoot><tr><td class=\"_220\">Total</td>");
        sb.Append("<td colspan=\"2\" class=\"_150\">&nbsp;</td>");
        sb.Append("<td class=\"_90\">");
        sb.Append(orderTotal.ToString("c"));
        sb.Append("</td></tr></tfoot></table>");
        tableHtml = sb.ToString();
    }
}

The first part of the code is similar to the previous examples except for the addition of a using statement referencing System.Text. A string variable - tableHtml - has been added. Then within the IsPost block, the code changes once the grid has been created. This time, a StringBuilder obejct is instantiated - hence the need to reference System.Text. StringBuilders are a more efficient way to build strings from multiple substrings than simple concatenation. The grid.GetHtml() method is called and the result is added to the StringBuilder. Notice that two additional methods have been chained on to the GetHtml call - ToString which converts the IHtmlString to a normal string, and Replace, which takes care of removing the closing </table> tag from the generated string.

Next, the <tfoot> tag and its contents are constructed and added to the StringBuilder. The closing </table> tag is restored so that the tfoot belongs to the actual table. Then the tableHtml variable is assigned the stringified value of the StringBuilder. Finally a means to render the tableHtml value is needed:

@if(!tableHtml.IsEmpty()){
    @Html.Raw(tableHtml)
}

If you render an IHtmlString to the page, it will be treated as HTML and output as such to the browser. All other strings are automatically HTML encoded by the Web Pages framework for security purposes. The IHtmlString generated by the grid's GetHtml method was converted to a standard string, so you need to use the Html.Raw helper to get Web Pages to treat the result in the same way as an IHtmlString, and you avoid getting angled brackets appearing all over your page.

I call this method a hack, and in truth, it is. But it allows you to control the rendered HTML for your footer without having to rely on JavaScript, which means the resulting footer is totally visible to search engines and other non-JavaScript enabled user agents if that is important to you. In this example, I have chopped off the closing </table> tag and injected a footer, but there's nothing to stop you injecting HTML for an additional row, for example, which could be used to add new records. If you do this, you will also need to remove the closing </tbody> element from the string and restore it at the end along with the lcosing </table> tag for valid HTML.

A Razor Web Pages site is available for download containing all the code for the 3 examples detailed here.

 

Date Posted: Monday, February 25, 2013 7:43 PM
Last Updated:
Posted by: Mikesdotnetting
Total Views to date: 42712

0 Comments

Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and some may not be published. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Not relevant to the article
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam
  • Anything in a language I don't understand including gibberish.

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.