Under the covers of HTML helpers in ASP.NET MVC

Coming from the Webforms world, the thing you might miss most when using ASP.NET MVC is the ability to create rich Web controls that generate all kinds of markup and do nifty things. HTML helpers do similar work, even if their plumbing is different, and because we can see the full source code of the MVC framework, we can explore their innards. Keep in mind that this isn't a straight analog, since there are no events to worry about. The helpers have one responsibility, and that's to display the right data as HTML. The truth is that you don't need to know any of this, as the existing helpers probably meet your needs 95% of the time. But in the event you want to build your own helpers for the purpose of encapsulating some kind of common, reusable markup (or keep your views cleaner), you'll benefit from understanding what goes on inside the black box.

By the way, if you've never looked, I'd strongly encourage you to download and poke around the MVC source code. I know that you're probably expecting a big mess of overly abstract pieces that you can't follow, but it's one of the cleanest and easiest to understand projects I've ever seen. They did a really great job with this, and the whole thing weighs in at only 10,000 lines of code, and 500 of that is just resource stuff.

HTML helpers generally do the dumb work of creating markup. They save you the hassle of making input tags or links or whatever. But the other thing they do, the magic, if you will, is process model state. Controller has a property called ModelState, which is an instance of the ModelStateDictionary class. This is passed along to the view in a context object. Your views inherit from ViewPage, which has an instance of HtmlHelper (the Html property). Got all that? I'm first trying to illustrate how data gets from the controller to the view, where the helpers do their thing. The controller is where you process input via model binding and validate, and this is what gets the model state involved. It's a different topic from what we're discussing here, but hopefully you've had exposure to it and understand the goodness that makes working with input and models so easy. (Read up on the tutorials on the official site, specifically the stuff about model binders and validation.)

The HtmlHelper class by itself doesn't do much to generate markup. It does have the anti-forgery token code in it, as well as some static methods to create route links (I'm not sure I understand the decision to put them there), but otherwise, the creation of markup lies in several other classes filled with extension methods to HtmlHelper, found in the System.Web.Mvc.Html namespace. They're grouped into classes for input, forms, links, selection controls, validation, etc. Since they're all extension methods, they have access to the data in the HtmlHelper instance (the Html property of the view), so that means they can work with the model state and virtually anything else available in the view. As extension methods, you can hopefully see that you can build your own to generate most any HTML you can think of.

Let's start with the simplest example: text boxes. Found in the InputExtensions class, there are several overloads:

public static string TextBox(this HtmlHelper htmlHelper, string name) {
    return TextBox(htmlHelper, name, null /* value */);
}

public static string TextBox(this HtmlHelper htmlHelper, string name, object value) {
    return TextBox(htmlHelper, name, value, (object)null /* htmlAttributes */);
}

public static string TextBox(this HtmlHelper htmlHelper, string name, object value, object htmlAttributes) {
    return TextBox(htmlHelper, name, value, new RouteValueDictionary(htmlAttributes));
}

public static string TextBox(this HtmlHelper htmlHelper, string name, object value, IDictionary<string, object> htmlAttributes) {
    return InputHelper(htmlHelper, InputType.Text, name, value, (value == null) /* useViewData */, false /* isChecked */, true /* setId */, true /* isExplicitValue */, htmlAttributes);
}

The MVC team is obviously thinking of your well being in allowing you to provide as much, or little, as you need to make that text box. The real meat comes in the last overload, which calls another extension method in the class named InputHelper. This is where we finally start to make some HTML.

Before we get into that method, consider what the typical code looks like in your view to make a text box. It may look something like this in a strongly typed view that has a "Name" property on the Model:

<%= Html.TextBox("Name", null, new { @class = "textField" })%>

This says, "Create a text box on the 'Name' property, don't give it a value, and while you're at it, add a class attribute with the value 'textField'." This corresponds to the third overload of the available extension methods. In essence, we're calling:

InputHelper(Html /* instance of HtmlHelper on the view */, InputType.Text, "Name", null, (null == null) /* useViewData */, false /* isChecked */, true /* setId */, true /* isExplicitValue */, new { @class = "textField" });

Pretty well-factored code! This works with the extension methods for check boxes, hidden fields, passwords and radio buttons. Note the fifth parameter, the Boolean useViewData parameter. What this says in English is, "If the value given is null, then use the view data."

Here's the InputHelper code, with some of the non-relevant parts taken out for the purpose of this example.

private static string InputHelper(this HtmlHelper htmlHelper, InputType inputType, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, IDictionary<string, object> htmlAttributes) {
    if (String.IsNullOrEmpty(name)) {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
    }

    TagBuilder tagBuilder = new TagBuilder("input");
    tagBuilder.MergeAttributes(htmlAttributes);
    tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType));
    tagBuilder.MergeAttribute("name", name, true);

    string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture);
    bool usedModelState = false;

    switch (inputType) {
        case InputType.CheckBox:
            ...
        case InputType.Radio:
            ...
        case InputType.Password:
            ...
        default:
            string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
            tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
            break;
    }

    if (setId) {
        tagBuilder.GenerateId(name);
    }

    // If there are any errors for a named field, we add the css attribute.
    ModelState modelState;
    if (htmlHelper.ViewData.ModelState.TryGetValue(name, out modelState)) {
        if (modelState.Errors.Count > 0) {
            tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
        }
    }

    ...

    return tagBuilder.ToString(TagRenderMode.SelfClosing);
}

Now we can dig in. To get things started, the method creates a TagBuilder object, which is used across most (maybe all) of the helper extension methods. Check out the source if you get a chance. It encapsulates the process of creating the tag and its attributes, and does all of the boring string formatting work. It creates an ID, checks for duplicate attributes, sets the innards of a non-self-closing tag, etc. The one important thing to note is that it also concatenates CSS classes (important for the validation part, as we'll see in a minute).

The switch block trickles down to the default, where it uses the GetModelStateValue method of the HtmlHelper to see if there is a value to drop into the text box. That method returns the value from the model if it's there and matches the type String (since we're using text here), otherwise it returns null. The next line calls up the TagBuilder to add a value attribute to our tag. What it puts there depends on what's available. You may recall from the documentation or books that the first thing it tries to do is use the value stored in ViewData.ModelState, if that's available, and that's what the EvalString method of HtmlHelper does. If that's not available, it goes with the value we passed in from the markup in the view. Since we specified null, it won't show anything, even if there was no value in model state. Again, read up on how model state is used to work with validation to repopulate a view with the values entered before posting the form to the controller.

After creating an id attribute for the tag, the method finally gets to the validation piece. Recall that fields that don't validate have their helpers render the CSS class "input-validation-error" in addition to any style classes you've specified. That happens here by checking to see if there are any errors associated with the model state of this field.

Finally, the method uses TagBuilder to build out the string of HTML to drop into the view. If you look around at the other helper methods, you'll find that they all work similarly to this.

To summarize, the helpers do this work:

  • Use TagBuilder to create the HTML tag
  • Pass in the attributes
  • Figure out what the value of the HTML control will be
  • Give the tag an id
  • If there are validation errors, apply the predetermined CSS class in addition to others specified
  • Output the string of HTML into the view
As I said before, I think the code is surprisingly easy to follow, and once you understand what's going on under the covers, and how it interacts with model state, you're in a good position to start working your own helpers.

 

2 Comments

  • Thanks Jeff - great to get to know the innards of what is proving to be a massive improvement from asp.net web forms for generating clean html.

    Do you think it would be possible to use HTML Helpers inside standard.net apps/services to generate out html?

    For example if you had a REST service, it would be pretty cool to be able to deliver rendered html.

    Cheers
    John

  • Well, in Webforms (or even MVC) you can use an HttpHandler to pretty much output anything you want. In MVC, the controller actions can output whatever you'd like, not just views.

Comments have been disabled for this content.