Chapter 6 – Understanding HTML Helpers

This is a rough draft of a chapter from the book ASP.NET MVC Framework Unleashed by Stephen Walther. Comments are welcome and appreciated. When the book is published, the text from this blog entry will be removed and only the code listings will remain.
Order this Book from Amazon

You use HTML helpers in a view to render HTML content. An HTML helper, in most cases, is just a method that returns a string.

You can build an entire ASP.NET MVC application without using a single HTML helper. However, HTML helpers make your life as a developer easier. By taking advantage of helpers, you can build your views with far less work.

In this chapter, you learn about the standard HTML helpers included with the ASP.NET MVC framework. You learn how to use the standard helpers to render HTML links and HTML form elements.

You also learn how to create custom helpers. We discuss the utility classes included in the ASP.NET framework that make it easier to build custom helpers. You learn how to work with the TagBuilder and the HtmlTextWriter classes.

Next, we tackle building a more complicated HTML helper: we build a DataGrid helper. The DataGrid helper enables you to easily display database records in an HTML table. It also supports paging and sorting.

Finally, we end this chapter with a discussion of how you can build unit tests for your custom HTML helpers.

*** Begin Note ***

In the ASP.NET MVC world, HTML helpers are the equivalent of ASP.NET Web Form controls. Like a web form control, an HTML helper enables you to encapsulate the rendering of HTML. However, unlike a Web Form control, HTML helpers are extremely lightweight. For example, an HTML helper does not have an event model and does not use view state.

*** End Note ***

Using the Standard HTML Helpers

The ASP.NET MVC framework includes a standard set of helpers that you can use to render the most common types of HTML elements. For example, you can use the standard set of helpers to render HTML links and HTML textboxes.

Rendering Links

The easiest way to render an HTML link in a view is to use the HTML.ActionLink() helper. The Html.ActionLink() does not link to a view. Instead, you use the Html.ActionLink() helper to create a link to a controller action.

For example, the view in Listing 1 includes a link to an action named About (see Figure 1).

Figure 1 – Link rendered by Html.ActionLink() helper

clip_image002

Listing 1 – ViewsHomeAbout.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <p>
        To learn more about this website, click the following link:
        <%= Html.ActionLink("About this Website", "About" ) %>
    </p>
</asp:Content>

In Listing 1, the first parameter passed to the Html.ActionLink() represents the link text and the second parameter represents the name of the controller action. This Html.ActionLink() helper renders the following HTML:

<a href=”/Home/About”>About this Website</a>

The Html.ActionLink() helper has several overloads and supports several parameters:

· linkText – The label for the link.

· actionName – The action that is the target of the link.

· routeValues – The set of values passed to the action.

· controllerName – The controller that is the target of the link.

· htmlAttributes – The set of HTML attributes to add to the link.

· protocol – The protocol for the link (for example, https)

· hostname – The host name for the link (for example, www.MyWebsite.com)

· fragment – The fragment (anchor target) for the link. For example, to link to a div in a view with an id of news, you would specify news for the fragment.

Notice that you can pass route values from an Html.ActionLink() to a controller action. For example, you might need to pass the Id of a database record that you want to edit. Here’s how you pass an Id parameter to the Edit() action:

[C#]

<%= Html.ActionLink(“Edit Record”, “Edit”, new {Id=3})

[VB]

<%= Html.ActionLink(“Edit Record”, “Edit”, New With {.Id=3})%>

When this Html.ActionLink() is rendered to the browser, the following link is created:

<a href=”/Home/Edit/3″>Edit Record</a>

*** Begin Note ***

Route values are URL encoded automatically. For example, the string “Hello World!” is encoded to “Hello%20World!”.

*** End Note ***

Rendering Image Links

Unfortunately, you can’t use the Html.ActionLink() helper to render an image link. Because the Html.ActionLink() helper HTML encodes its link text automatically, you cannot pass an <img> tag to this method and expect the tag to render as an image.

Instead, you need to use the Url.Action() helper to generate the proper link. Here’s how you can generate a delete link with an image:

<a href=”<%= Url.Action(“Delete”) %>”><img src=”../../Content/Delete.png” alt=”Delete” style=”border:0px” /></a>

The Url.Action() helper supports a set of parameters that are very similar to those supported by the Html.ActionLink() helper.

Rendering Form Elements

There are several HTML helpers that you can use to render HTML form elements:

· BeginForm()

· CheckBox()

· DropDownList()

· EndForm()

· Hidden()

· ListBox()

· Password()

· RadioButton()

· TextArea()

· TextBox()

The view in Listing 2 illustrates how you can use several of these HTML helpers. The view renders an HTML page with a simple user registration form (see Figure 2).

Listing 2 – ViewsCustomerRegister.aspx [C#]

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Customer>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>

    <% using (Html.BeginForm()) {%>

        <fieldset>
            <legend>Register</legend>
            <p>
                <label for="FirstName">First Name:</label>
                <%= Html.TextBox("FirstName") %>
                <%= Html.ValidationMessage("FirstName", "*") %>
            </p>
            <p>
                <label for="LastName">Last Name:</label>
                <%= Html.TextBox("LastName") %>
                <%= Html.ValidationMessage("LastName", "*") %>
            </p>
            <p>
                <label for="Password">Password:</label>
                <%= Html.Password("Password") %>
                <%= Html.ValidationMessage("Password", "*") %>
            </p>
            <p>
                <label for="Password">Confirm Password:</label>
                <%= Html.Password("ConfirmPassword") %>
                <%= Html.ValidationMessage("ConfirmPassword", "*") %>
            </p>
            <p>
                <label for="Profile">Profile:</label>
                <%= Html.TextArea("Profile", new {cols=60, rows=10})%>
            </p>
            <p>
                <%= Html.CheckBox("ReceiveNewsletter") %>
                <label for="ReceiveNewsletter" style="display:inline">Receive Newsletter?</label>
            </p>
            <p>
                <input type="submit" value="Register" />
            </p>
        </fieldset>

    <% } %>

</asp:Content>

Listing 2 – ViewsCustomerRegister.aspx [VB]

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of MvcApplication1.MvcApplication1.Models.Customer)" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>

    <% Using Html.BeginForm() %>

        <fieldset>
            <legend>Register</legend>
            <p>
                <label for="FirstName">First Name:</label>
                <%= Html.TextBox("FirstName") %>
                <%= Html.ValidationMessage("FirstName", "*") %>
            </p>
            <p>
                <label for="LastName">Last Name:</label>
                <%= Html.TextBox("LastName") %>
                <%= Html.ValidationMessage("LastName", "*") %>
            </p>
            <p>
                <label for="Password">Password:</label>
                <%= Html.Password("Password") %>
                <%= Html.ValidationMessage("Password", "*") %>
            </p>
            <p>
                <label for="Password">Confirm Password:</label>
                <%= Html.Password("ConfirmPassword") %>
                <%= Html.ValidationMessage("ConfirmPassword", "*") %>
            </p>
            <p>
                <label for="Profile">Profile:</label>
                <%= Html.TextArea("Profile", new With {.cols=60, .rows=10})%>
            </p>
            <p>
                <%= Html.CheckBox("ReceiveNewsletter") %>
                <label for="ReceiveNewsletter" style="display:inline">Receive Newsletter?</label>
            </p>
            <p>
                <input type="submit" value="Register" />
            </p>
        </fieldset>

    <% End Using %>

</asp:Content>

It is worth emphasizing, once again, that these form helpers are simply rendering strings. For example, the Html.TextBox() helper renders a string that includes an “<input>” tag. If you prefer, you could create the view in Listing 2 without using any of these helpers.

Figure 2 – The Register page

clip_image004

*** Begin Note ***

You might have noticed that Listing 2 includes validation helpers. We discuss the validation helpers — Html.ValidationMessage() and Html.ValidationSummary — in Chapter 8, Validating Form Data.

*** End Note ***

Rendering a Form

In Listing 2, the opening and closing <form> tags are created with a using statement. The opening and closing tags are created like this:

[C#]

<% using (Html.BeginForm()) {%>

… Form Contents …

<% } %>

[VB]

<% Using Html.BeginForm() %>

… Form Contents …

<% End Using %>

The advantage of opening and closing a <form> tag with a using statement is that you won’t accidently forget to close the <form> tag.

However, if you find this syntax confusing or otherwise disagreeable then you don’t need to use a using statement. Instead, you can open the <form> tag with Html.BeginForm() and close the <form> with HTml.EndForm() like this:

[C#]

<% Html.BeginForm(); %>

… Form Contents …

<% Html.EndForm(); %>

[VB]

<% Html.BeginForm() %>

… Form Contents …

<% Html.EndForm() %>

By default, the Html.BeginForm() method renders a form that posts back to the same controller action. In other words, if you retrieved the view by invoking the Customer controller Details() action, then the Html.BeginForm() renders a <form> tag that looks like:

<form action="/Customer/Details" method="post">
</form>

If you want to post to another action, or modify any other property of the <form> tag, then you can call the Html.BeginForm() helper with one or more parameters. The Html.BeginForm() helper accepts the following parameters:

· routeValues — The set of values passed to the action.

· actionName – The action that is the target of the form post.

· controllerName – The controller that is the target of the form post.

· method – The HTTP method of the form post. The possible values are restricted to POST and GET (You can’t use other HTTP methods within HTML, you must use JavaScript).

· htmlAttributes – The set of HTML attributes to add to the form.

Rendering a DropDownList

You can use the Html.DropDownList() helper to render a set of database records in an HTML <select> tag. You represent the set of database records with the SelectList class.

For example, the Index() action in Listing 3 creates an instance of the SelectList class that represents all of the customers from the Customers database table. You can pass the following parameters to the constructor for a SelectList when creating a new SelectList:

· items – The items represented by the SelectList.

· dataValueField – The name of the property to associate with each item in the SelectList as the value of the item.

· dataTextField – The name of the property to display for each item in the SelectList as the label of the item.

· selectedValue – The item to select in the SelectList.

In Listing 3, the SelectList is assigned to ViewData with the name CustomerId.

Listing 3 – ControllersCustomerController.cs [C#]

public ActionResult Index()
{
    ViewData["CustomerId"] = new SelectList(_entities.CustomerSet.ToList(), "Id", "LastName");
    return View();
}

Listing 3 – ControllersCustomerController.vb [VB]

Function Index() As ActionResult
    ViewData("CustomerId") = New SelectList(_entities.CustomerSet.ToList(), "Id", "LastName")
    Return View()
End Function

In a view, you can display a SelectList by calling the Html.DropDownList() helper with the name of the SelectList. The view in Listing 4 displays the CustomerId SelectList (see Figure 3).

Listing 4 – ViewsCustomerIndex.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <p>
    <%= Html.DropDownList("CustomerId") %>
    </p>

</asp:Content>

Figure 3 – Displaying the CustomerList

clip_image006

The view in Listing 4 renders the following HTML <select> tag:

<select id="CustomerId" name="CustomerId">
  <option value="1">Walther</option>
  <option value="2">Henderson</option>
  <option value="3">Smith</option>
</select

The Html.DropDownList() helper also supports an optionLabel parameter. You can use this parameter to create a default option at the top of the dropdown list (see Figure 4). You add an optionLabel like this:

<%= Html.DropDownList(“CustomerId”, “Select a Customer”) %>

Figure 4 – Displaying an option label

clip_image008

If a user submits a form with the option label selected, then an empty string is submitted to the server.

Encoding HTML Content

You should always HTML encode user submitted content. Otherwise, an evil hacker can initiate a JavaScript injection attack and, potentially, steal sensitive information from your users such as passwords and credit card numbers.

In a JavaScript injection attack, a hacker submits a JavaScript script when completing an HTML form. When the value of the form field is redisplayed, the script steals information from the page and sends the information to the hacker.

For example, because users typically select their own user names, you should always HTML encode user names that you display in a view like this:

<%= Html.Encode(UserName) %>

HTML encoding replaces characters with special meaning in an HTML document with safe characters:

· < renders &lt;

· > renders &gt;

· “ renders &quot;

· & renders &amp;

Imagine that a hacker submits the following script as the value of a form field:

<script>alert(‘Boom!’)</script>

When this script is HTML encoded, the script no longer executes. The script gets encoded into the harmless string:

&lt;script&gt;alert(‘Boom!’)&lt;/script&gt;

*** Begin Note ***

The ASP.NET MVC framework prevents a hacker from submitting a form that contains suspicious characters automatically through a feature called request validation. We discussed request validation in Chapter 4, Understanding Views.

*** End Note ***

Using Anti-Forgery Tokens

There is a particular type of JavaScript injection attack that is called a Cross-Site Request Forgery (CSRF) attack. In a CSRF attack, a hacker takes advantage of the fact that you are logged into one website to steal or modify your information at another website.

*** Begin Note ***

To learn more about CSRF attacks, see the Wikipedia entry at http://en.wikipedia.org/wiki/Csrf. The example discussed in this section is based on the example described in the Wikipedia entry.

*** End Note ***

For example, imagine that you have an online bank account. The bank website identifies and authenticates you with a cookie. Now, imagine that you visit a forums website. This forums website enables users to post messages that contain images. An evil hacker has posted an image to the forums that looks like this:

<img src=”http://www.BigBank.com/withdraw?amount=9999” />

Notice that the src attribute of this image tag points to a URL at the bank website.

When you view this message in your browser, $9,999 dollars is withdrawn from your bank account. The hacker is able to withdraw money from your bank account because the bank website uses a browser cookie to identify you. The hacker has hijacked your browser.

If you are creating the bank website, then you can prevent a CSRF attack by using the Html.AntiForgeryToken() helper. For example, the view in Listing 5 uses the Html.AntiForgeryToken() helper.

Listing 5 – ViewsBankWithdraw.aspx [C#]

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Withdraw</h2>

    <% using (Html.BeginForm()) {%>

        <%= Html.AntiForgeryToken() %>

        <fieldset>
            <legend>Fields</legend>
            <p>
                <label for="Amount">Amount:</label>
                <%= Html.TextBox("Amount") %>
            </p>
            <p>
                <input type="submit" value="Withdraw" />
            </p>
        </fieldset>

    <% } %>

</asp:Content>

Listing 5 – ViewsBankWithdraw.aspx [VB]

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Withdraw</h2>

    <% Using Html.BeginForm() %>

        <%= Html.AntiForgeryToken() %>

        <fieldset>
            <legend>Fields</legend>
            <p>
                <label for="Amount">Amount:</label>
                <%= Html.TextBox("Amount") %>
            </p>
            <p>
                <input type="submit" value="Withdraw" />
            </p>
        </fieldset>

    <% End Using %>

</asp:Content>

This helper creates a hidden input field that represents a cryptographically strong random value. Each time you request the view, you get a different random value in a hidden field that looks like:

<input 
  name="__RequestVerificationToken" 
  type="hidden"
   value="6tbg3PWU9oAD3bhw6jZwxrYRyWPhKede87K/PFgaw
     6MI3huvHgpjlCcPzDzrTkn8" />

The helper also creates a cookie that represents the random value. The value in the cookie is compared against the value in the hidden form field to determine whether a CSRF attack is being performed.

The Html.AntiForgeryToken() helper accepts the following optional parameters:

· salt – Enables you to add a cryptographic salt to the random value to increase the security of the anti-forgery token..

· domain – The domain associated with the anti-forgery cookie. The cookie is sent only when requests originate from this domain.

· path – The virtual path associated with the anti-forgery cookie. The cookie is sent only when requests originate from this path.

Generating the random value with the Html.AntiForgeryToken() helper is only half the story. To prevent CSRF attacks, you also must add a special attribute to the controller action that accepts the HTML form post. The Withdraw() controller action in Listing 6 is decorated with a ValidateAntiForgeryToken attribute.

Listing 6 – ControllersBankController.cs [C#]

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class BankController : Controller
    {
        //
        // GET: /Bank/Withdraw

        public ActionResult Withdraw()
        {
            return View();
        }

        //
        // POST: /Bank/Withdraw
        [AcceptVerbs(HttpVerbs.Post)]
        [ValidateAntiForgeryToken]
        public ActionResult Withdraw(decimal amount)
        {
            // Perform withdrawal
            return View();
        }

    }
}

Listing 6 – ControllersBankController.vb [VB]

Public Class BankController
    Inherits Controller
    '
    ' GET: /Bank/Withdraw

    Public Function Withdraw() As ActionResult
        Return View()
    End Function

    '
    ' POST: /Bank/Withdraw
    <AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken> _
    Public Function Withdraw(ByVal amount As Decimal) As ActionResult
        ' Perform withdrawal
        Return View()
    End Function

End Class

The ValidateAntiForgeryToken attribute compares the hidden form field to the cookie. If they don’t match then the attribute throws the exception in Figure 5.

Figure 5 – AntiForgery validation failure

clip_image010

*** Begin Warning ***

Visitors to your website must have cookies enabled or they will get an AntiForgery exception when posting to a controller action that is decorated with the ValidateAntiForgeryToken attribute.

*** End Warning ***

Creating Custom HTML Helpers

The ASP.NET MVC framework ships with a limited number of HTML helpers. The members of the ASP.NET MVC team identified the most common scenarios in which you would need a helper and focused on creating helpers for these scenarios.

Fortunately, creating new HTML helpers is a very easy process. You create a new HTML helper by creating an extension method on the HtmlHelper class. For example, Listing 7 contains a new Html.SubmitButton() helper that renders an HTML form submit button.

Listing 7 – HelpersSubmitButtonHelper.cs [C#]

using System;
using System.Web.Mvc;

namespace Helpers
{
    public static class SubmitButtonHelper
    {
        /// <summary>
        /// Renders an HTML form submit button
        /// </summary>
        public static string SubmitButton(this HtmlHelper helper, string buttonText)
        {
            return String.Format("<input type="submit" value="{0}" />", buttonText);
        }

    }
}

Listing 7 – HelpersSubmitButtonHelper.cs [VB]

Public Module SubmitButtonHelper

    ''' <summary>
    ''' Renders an HTML form submit button
    ''' </summary>
    <System.Runtime.CompilerServices.Extension> _
    Function SubmitButton(ByVal helper As HtmlHelper, ByVal buttonText As String) As String
        Return String.Format("<input type=""submit"" value=""{0}"" />", buttonText)
    End Function

End Module

Listing 7 contains an extension method named SubmitButton(). The SubmitButton() helper simply returns a string that represents an HTML <input type=”submit” /> tag.

Because the SubmitButton() method extends the HtmlHelper class, this method appears as a method of the HtmlHelper class in Intellisense (see Figure 6).

Figure 6 – Html.SubmitButton() Html helper included in Intellisense

clip_image012

The view in Listing 8 uses the new Html.SubmitButton() helper to render the submit button for a form. Make sure that you import the namespace associated with your helper or the helper won’t appear in Intellisense. The correct namespace is imported in Listing 8 with the help of the <%@ Import %> directive.

*** Begin Note ***

As an alternative to registering a namespace for a particular view with the <%@ Import %> directive, you can register a namespace for an entire application in the system.web.pages.namespaces section of the web configuration (web.config) file.

*** End Note ***

Listing 8 – ViewsCustomerCreate.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Customer>" %>
<%@ Import Namespace="Helpers" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <% using (Html.BeginForm()) {%>

        <fieldset>
            <legend>Fields</legend>
            <p>
                <label for="FirstName">FirstName:</label>
                <%= Html.TextBox("FirstName") %>
            </p>
            <p>
                <label for="LastName">LastName:</label>
                <%= Html.TextBox("LastName") %>
            </p>
            <p>
                <%= Html.SubmitButton("Create Customer") %>
            </p>
        </fieldset>

    <% } %>

</asp:Content>

*** Begin Note ***

All of the standard HTML helpers, such as the Html.TextBox() helper, are also implemented as extension methods. This means that you can swap the standard set of helpers for a custom set of helpers.

*** End Note ***

Using the TagBuilder Class

The TagBuilder class is a utility class included in the ASP.NET MVC framework that you can use when building HTML helpers. The TagBuilder class, as it name suggests, makes it easier to build HTML tags.

Here’s a list of the methods of the TagBuilder class:

· AddCssClass() – Enables you to add a new class=”” attribute to a tag.

· GenerateId() – Enables you to add an id attribute to a tag. This method automatically replaces periods in the id (by default, periods are replaced by underscores)

· MergeAttribute() – Enables you to add attributes to a tag. There are multiple overloads of this method.

· SetInnerText() – Enables you to set the inner text of the tag. The inner text is HTML encode automatically.

· ToString() – Enables you to render the tag. You can specify whether you want to create a normal tag, a start tag, an end tag, or a self-closing tag.

The TagBuilder class has four important properties:

· Attributes – Represents all of the attributes of the tag.

· IdAttributeDotReplacement – Represents the character used by the GenerateId() method to replace periods (the default is an underscore).

· InnerHTML – Represents the inner contents of the tag. Assigning a string to this property does not HTML encode the string.

· TagName – Represents the name of the tag.

These methods and properties give you all of the basic methods and properties that you need to build up an HTML tag. You don’t really need to use the TagBuilder class. You could use a StringBuilder class instead. However, the TagBuilder class makes your life a little easier.

The helper in Listing 9, the Html.ImageLink() helper, is created with a TagBuilder. The Html.ImageLink() helper renders an image link.

Listing 9 – HelpersImageLinkHelper.cs [C#]

using System.Web.Mvc;
using System.Web.Routing;

namespace Helpers
{
    public static class ImageLinkHelper
    {
        public static string ImageLink(this HtmlHelper helper, string actionName, string imageUrl, string alternateText)
        {
            return ImageLink(helper, actionName, imageUrl, alternateText, null, null, null);
        }

        public static string ImageLink(this HtmlHelper helper, string actionName, string imageUrl, string alternateText, object routeValues)
        {
            return ImageLink(helper, actionName, imageUrl, alternateText, routeValues, null, null);
        }

        public static string ImageLink(this HtmlHelper helper, string actionName, string imageUrl, string alternateText, object routeValues, object linkHtmlAttributes, object imageHtmlAttributes)
        {
            var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
            var url = urlHelper.Action(actionName, routeValues);

            // Create link
            var linkTagBuilder = new TagBuilder("a");
            linkTagBuilder.MergeAttribute("href", url);
            linkTagBuilder.MergeAttributes(new RouteValueDictionary(linkHtmlAttributes));

            // Create image
            var imageTagBuilder = new TagBuilder("img");
            imageTagBuilder.MergeAttribute("src", urlHelper.Content(imageUrl));
            imageTagBuilder.MergeAttribute("alt", urlHelper.Encode(alternateText));
            imageTagBuilder.MergeAttributes(new RouteValueDictionary(imageHtmlAttributes));

            // Add image to link
            linkTagBuilder.InnerHtml = imageTagBuilder.ToString(TagRenderMode.SelfClosing);

            return linkTagBuilder.ToString();
        }
    }
}

Listing 9 – HelpersImageLinkHelper.vb [VB]

Public Module ImageLinkHelper

    <System.Runtime.CompilerServices.Extension> _
    Function ImageLink(ByVal helper As HtmlHelper, ByVal actionName As String, ByVal imageUrl As String, ByVal alternateText As String) As String
        Return ImageLink(helper, actionName, imageUrl, alternateText, Nothing, Nothing, Nothing)
    End Function

    <System.Runtime.CompilerServices.Extension> _
    Function ImageLink(ByVal helper As HtmlHelper, ByVal actionName As String, ByVal imageUrl As String, ByVal alternateText As String, ByVal routeValues As Object) As String
        Return ImageLink(helper, actionName, imageUrl, alternateText, routeValues, Nothing, Nothing)
    End Function

    <System.Runtime.CompilerServices.Extension> _
    Function ImageLink(ByVal helper As HtmlHelper, ByVal actionName As String, ByVal imageUrl As String, ByVal alternateText As String, ByVal routeValues As Object, ByVal linkHtmlAttributes As Object, ByVal imageHtmlAttributes As Object) As String
        Dim urlHelper = New UrlHelper(helper.ViewContext.RequestContext)
        Dim url = urlHelper.Action(actionName, routeValues)

        ' Create link
        Dim linkTagBuilder = New TagBuilder("a")
        linkTagBuilder.MergeAttribute("href", url)
        linkTagBuilder.MergeAttributes(New RouteValueDictionary(linkHtmlAttributes))

        ' Create image
        Dim imageTagBuilder = New TagBuilder("img")
        imageTagBuilder.MergeAttribute("src", urlHelper.Content(imageUrl))
        imageTagBuilder.MergeAttribute("alt", urlHelper.Encode(alternateText))
        imageTagBuilder.MergeAttributes(New RouteValueDictionary(imageHtmlAttributes))

        ' Add image to links
        linkTagBuilder.InnerHtml = imageTagBuilder.ToString(TagRenderMode.SelfClosing)

        Return linkTagBuilder.ToString()
    End Function
End Module

The Html.ImageLink() helper in Listing 9 has three overloads. The helper accepts the following parameters:

actionName – The controller action to invoke.

imageUrl – The URL of the image to display.

alternateText – The alt text to display for the image.

routeValues – The set of route values to pass to the controller action.

linkHtmlAttributes – The set of HTML attributes to apply to the link.

imageHtmlAttribute – The set of HTML attributes to apply to the image.

For example, you can render a delete link by calling the Html.ImageLink() helper like this:

[C#]

<%= Html.ImageLink(“Delete”, “~/Content/Delete.png”, “Delete Account”, new {AccountId=2}, null, new {border=0}) %>

[VB]

<%= Html.ImageLink(“Delete”, “~/Content/Delete.png”, “Delete Account”, New With {.AccountId=2}, Nothing, New With {.border=0}) %>

Two instances of the TagBuilder class are used in Listing 9. The first TagBuilder is used to build up the <a> link tag. The second TagBuilder is used to build up the <img> image tag.

Notice that an instance of the UrlHelper class is created. Two methods of this class are called. First, the UrlHelper.Action() method is used to generate the link to the controller action.

Second, the UrlHelper.Content() method is used to convert an application relative path into a full relative path. For example, if your application is named MyApplication then the UrlHelper.Content() method would convert the application relative path “~/Content/Delete.png” into the relative path “/MyApplication/Content/Delete.png”.

Using the HtmlTextWriter Class

As an alternative to using the TagBuilder class to build up HTML content in an HTML helper, you can use the HtmlTextWriter class. Like the TagBuilder class, the HtmlTextWriter class has specialized methods for building up a string of HTML.

Here is a list of some of the more interesting methods of the HtmlTextWriter class (this is not a comprehensive list):

· AddAttribute() – Adds an HTML attribute. When RenderBeginTag() is called, this attribute is added to the tag.

· AddStyleAttribute() – Adds a style attribute. When RenderBeginTag() is called, this style attribute is added to the tag.

· RenderBeginTag() – Renders an opening HTML tag to the output stream.

· RenderEndTag() – Closes the last tag opened with RenderBeginTag().

· Write() – Writes text to the output stream.

· WriteLine() – Writes a newline to the output stream (good for keeping your HTML readable when you do a browser View Source).

For example, the HTML helper in Listing 10 uses the HtmlTextWriter to create a bulleted list.

Listing 10 – HelpersBulletedListHelper.cs [C#]

using System;
using System.Collections;
using System.IO;
using System.Web.Mvc;
using System.Web.UI;

namespace Helpers
{
    public static class BulletedListHelper
    {
        public static string BulletedList(this HtmlHelper helper, string name)
        {
            var items = helper.ViewData.Eval(name) as IEnumerable;
            if (items == null)
                throw new NullReferenceException("Cannot find " + name + " in view data");

            var writer = new HtmlTextWriter(new StringWriter());

            // Open UL
            writer.RenderBeginTag(HtmlTextWriterTag.Ul);
            foreach (var item in items)
            {
                writer.RenderBeginTag(HtmlTextWriterTag.Li);
                writer.Write(helper.Encode(item));
                writer.RenderEndTag();
                writer.WriteLine();
            }
            // Close UL
            writer.RenderEndTag();

            // Return the HTML string
            return writer.InnerWriter.ToString();
        }
    }
}

Listing 10 – HelpersBulletedListHelper.vb [VB]

Imports System.IO

Public Module BulletedListHelper

    <System.Runtime.CompilerServices.Extension()> _
    Function BulletedList(ByVal helper As HtmlHelper, ByVal name As String) As String
        Dim items As IEnumerable = helper.ViewData.Eval(name)
        If items Is Nothing Then
            Throw New NullReferenceException("Cannot find " & name & " in view data")
        End If

        Dim writer = New HtmlTextWriter(New StringWriter())

        ' Open UL
        writer.RenderBeginTag(HtmlTextWriterTag.Ul)
        For Each item In items
            writer.RenderBeginTag(HtmlTextWriterTag.Li)
            writer.Write(helper.Encode(item))
            writer.RenderEndTag()
            writer.WriteLine()
        Next item
        ' Close UL
        writer.RenderEndTag()

        ' Return the HTML string
        Return writer.InnerWriter.ToString()
    End Function
End Module

The list of customers is retrieved from view state with the help of the ViewData.Eval() method. If you call ViewData.Eval(“Customers”), the method attempts to retrieve an item from the view data dictionary named Customers. However, if the Customers item cannot be retrieved from the view data dictionary, then the method attempts to retrieve the value of a property with the name Customers from the view data model (the view data dictionary take precedence over the view data model).

The HtmlTextWriter class is used to render the HTML <ul> and <li> tags needed to create the bulleted list. Each item from the items collection is rendered into the list.

You can call the Html.BulletedList() helper in a view like this:

<%= Html.BulletedList(“Customers”) %>

You can add the list of customers to view data with the controller action in Listing 11.

Listing 11 – ControllersCustomerController.cs [C#]

public ActionResult List()
{
    ViewData["Customers"] = from c in _entities.CustomerSet
                            select c.LastName;
    return View();
}

Listing 11 – ControllersCustomerController.vb [VB]

Function List() As ActionResult
    ViewData("Customers") = From c In _entities.CustomerSet _
                            Select c.LastName
    Return View()
End Function

When you invoke the List() action, a list of customer last names are added to view state. When you call the Html.BulletedList() helper method, you get the bulleted list displayed in Figure 7.

Figure 7 – Using the Html.BulletedList HTML helper

clip_image014

There are multiple ways that you can build up HTML content within an HTML helper. You can use the TagBuilder class, the HtmlTextWriter class, or even the StringBuilder class. The choice is entirely a matter of preference.

Creating a DataGrid Helper

In this section, we tackle a more complicated HTML helper: we build an Html.DataGrid() helper that renders a list of database records in an HTML table. We start with the basics and then we add sorting and paging to our Html.DataGrid() helper.

The basic Html.DataGrid() helper is contained in Listing 12.

Listing 12 – HelpersDataGridHelperBasic.cs [C#]

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Mvc;
using System.Web.UI;

namespace Helpers
{

    public static class DataGridHelper
    {
        public static string DataGrid<T>(this HtmlHelper helper)
        {
            return DataGrid<T>(helper, null, null);
        }

        public static string DataGrid<T>(this HtmlHelper helper, object data)
        {
            return DataGrid<T>(helper, data, null);
        }


        public static string DataGrid<T>(this HtmlHelper helper, object data, string[] columns)
        {
            // Get items
            var items = (IEnumerable<T>)data;
            if (items == null)
                items = (IEnumerable<T>)helper.ViewData.Model;


            // Get column names
            if (columns == null)
                columns = typeof(T).GetProperties().Select(p => p.Name).ToArray();

            // Create HtmlTextWriter
            var writer = new HtmlTextWriter(new StringWriter());

            // Open table tag
            writer.RenderBeginTag(HtmlTextWriterTag.Table);

            // Render table header
            writer.RenderBeginTag(HtmlTextWriterTag.Thead);
            RenderHeader(helper, writer, columns);
            writer.RenderEndTag();

            // Render table body
            writer.RenderBeginTag(HtmlTextWriterTag.Tbody);
            foreach (var item in items)
                RenderRow<T>(helper, writer, columns, item);
            writer.RenderEndTag();

            // Close table tag
            writer.RenderEndTag();

            // Return the string
            return writer.InnerWriter.ToString();
        }


        private static void RenderHeader(HtmlHelper helper, HtmlTextWriter writer, string[] columns)
        {
            writer.RenderBeginTag(HtmlTextWriterTag.Tr);
            foreach (var columnName in columns)
            {
                writer.RenderBeginTag(HtmlTextWriterTag.Th);
                writer.Write(helper.Encode(columnName));
                writer.RenderEndTag();
            }
            writer.RenderEndTag();
        }


        private static void RenderRow<T>(HtmlHelper helper, HtmlTextWriter writer, string[] columns, T item)
        {
            writer.RenderBeginTag(HtmlTextWriterTag.Tr);
            foreach (var columnName in columns)
            {
                writer.RenderBeginTag(HtmlTextWriterTag.Td);
                var value = typeof(T).GetProperty(columnName).GetValue(item, null) ?? String.Empty;
                writer.Write(helper.Encode(value.ToString()));
                writer.RenderEndTag();
            }
            writer.RenderEndTag();
        }

    }
}

Listing 12 – HelpersDataGridHelperBasic.vb [VB]

Imports System.IO

Public Module DataGridHelper

    <System.Runtime.CompilerServices.Extension()> _
    Function DataGrid(Of T)(ByVal helper As HtmlHelper) As String
        Return DataGrid(Of T)(helper, Nothing, Nothing)
    End Function


    <System.Runtime.CompilerServices.Extension()> _
    Function DataGrid(Of T)(ByVal helper As HtmlHelper, data As object) As String
        Return DataGrid(Of T)(helper, data, Nothing)
    End Function


    <System.Runtime.CompilerServices.Extension()> _
    Function DataGrid(Of T)(ByVal helper As HtmlHelper, ByVal data As IEnumerable(Of T), ByVal columns() As String) As String
        ' Get items
        Dim items = CType(data, IEnumerable(Of T))
        If items Is Nothing Then
            items = CType(helper.ViewData.Model, IEnumerable(Of T))
        End If

        ' Get column names
        If columns Is Nothing Then
            columns = GetType(T).GetProperties().Select(Function(p) p.Name).ToArray()
        End If

        ' Create HtmlTextWriter
        Dim writer = New HtmlTextWriter(New StringWriter())

        ' Open table tag
        writer.RenderBeginTag(HtmlTextWriterTag.Table)

        ' Render table header
        writer.RenderBeginTag(HtmlTextWriterTag.Thead)
        RenderHeader(helper, writer, columns)
        writer.RenderEndTag()

        ' Render table body
        writer.RenderBeginTag(HtmlTextWriterTag.Tbody)
        For Each item In items
            RenderRow(Of T)(helper, writer, columns, item)
        Next item
        writer.RenderEndTag()

        ' Close table tag
        writer.RenderEndTag()

        ' Return the string
        Return writer.InnerWriter.ToString()
    End Function


    Private Sub RenderHeader(ByVal helper As HtmlHelper, ByVal writer As HtmlTextWriter, ByVal columns() As String)
        writer.RenderBeginTag(HtmlTextWriterTag.Tr)
        For Each columnName In columns
            writer.RenderBeginTag(HtmlTextWriterTag.Th)
            writer.Write(helper.Encode(columnName))
            writer.RenderEndTag()
        Next columnName
        writer.RenderEndTag()
    End Sub


    Private Sub RenderRow(Of T)(ByVal helper As HtmlHelper, ByVal writer As HtmlTextWriter, ByVal columns() As String, ByVal item As T)
        writer.RenderBeginTag(HtmlTextWriterTag.Tr)
        For Each columnName In columns
            writer.RenderBeginTag(HtmlTextWriterTag.Td)
            Dim value = GetType(T).GetProperty(columnName).GetValue(item, Nothing)
            if IsNothing(value) Then value = String.Empty
            writer.Write(helper.Encode(value.ToString()))
            writer.RenderEndTag()
        Next columnName
        writer.RenderEndTag()
    End Sub

End Module

In Listing 12, the Html.DataGrid() helper method has three overloads. All three overloads are generic overloads. You must supply the type of object – for example, Product – that the Html.DataGrid() should render.

Here are some examples of how you can call the html.DataGrid() helper:

[C#]

<%= Html.DataGrid<Product>()%>

<%= Html.DataGrid<Product>(ViewData["products"]) %>

<%= Html.DataGrid<Product>(Model, new string[] {“Id”, “Name”})%>

[VB]

<%= Html.DataGrid(Of Product)()%>

<%= Html.DataGrid(Of Product)(ViewData(“products”)) %>

<%= Html.DataGrid(Of Product)(Model, New String() {“Id”, “Name”})%>

In the first case, the Html.DataGrid() helper renders all of the items in the view data model into an HTML table. All of the public properties of the Product class are rendered in each row of the HTML table.

In the second case, the contents of the products item in view data is rendered into an HTML table. Again all of the public properties of the Public class are rendered.

In the third case, once again, the contents of the view data model are rendered into an HTML table. However, only the Id and Name properties are rendered (see Figure 8).

Figure 8 – Rendering an HTML table with the Html.DataGrid() helper

clip_image016

The Html.DataGrid() helper uses an HtmlTextWriter to render the HTML table <table>, <thead>, <tr>, <tbody>, <th>, and <td> tags. Rendering these tags by taking advantage of the HtmlTextWriter results in cleaner and more readable code then using string concatenation (Please try to avoid string concatenation whenever possible!).

A tiny bit of reflection is used in the DataGrid() helper. First, reflection is used in the second DataGrid() method to retrieve the list of columns to display when no explicit list of columns is supplied to the helper:

[C#]

columns = typeof(T).GetProperties().Select(p => p.Name).ToArray();

[VB]

columns = GetType(T).GetProperties().Select(Function(p) p.Name).ToArray()

Also, reflection is used to retrieve the value of a property to display within the RenderRow() method:

[C#]

var value = typeof(T).GetProperty(columnName).GetValue(item, null) ?? String.Empty;

[VB]

Dim value = GetType(T).GetProperty(columnName).GetValue(item, Nothing)

If IsNothing(value) Then value = String.Empty

*** Begin Note ***

Reflection is a .NET framework feature that enables you get information about classes, methods, and properties at runtime. You can even use reflection to dynamically load assemblies and execute methods at runtime.

*** End Note ***

The Html.DataGrid() helper displays any collection of items that implements the IEnumerable<T> interface. For example, the controller action in Listing 13 assigns a set of products to the view data model property. This list of products can be displayed by the Html.DataGrid() helper.

Listing 13 – ControllersProductController.cs [C#]

using System.Linq;
using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private ToyStoreDBEntities _entities = new ToyStoreDBEntities();

        public ActionResult Index()
        {
            return View(_entities.ProductSet.ToList());
        }
    }
}

Listing 13 – ControllersProductController.vb [VB]

Public Class ProductController
    Inherits System.Web.Mvc.Controller

    Private _entities As New ToyStoreDBEntities()

    Function Index() As ActionResult
        Return View(_entities.ProductSet.ToList())
    End Function

End Class

Adding Sorting to the DataGrid Helper

Let’s make our Html.DataGrid() helper just a little more fancy. In this section, we’ll add sorting support. When you click a column header in the HTML table rendered by the Html.DataGrid() helper, the HTML table is sorted by the selected column (see Figure 9).

Figure 9 – Html.DataGrid() with sorting support

clip_image018

In order to add the sorting support, we need to modify just one method of the existing DataGridHelper class. We need to modify the RenderHeader() method so that it renders links for headers. The modified RenderHeader() method is contained in Listing 14.

Listing 14 – HelpersDataGridHelperSorting.cs [C#]

private static void RenderHeader(HtmlHelper helper, HtmlTextWriter writer, string[] columns)
{
    writer.RenderBeginTag(HtmlTextWriterTag.Tr);
    foreach (var columnName in columns)
    {
        writer.RenderBeginTag(HtmlTextWriterTag.Th);
        var currentAction = (string)helper.ViewContext.RouteData.Values["action"];
        var link = helper.ActionLink(columnName, currentAction, new {sort=columnName});
        writer.Write(link);
        writer.RenderEndTag();
    }
    writer.RenderEndTag();
}

Listing 14 – HelpersDataGridHelperSorting.vb [VB]

Private Sub RenderHeader(ByVal helper As HtmlHelper, ByVal writer As HtmlTextWriter, ByVal columns() As String)
    writer.RenderBeginTag(HtmlTextWriterTag.Tr)
    For Each columnName In columns
        writer.RenderBeginTag(HtmlTextWriterTag.Th)
        Dim currentAction = CStr(helper.ViewContext.RouteData.Values("action"))
        Dim link = helper.ActionLink(columnName, currentAction, New With {Key .sort = columnName})
        writer.Write(link)
        writer.RenderEndTag()
    Next columnName
    writer.RenderEndTag()
End Sub

The modified RenderHeader() method in Listing 14 creates a link for each header column by calling the HtmlHelper.ActionLink() method. Notice that the name of the header column is included as a route value in the link. For example, the following link is rendered for the Price header:

/Product/SortProducts?sort=Price

The actual database sorting happens within the Product controller. The SortProducts action in Listing 15 returns the products in different sort orders depending on the value of the sort parameter passed to the action.

Listing 15 – ControllersProductController.cs with SortProducts [C#]

public ActionResult SortProducts(string sort)
{
    IEnumerable<Product> products;
    sort = sort ?? string.Empty;
    switch (sort.ToLower())
    {
        case "name":
            products = from p in _entities.ProductSet
                       orderby p.Name select p;
            break;
        case "price":
            products = from p in _entities.ProductSet
                       orderby p.Price
                       select p;
            break;
        default:
            products = from p in _entities.ProductSet
                       orderby p.Id
                       select p;
            break;
    }

    return View(products);
}

Listing 15 – ControllersProductController.vb with SortProducts [VB]

Function SortProducts(ByVal sort As String) As ActionResult
    Dim products As IEnumerable(Of Product)
    sort = If((sort <> Nothing), sort, String.Empty)
    Select Case sort.ToLower()
        Case "name"
            products = From p In _entities.ProductSet _
                       Order By p.Name _
                       Select p
        Case "price"
            products = From p In _entities.ProductSet _
                       Order By p.Price _
                       Select p
        Case Else
            products = From p In _entities.ProductSet _
                       Order By p.Id _
                       Select p
    End Select

    Return View(products)
End Function

Adding Paging to the DataGrid Helper

It really wouldn’t be a proper Html.DataGrid() helper unless the helper supported paging. In this section, we modify our Html.DataGrid() helper so that it supports efficient paging through a large set of database records (see Figure 10).

Figure 10 – Paging through database records

clip_image020

In order to add paging support, we need to create two new supporting classes:

· PagedList – An instance of this class is passed to the Html.DataGrid() helper to represent a single page of records.

· PagingLinqExtensions – This class contains extension methods that extend the IQueryable<T> interface with ToPagedList() methods that return a PagedList from a query.

The PagedList class is contained in Listing 16. This class

Listing 16 – PagingPagedList.cs [C#]

using System;
using System.Collections.Generic;

namespace Paging
{
    public class PagedList<T> : List<T>
    {
        public PagedList(IEnumerable<T> items, int pageIndex, int pageSize, int totalItemCount, string sortExpression)
        {
            this.AddRange(items);
            this.PageIndex = pageIndex;
            this.PageSize = pageSize;
            this.SortExpression = sortExpression;
            this.TotalItemCount = totalItemCount;
            this.TotalPageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize);
        }

        public int PageIndex { get; set; }
        public int PageSize { get; set; }
        public string SortExpression { get; set; }
        public int TotalItemCount { get; set; }
        public int TotalPageCount { get; private set; }

    }
}

Listing 16 – PagingPagedList.vb [VB]

Public Class PagedList(Of T)
    Inherits List(Of T)

    Private _pageIndex As Integer
    Private _pageSize As Integer
    Private _sortExpression As String
    Private _totalItemCount As Integer
    Private _totalPageCount As Integer

    Public Sub New(ByVal items As IEnumerable(Of T), ByVal pageIndex As Integer, ByVal pageSize As Integer, ByVal totalItemCount As Integer, ByVal sortExpression As String)
        Me.AddRange(items)
        Me.PageIndex = pageIndex
        Me.PageSize = pageSize
        Me.SortExpression = sortExpression
        Me.TotalItemCount = totalItemCount
        Me.TotalPageCount = CInt(Fix(Math.Ceiling(totalItemCount / CDbl(pageSize))))
    End Sub

    Public Property PageIndex() As Integer
        Get
            Return _pageIndex
        End Get
        Set(ByVal value As Integer)
            _pageIndex = value
        End Set
    End Property
    Public Property PageSize() As Integer
        Get
            Return _pageSize
        End Get
        Set(ByVal value As Integer)
            _pageSize = value
        End Set
    End Property

    Public Property SortExpression() As String
        Get
            Return _sortExpression
        End Get
        Set(ByVal value As String)
            _sortExpression = value
        End Set
    End Property
    Public Property TotalItemCount() As Integer
        Get
            Return _totalItemCount
        End Get
        Set(ByVal value As Integer)
            _totalItemCount = value
        End Set
    End Property
    Public Property TotalPageCount() As Integer
        Get
            Return _totalPageCount
        End Get
        Private Set(ByVal value As Integer)
            _totalPageCount = value
        End Set
    End Property

End Class

 

The PagedList class inherits from the base generic List class and adds specialized properties for paging. The PageList class represents the following properties:

· PageIndex – The currently selected page (zero based).

· PageSize – The number of records to display per page.

· SortExpression – The column that determines the sort order of the records.

· TotalItemCount – The total number of items in the database.

· TotalPageCount – The total number of page numbers to display.

The second class, the PagingLinqExtensions class, extends the IQueryable interface to make it easier to return a PagedList from a LINQ query. The PagingLinqExtensions class is contained in Listing 17.

Listing 17 – PagingPagingLinqExtensions.cs [C#]

using System;
using System.Linq;

namespace Paging
{
    public static class PageLinqExtensions
    {
        public static PagedList<T> ToPagedList<T>
            (
                this IQueryable<T> allItems,
                int? pageIndex,
                int pageSize
            )
        {
            return ToPagedList<T>(allItems, pageIndex, pageSize, String.Empty);

        }

        public static PagedList<T> ToPagedList<T>
            (
                this IQueryable<T> allItems,
                int? pageIndex,
                int pageSize,
                string sort
            )
        {
            var truePageIndex = pageIndex ?? 0;
            var itemIndex = truePageIndex * pageSize;
            var pageOfItems = allItems.Skip(itemIndex).Take(pageSize);
            var totalItemCount = allItems.Count();
            return new PagedList<T>(pageOfItems, truePageIndex, pageSize, totalItemCount, sort);

        }
    }
}

Listing 17 – PagingPagingLinqExtensions.vb [VB]

Public Module PageLinqExtensions

    <System.Runtime.CompilerServices.Extension> _
    Function ToPagedList(Of T)(ByVal allItems As IQueryable(Of T), ByVal pageIndex As Integer?, ByVal pageSize As Integer) As PagedList(Of T)
        Return ToPagedList(Of T)(allItems, pageIndex, pageSize, String.Empty)
    End Function

    <System.Runtime.CompilerServices.Extension> _
    Function ToPagedList(Of T)(ByVal allItems As IQueryable(Of T), ByVal pageIndex As Integer?, ByVal pageSize As Integer, ByVal sort As String) As PagedList(Of T)
        Dim truePageIndex = If(pageIndex.HasValue, pageIndex, 0)
        Dim itemIndex = truePageIndex * pageSize
        Dim pageOfItems = allItems.Skip(itemIndex).Take(pageSize)
        Dim totalItemCount = allItems.Count()
        Return New PagedList(Of T)(pageOfItems, truePageIndex, pageSize, totalItemCount, sort)
    End Function
End Module

 

The PagingLinqExtensions class makes it possible to return a PagedList like this:

[C#]

var products = _entities.ProductSet

    .OrderBy(p => p.Id)

    .ToPagedList(page, 2);

[VB]

Dim products = _entities.ProductSet _

    .OrderBy(Function(p) p.Id) _

    .ToPagedList(page, 2)

Notice how you can call ToPagedList() directly on a LINQ query. The PagingLinqExtensions class simplifies your code.

Finally, we need to modify our Html.DataGrid() class to use the PagedList class to represent database records. The modified Html.DataGrid() class includes the new RenderPagerRow() method contained in Listing 18.

Listing 18 – HelpersDataGridHelperPaging.cs [C#]

private static void RenderPagerRow<T>(HtmlHelper helper, HtmlTextWriter writer, PagedList<T> items, int columnCount)
{
    // Don't show paging UI for only 1 page
    if (items.TotalPageCount == 1)
        return;

    // Render page numbers
    writer.RenderBeginTag(HtmlTextWriterTag.Tr);
    writer.AddAttribute(HtmlTextWriterAttribute.Colspan, columnCount.ToString());
    writer.RenderBeginTag(HtmlTextWriterTag.Td);
    var currentAction = (string)helper.ViewContext.RouteData.Values["action"];
    for (var i = 0; i < items.TotalPageCount; i++)
    {
        if (i == items.PageIndex)
        {
            writer.Write(String.Format("<strong>{0}</strong>&nbsp;", i + 1));
        }
        else
        {
            var linkText = String.Format("{0}", i + 1);
            var link = helper.ActionLink(linkText, currentAction, new { page = i, sort=items.SortExpression});
            writer.Write(link + "&nbsp;");
        }
    }
    writer.RenderEndTag();
    writer.RenderEndTag();
}

Listing 18 – HelpersDataGridHelperPaging.vb [VB]

    Private Sub RenderPagerRow(Of T)(ByVal helper As HtmlHelper, ByVal writer As HtmlTextWriter, ByVal items As PagedList(Of T), ByVal columnCount As Integer)
        ' Don't show paging UI for only 1 page
        If items.TotalPageCount = 1 Then
            Return
        End If

        ' Render page numbers
        writer.RenderBeginTag(HtmlTextWriterTag.Tr)
        writer.AddAttribute(HtmlTextWriterAttribute.Colspan, columnCount.ToString())
        writer.RenderBeginTag(HtmlTextWriterTag.Td)
        Dim currentAction = CStr(helper.ViewContext.RouteData.Values("action"))
        For i = 0 To items.TotalPageCount - 1
            If i = items.PageIndex Then
                writer.Write(String.Format("<strong>{0}</strong>&nbsp;", i + 1))
            Else
                Dim linkText = String.Format("{0}", i + 1)
                Dim link = helper.ActionLink(linkText, currentAction, New With {Key .page = i, Key .sort = items.SortExpression})
                writer.Write(link & "&nbsp;")
            End If
        Next i
        writer.RenderEndTag()
        writer.RenderEndTag()
    End Sub

The RenderPagerRow() method in Listing 18 renders the user interface for paging. This method simply renders a list of page numbers that act as hyperlinks. The selected page number is highlighted with an HTML <strong> tag.

The modified Html.DataGrid() helper requires an instance of the PagedList class for its data parameter. You can use the controller action in Listing 19 to add the right data to view state.

Listing 19 – ControllersProductController.cs with PagedProducts [C#]

public ActionResult PagedProducts(int? page)
{
    var products = _entities.ProductSet
        .OrderBy(p => p.Id).ToPagedList(page, 2);

    return View(products);
}

Listing 19 – ControllersProductController.vb with PagedProducts [VB]

Function PagedProducts(ByVal page As Integer?) As ActionResult
    Dim products = _entities.ProductSet _
            .OrderBy(Function(p) p.Id) _
            .ToPagedList(page, 2)

    Return View(products)
End Function

*** Begin Warning ***

In Listing 19, notice that the ToPagedList() method is called on a LINQ query that includes a call to the OrderBy() method. When using the Entity Framework, you must order the results of a query before you can extract a page of records from the query.

*** End Warning ***

If you want to both page and sort the products, then you can use the controller action in Listing 20.

Listing 20 – ControllersProductController.cs with PagedSortedProducts [C#]

public ActionResult PagedSortedProducts(string sort, int? page)
{
    IQueryable<Product> products;
    sort = sort ?? string.Empty;
    switch (sort.ToLower())
    {
        case "name":
            products = from p in _entities.ProductSet
                       orderby p.Name
                       select p;
            break;
        case "price":
            products = from p in _entities.ProductSet
                       orderby p.Price
                       select p;
            break;
        default:
            products = from p in _entities.ProductSet
                       orderby p.Id
                       select p;
            break;
    }

    ViewData.Model = products.ToPagedList(page, 2, sort);
    return View();
}

Listing 20 – ControllersProductController.cs with PagedSortedProducts [VB]

Function PagedSortedProducts(ByVal sort As String, ByVal page As Integer?) As ActionResult
    Dim products As IQueryable(Of Product)
    sort = If((sort <> Nothing), sort, String.Empty)
    Select Case sort.ToLower()
        Case "name"
            products = From p In _entities.ProductSet _
                       Order By p.Name _
                       Select p
        Case "price"
            products = From p In _entities.ProductSet _
                       Order By p.Price _
                       Select p
        Case Else
            products = From p In _entities.ProductSet _
                       Order By p.Id _
                       Select p
    End Select

    ViewData.Model = products.ToPagedList(page, 2, sort)
    Return View()
End Function

Notice that when you want to support sorting, you must pass the current sort column to the ToPageList() method. If you don’t pass the current sort column then clicking a page number causes the Html.DataGrid() to forget the sort order.

Testing Helpers

In general, you should place any complicated view logic in an HTML helper. There is a simple reason for this: you can test a helper but you cannot test a view.

The Html.DataGrid() helper that we created in the previous section is a good example of a helper that requires unit tests. There are several things that I could have gotten wrong while writing this helper. You should never trust anything that you write!

Here are some expectations for our helper that we might want to test:

· The helper displays the right number of table rows. For example, if you specify that the page size is 2 rows then calling the Html.DataGrid() helper method should render an HTML table that contains 4 rows (1 header row + 2 data rows + 1 pager row).

· The helper selects the right page number. For example, if you specify that the current page index is 1 then page number 2 should be highlighted in bold in the pager user interface.

The test class in Listing 21 contains unit tests for both of these expectations.

Listing 21 – HelpersDataGridHelperTests.cs [C#]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcFakes;
using Paging;

namespace MvcApplication1.Tests.Helpers
{
    [TestClass]
    public class DataGridHelperTests
    {

        public List<Product> CreateItems(int count)
        {
            var items = new List<Product>();
            for (var i=0;i < count;i++)
            {
                var newProduct = new Product();
                newProduct.Id = i;
                newProduct.Name = String.Format("Product {0}", i);
                newProduct.Price = count - i;
                items.Add(newProduct);
            }
            return items;
        }


        [TestMethod]
        public void SecondPageNumberSelected()
        {
            // Arrange
            var items = CreateItems(5);
            var data = items.AsQueryable().ToPagedList(1, 2);

            // Act
            var fakeHtmlHelper = new FakeHtmlHelper();
            var results = DataGridHelper.DataGrid<Product>(fakeHtmlHelper, data);

            // Assert
            StringAssert.Contains(results, "<strong>2</strong>");

        }


        [TestMethod]
        public void CorrectNumberOfRows()
        {
            // Arrange
            var items = CreateItems(5);
            var data = items.AsQueryable().ToPagedList(1, 2);

            // Act
            var fakeHtmlHelper = new FakeHtmlHelper();
            var results = DataGridHelper.DataGrid<Product>(fakeHtmlHelper, data);

            // Assert (1 header row + 2 data rows + 1 pager row)
            Assert.AreEqual(4, Regex.Matches(results, "<tr>").Count);
        }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Listing 21 – HelpersDataGridHelperTests.vb [VB]

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports MvcFakes
Imports System.Text.RegularExpressions

<TestClass()> _
Public Class DataGridHelperTests

    Public Function CreateItems(ByVal count As Integer) As List(Of Product)
        Dim items = New List(Of Product)()
        For i = 0 To count - 1
            Dim newProduct = New Product()
            newProduct.Id = i
            newProduct.Name = String.Format("Product {0}", i)
            newProduct.Price = count - i
            items.Add(newProduct)
        Next i
        Return items
    End Function


    <TestMethod()> _
    Public Sub SecondPageNumberSelected()
        ' Arrange
        Dim items = CreateItems(5)
        Dim data = items.AsQueryable().ToPagedList(1, 2)

        ' Act
        Dim fakeHtmlHelper = New FakeHtmlHelper()
        Dim results = DataGridHelper.DataGrid(Of Product)(fakeHtmlHelper, data)

        ' Assert
        StringAssert.Contains(results, "<strong>2</strong>")

    End Sub


    <TestMethod()> _
    Public Sub CorrectNumberOfRows()
        ' Arrange
        Dim items = CreateItems(5)
        Dim data = items.AsQueryable().ToPagedList(1, 2)

        ' Act
        Dim fakeHtmlHelper = New FakeHtmlHelper()
        Dim results = DataGridHelper.DataGrid(Of Product)(fakeHtmlHelper, data)

        ' Assert (1 header row + 2 data rows + 1 pager row)
        Assert.AreEqual(4, Regex.Matches(results, "<tr>").Count)
    End Sub
End Class

Public Class Product
    Private _id As Integer
    Private _name As String
    Private _price As Decimal

    Public Property Id() As Integer
        Get
            Return _id
        End Get
        Set(ByVal value As Integer)
            _id = value
        End Set
    End Property

    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property

    Public Property Price() As Decimal
        Get
            Return _price
        End Get
        Set(ByVal value As Decimal)
            _price = value
        End Set
    End Property
End Class

If I want to feel completely confident about the Html.DataGrid() helper then I would need to write several more unit tests than the two tests contained in Listing 21. However, Listing 21 is a good start.

Both of the unit tests in Listing 21 take advantage of a utility method named CreateItems() that creates a list that contains a specific number of products.

Both unit tests also take advantage of the FakeHtmlHelper class from the MvcFakes project. When you call an HTML helper, you must supply an instance of the HtmlHelper class as the first parameter. The FakeHtmlHelper enables you to easily fake this helper.

*** Begin Note ***

Before you can run the tests in Listing 21, you must add a reference to the MvcFakes project to your Test project. The MvcFakes project is included in the CommonCode folder on the CD that accompanies this book.

*** End Note ***

Summary

This chapter was devoted to the topic of HTML helpers. You learned how to create views more easily by using HTML helpers to render HTML content.

In the first part of this chapter, you learned how to use the standard set of HTML helpers included with the ASP.NET MVC framework. For example, you learned how to create HTML links with the Html.ActionLink() and Url.Action() helpers. You also learned how to use the form helpers to render standard HTML form elements such as dropdown lists and textboxes.

We also discussed how you can make your websites more secure against JavaScript injection attacks by taking advantage of the Html.AntiForgeryToken() and Html.Encode() helpers.

In the next part of this chapter, we examined how you can create custom HTML helpers. We talked about the different utility classes that you can use when building a custom helper. In particular, you learned how to use the TagBuilder and HtmlTextWriter classes.

We then tackled building a real-world HTML helper. We created an Html.DataGrid() helper that renders database records in an HTML table. We added both sorting and paging support to our custom Html.DataGrid() helper.

In the final section of this chapter, you learned how to build unit tests for your custom HTML helpers. We created two unit tests for our Html.DataGrid() helper.

Discussion

  1. Brian says:

    Looks good. I’m reading over it now. Definitely looking forward to the book after readign all your sample chapters.

    Likewise, I am hoping that ASP.NET MVC final release is shipped very very soon. I sure miss having the official documentation available at my fingertips in the IDE. Any clues on ETA? ;)

  2. myjunc says:

    I am hoping that ASP.NET MVC final release is shipped very very soon

  3. BlueSky says:

    Perfect!

  4. Hello Stephen
    Thanks again for sharing your great work. Here my thoughts!
    1. In the line 26 of the Listing 2, the for attribute is bad.

    2. In the method parameter of the The Html.BeginForm() the phrase “You can’t use other HTTP methods within HTML, you must use JavaScript” means that with JavaScript can I use other HTTP methods?

    3. Thinking about HTML validation you don´t mention what HTML generates the MVC helpers? HTML 4.01 or XHTML 1.0 or 1.1? What if I want to validate my pages with w3c html validator with a specific DOCTYPE? How I tell the helpers to generate valid HTML? Or do I need to construct my custom helpers?

    4. When you tell about “Please try to avoid string concatenation whenever possible!”, I think you are telling “Please try to avoid the archaic string concatenation (a+b) whenever possible!”. I have read that String Builder has a very good string concatenation, no?

    5. Isn`t clearer “Adding Server-Side Sorting to the DataGrid Helper” than “Adding Sorting to the DataGrid Helper”?

    6. What if I want Client-Side Sorting?

    OK… Again a very good chapter. This book will be great!

  5. Jack says:

    Great chapter!

  6. Craig says:

    I recommend using Html.RouteLink in lieu of Html.ActionLink in almost every case.

  7. aogan says:

    Another very good chapter. I especially liked the PageLinqExtensions class. Here are couple of small comments:
    1) UrlHelper is mentioned but not explained.
    2) Usage of HtmlHelper is not explain. With that I mean the usage of helper.ViewContext or helper.ViewData etc

  8. achu says:

    great work, helped me a lot. waiting for the book.

  9. AlexMilo says:

    I like how the msdn site links to your page where you use firefox in a few of your screenshots lol

  10. Mark Gordon says:

    Thanks for writing the article it is helpful.

    But I’m curious, didn’t at some point when you were writing this post, a thought cross your mind that hand coding a UI hasn’t been required since early DOS tools? Therefore why are we sitting today with Visual studio 2008 hand coding a UI with HTML helper code. Personally I would be embarassed if I worked on this project…

    Shouldn’t it be obvious that a tool, being spun as object oriented, wan’t finished until it was able to subclass the MVC form then subclass the controls using something like a class browser then drag and drop them on the form – Isn’t that a far more productive way of developing applications?

    There is no developer that would deliver a custom application where the end user would have to start wordpad to edit xml data files then why does end-users of visual studio praise these out-dated unproductive programming models.

  11. Brian says:

    Mark – I think the reasoning lies in the fact that drag and drop designer and controls does not give you full control of the HTML (underlying) output. In many cases you end up with extra HTML “junk” that you do not want on your rendered page. This is the fight that has been going on forever between WYSIWYG designers and hand coding – especially in these views.

    I think the item to take home is gain of control. If you do not need it then you can opt out. I’m pretty sure more declarative tags (dare I say MVC controls?) will be available after the 1.0 release. I like the extra options myself. You may grow to as well :)

    BTW many applications (not ASP.NET WebForms) are coded in this manner – many of which are enterprise deployed so it is not so “unproductive”.

  12. Mark Gordon says:

    Hi Brian,

    Thanks for your input.

    In 90% of the applications I built I tend to use a UI layer this goes back to the MFC c++ app development days and extended up even through VFP as it was fully implemented in that tool as well.

    I probably don’t need to go into the benefits of a UI tier. Needless to say, coding this out in MVC there is a complete lose of inheritance for the most part (unless I’m missing something). This holds true for Microsoft’s composite UI controls found in WPF as well. Creating custom classes or extending the control IMHO is not a suitable replacement of the “true” oop ntier design patterns (subclassing).

    I hope as this technology matures Microsoft corrects the inheritance “bugs”, which I personally think there is an issue at the core IDE level. Currently the seperation between data and ui seems to graying almost to a point it is starting to resemble old 2 tier client server architecture again (ok maybe not quite as bad)… But I remember the debates over thin thick clients etc which finally after years there was some level of “general” community consensus on the n-tier pattern. With WPF XAML and MVC that pandoras box will probably open again unless the community/industry reject this paradigm which personally I think the acceptance jury is still out WPF has been out a couple years and I don’t see any significant major inroads yet.

    I thought one of the reasons Microsoft adpated the MVC patterns, besides to compete with ruby, was to resolve the seperation issues in webforms. Instead it appears they may have fixed one thing but broke something else in the process. :( ….

    Regards,
    Mark

  13. Garry says:

    Thanks for this. This is a very good overview of html helpers. I feel more confident in implementing them now. I especially like the way you can create your own helpers and be in total control of the HTML they create.

  14. DaveM says:

    Personally, I think that MVC is the best thing since sliced bread, and the developers should feel great. For TDD this is no better platform…

    Stephen,
    In Listing 14, Line 8 I had to change this:
    var link = helper.ActionLink(linkText, currentAction, new { page = i, sort = items.SortExpression });

    to this

    var link = System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, linkText, currentAction, new { page = i, sortCol = items.SortCol, sortDir = items.SortDir });

    Any thoughts?

  15. Mark Fox says:

    Can you explicitly note that when swapping the existing RenderHeader with the sort enabled one (Listing 14/15) you need to add a using/imports statement for System.Web.Mvc.Html.

  16. James says:

    Listing 7 – HelpersSubmitButtonHelper.cs [VB]

    should be:

    Listing 7 – HelpersSubmitButtonHelper.vb [VB]

  17. ezyshop2u says:

    Thanks for writing the article it is helpful. Its help me a lot to more understand & easy for me to make trial & error in html!

    Thanks a lot!

  18. Matt says:

    Great article and very helpful!

    My only question is on the sorting. According to your article, there is an action in your product controller called SortProducts but wouldnt that look for a page called SortProducts once your return the view? I had created a similar example and i do get an error saying that it cant find that page. Do i have to add this code to the Index action? Or do i have to create a page called the same thing as my sorting action?

  19. Erik says:

    Houston we have a problem.

    I can not get to these properties that you guys are showing in the html. All I have is 7 one is actionlink and one is viewdata.

    Here are my imports
    using System;
    using System.Web;
    using System.Web.Mvc;

    What am i missing here?

  20. this could certainly reduce my trial and error. thanks a bunch for this tutorial. hopefully more will come

  21. Jon says:

    @erik System.Web.Mvc.Html

  22. bill says:

    very good information. thanks for the excellent article.

  23. Great article. I’ll be using some of this info for my projects. Thanks so much.

  24. I appreciate this info. HTML helpers are something you don’t see much information about. You, sir, obviously see the importance of spreading this wealth of knowledge. Thank you

  25. Great Post! Really these information will help me a lot. Thanks Stephen.

  26. Stephen! Thanks for this code, this is really helpful.

  27. Nice Stephen. Definitely the right move.

  28. Understanding says:

    Understanding

  29. Term Paper says:

    Stephen! Thanks for this code, this is really helpful.

  30. Thesis says:

    Do i have to add this code to the Index action? Or do i have to create a page called the same thing as my sorting action?

  31. HTML helpers are something you don’t see much information about. You, sir, obviously see the importance of spreading this wealth of knowledge. Thank you

  32. will says:

    im working with html in notepad

  33. jfwer this is given in attachment. I understand that very well. Thanks.

  34. Steve says:

    Very useful…helped me a lot.

    Free PS3 Slim * Free PSP Go

  35. No doubt this article has helped me a lot. thank you for for sharing your knowledge to others. Keep it up dude.

  36. buy essays says:

    A kind of intereating facts about Chapter 6 – Understanding HTML Helpers. The perfect custom writings and the ability to buy custom essays about Chapter 6 – Understanding HTML Helpers is proposed by essay writing organization.

  37. No doubt this article has helped me a lot. thank you for for sharing your knowledge to others. Keep it up dude.

  38. FHI and CHI flat irons are similar in many respects. They offer pretty much the same plate widths; the CHI Farouk Turbo flat iron comes with .7″, 1″ and 2″ plates, whereas FHI flat irons come with 1″, 1 ?” and 1 ?” plates.

  39. HTML help says:

    Very useful HTML guidelines for students. Thanks.

  40. sohbet says:

    I very much agree with you that this is the case in the US as well.

  41. chat says:

    No doubt this article has helped me a lot. thank you for for sharing your knowledge to others. Keep it up dude…

  42. online games says:

    Thanks for this. This is a very good overview of html helpers.

  43. AnnaLee says:

    Buy an essay or pre written essays and be insured you know good information just about Chapter 6 – Understanding HTML Helpers

  44. hellfire says:

    All is ok in your studying career when students choose the professional essay writing organization to buy essay in. Or it used to be achievable to buy term papers corresponding with html ^)

  45. Andrew Lukas says:

    It’s distinguished for you to hold dear though, you need to buy term paper or buy research papers just because a school isn’t the finest at everything doesn’t mean it can’t be the best at sparse things. Essays blogs can keep more usefull for your article you can also buy essay. But first of all, my gratitude to this article, it has a actuation.

  46. You must know that I always buy essays or already written essays selecting writing service
    and some people purchase custom essay just about this blog . Thank you for the supreme article!

  47. Alex says:

    Yeah assuredly very
    cooperative for the elocutionists it was pleasant to read about Using the Standard HTML Helpers! If you need to get a great job firstofall you need buy resume. Study and don’t forget – if you have to work and study at the same time, there arehotshots who are ready to avail you with your resume when you under time burden and looking for a great job.

  48. John Smith says:

    If you have knowledge some content just about about us, people have to complete the essay writing topics about HTML.

  49. sasa says:

    Some papers writing services offer intereating interesting just about good, thus buy custom essay papers or buy a term paper to get know some facts about good.

  50. MarvinGot says:

    It’s not so easy to make a professional written essay, essentially if you are intent. I give advice you to notice buy an essay and to be devoid from discredit that your work will be done by custom writing service

  51. This must have taken you ages to put together this post. Thanks for going to the effort. I’ve been reading about HTML helpers for a few hours but have definitely learnt most from reading this post.

    James – electric fireplaces

  52. Roders wqwqw says:

    It’s nice text about Understanding HTML Helpers. That’s interesting to discover the essay writing services ,which would do the essay writing or custom essay. So, all would like to buy a paper.

  53. James Paul says:

    Thank you for this post. That grid code is going to be very helpful for a project I’m working on.

    sex games

    free sex games

  54. It’s lucky to know this, if it is really true. Companies tend not to realize when they create security holes from day-to-day operation.3

  55. Leopold Kravchuk says:

    Lots people give the responsibility to professional writers because they miss the talent to compose a respectable paper about HTML that is the cause why students need to use plagiarism check, but such people like writer don’t do that. Thanks a lot for the knowledge

  56. OK, I think I get most of that. Could you perhaps explain URLHelper please?

  57. Vasya Pinchuk says:

    Some people give the responsibility to expert writers because they don’t have the talent to write a good paper about it so that the reason why customers buy term paper, but such customers like writer don’t do that. Thanks for the topic

  58. bill says:

    Official Ed Hardy Store for all Clothing and Gear by Christian Audigier. The lifestyle brand is inspired by vintage tattoo art incorporating apparel Ed Hardy Clothing | Ed Hardy Hoodies | ED Hardy Long Sleeve | Ed Hardy Handbags