Html.EditorFor and htmlAttributes

Many of the Razor HTML helpers allow passing an anonymous object in a parameter called htmlAttributes. As the name indicates, the members of this object will be added to the generated HTML as attributes. For example:

@Html.TextBoxFor(m => m.Foo, new { @class = "foo" })

Would result in generated HTML similar to:

<input type="text" id="Foo" name="Foo" value="" class="foo" />

As a result, many new developers to ASP.NET MVC will attempt something like the following, only to be confused as to why the attributes are not being added:

@Html.EditorFor(m => m.Foo, new { @class = "foo" })

The helpers, Html.EditorFor and Html.DisplayFor are unique in that they're what's referred to as "templated helpers". Instead of just spitting out some defined bit of HTML, they're capable of actually using views to determine what their output will be. These views are referred to as "editor templates" or "display templates", respectively, and I have an introduction to their use if you're interested in learning more about them.

If you take a look at the MSDN documentation for EditorExtensions.EditorFor (the actual namespace of the method), you can see a list of all the overloads and what parameters each accept. In each overload, you'll notice that the parameter that would normally be called htmlAttributes in most of the other helpers is instead indicated as additionalViewData in the case of EditorFor. Looking again at the example above, the new developer thought that an attribute of class="foo" would have been added to the generated HTML, but what actually happened is that a new key named "class" was added to the the ViewData dictionary that exists for the view being rendered. In a custom editor template, then, you could retrieve this value, ViewData["class"], and use it somehow.

As of MVC 5.1, the dev team realized that this was a fairly common use case: needing to add additional HTML attributes. As a result, they provided a way without having to actually create custom editor templates. Now you can do:

@Html.EditorFor(m => m.Foo, new { htmlAttributes = new { @class = "foo" } })

Notice that this is actually an anonymous object inside an anonymous object. Remember that the actual parameter is for additional view data, so what this does is essentially add the key ViewData["htmlAttributes"] with the value set to the anonymous object that contains the class you want set. EditorFor, now, looks for this key and will add the appropriate HTML attributes in its default templates.

To complicate things even further, though, if you do need to create a custom editor template, you're still completely on your own. This little workaround only applies to the default templates. Once you start to customize things, then you have full control over the rendered output. Full control means that unless you make something happen, it doesn't happen. However, you can simulate the same functionality via:

@{
    var htmlAttributes = ViewData["htmlAttributes"] ?? new { };
}
@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue.ToString(), htmlAttributes)

Again, all we're doing is pulling out the value from ViewData and using that to set the value for htmlAttributes. However, what if you want to have some default HTML attributes and also allow passing in additional or even replacement attributes? Take the following editor template as an example:

<!-- Views\Shared\EditorTemplates\Date.cshtml -->
@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue.ToString(), new { type = "date", @class = "form-control" })

Here, we're setting the input type to be the HTML5 "date" type, so that we get an appropriate date selection control in supporting browsers. We're also adding the "form-control" class that Bootstrap requires for form fields. If we want to allow passing in HTML attributes, then we need some way to merge these defaults with whatever is passed in. Unfortunately, there's no built-in way to do that, so we'll have to come up with something on our own. My choice was to add an extension to HtmlHelper. There's other ways this could be done, though.

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

public static partial class HtmlHelperExtensions
{
    public static IDictionary<string, object> MergeHtmlAttributes(this HtmlHelper helper, object htmlAttributesObject, object defaultHtmlAttributesObject)
    {
        var concatKeys = new string[] { "class" };

        var htmlAttributesDict = htmlAttributesObject as IDictionary<string, object>;
        var defaultHtmlAttributesDict = defaultHtmlAttributesObject as IDictionary<string, object>;

        RouteValueDictionary htmlAttributes = (htmlAttributesDict != null)
            ? new RouteValueDictionary(htmlAttributesDict)
            : HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject);
        RouteValueDictionary defaultHtmlAttributes = (defaultHtmlAttributesDict != null)
            ? new RouteValueDictionary(defaultHtmlAttributesDict)
            : HtmlHelper.AnonymousObjectToHtmlAttributes(defaultHtmlAttributesObject);

        foreach (var item in htmlAttributes)
        {
            if (concatKeys.Contains(item.Key))
            {
                defaultHtmlAttributes[item.Key] = (defaultHtmlAttributes[item.Key] != null)
                    ? string.Format("{0} {1}", defaultHtmlAttributes[item.Key], item.Value)
                    : item.Value;
            }
            else
            {
                defaultHtmlAttributes[item.Key] = item.Value;
            }
        }

        return defaultHtmlAttributes;
    }
}

That code looks much more complicated than it is. The first half is just some type play. If a dictionary is passed in, then we'll pretty much use that unmolested, but if we have an anonymous object, we'll need to use the handy AnonymousObjectToHtmlAttributes method MVC provides to convert that into a dictionary. In truth, we're dealing with RouteValueDictionary here, which admittedly seems a little odd for the purpose. It seems the MVC team created this class to work with routes, realized it had other uses as well, and never bothered to give it a more generic name. If it bothers you, take heart that the MVC team uses it all over the place as well, not just with routing.

Then, we're just merging the two dictionaries. I use a string array called concatKeys to hold attribute names where we would want to combine rather than clobber duplicate dictionary keys. Right now, that's just class, but using a string array makes for easy modifications later.

Now, let's alter our editor template above to use this:

<!-- Views\Shared\EditorTemplates\Date.cshtml -->
@{
    var defaultHtmlAttributesObject = new { type = "date", @class = "form-control" };
    var htmlAttributesObject = ViewData["htmlAttributes"] ?? new { };
    var htmlAttributes = Html.MergeHtmlAttributes(htmlAttributesObject, defaultHtmlAttributesObject);
}
@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue.ToString(), htmlAttributes)

First, we create an anonymous object to hold our default attributes. Then, we pull out the htmlAttributes key from ViewData as you've seen before. Finally, we use the extension method we create to combine these two anonymous objects and pass the result to our TextBox control. Now, you can pass additonal HTML attributes to your custom editor templates, and since we kept the ViewData key name the same, it's seemless whether you end up using an custom editor editor template or one of the default editor templates.

comments powered by Disqus