IHtmlString and what it can do for you

IHtmlString and what it can do for you

Start of this year, we have been working very hard on converting our MVC application from free-flowing Html in our Razor views to helper based templates. The ultimate goal is to be able to change one template and the rest of the application will follow the example. And with free-flowing HTML you can’t do that easily.

For example this is our old type of Create.cshtml for creating a Company:

@model MyApplication.Domain.Models.Country

@{
    ViewBag.Title = "Create Country";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    <fieldset class="label-on-left monitor-within">
        <legend>Country Details</legend>
        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.ISOCode)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ISOCode)
            @Html.ValidationMessageFor(model => model.ISOCode)
        </div>
        <div>
        <input type="submit" value="Create" /> | 
        @Html.ActionLink("Back to List", "Index")
        </div>
    </fieldset>
}

That looks OK if you have a handful of these. We have about 800 views. And all slightly different.

Imagine if the presentation standard changes and for every (for example) submit button you need to apply some class? Or worse add some other element before the label. Or change names of classes. Nightmare!

We have identified many standard elements on our pages and encapsulated them into standard Html Helpers. Now the same view looks like this:

@model Country

@{
    const string Title = "Create Country";
    ViewBag.Title = Title;
}

@Html.MyAppHeader(Title)

@using (Html.MyAppBeginForm("Country Details"))
{
    @Html.MyAppEditor(m => m.Name)
    @Html.MyAppEditor(m => m.ISOCode)
    @Html.MyAppCreateButton() 
    @Html.MyAppBackToList("CountriesIndex", MVC.Core.Countries.Index())
}

Now you loose the flexibility of modifying your Html as you are pleased, but that is what we are after! We wanted to have all these views to look and feel the same, no deviations.

Some Html helpers have one or two parameters, but some other grew into monsters with many optional parameters. Like this one:

public static MyAppMvcForm MyAppBeginForm(this HtmlHelper htmlHelper, String title = "", ActionResult action = null, FormMethod method = FormMethod.Post, FormEncodingType? encodingType = null, String id = null, bool disableFormMonitor = false, bool removeFieldset = false) 

Terrifying! And I should have started this refactoring not from creating templates with optional parameters, but rather use builder pattern. So this work is to be done once we finish with the major overhaul.

Today I took a stub on actually implementing this Builder pattern for one of the elements. And it worked out rather nicely:

public static class MyAppUploadElementHelpers
{
    public static UploadTagBuilder MyAppUploadElement(this HtmlHelper htmlHelper, String name)
    {
        // having HtmlHelper here only for convention
        return new UploadTagBuilder(name);
    }
}

public class UploadTagBuilder 
{
    private readonly string inputName;
    private bool multipleFiles = true;
    private bool isAsync;
    private String targetUrl;


    public UploadTagBuilder(string inputName)
    {
        this.inputName = inputName;
    }

    public UploadTagBuilder Async(String postUrl)
    {
        isAsync = true;
        this.targetUrl = postUrl;
        return this;
    }


    public UploadTagBuilder MultipleFiles(bool multiple)
    {
        multipleFiles = multiple;
        return this;
    }


    public MvcHtmlString Build()
    {
        var inputTag = new TagBuilder("input");
        inputTag.MergeAttribute("name", inputName);
        inputTag.MergeAttribute("type", "file");
        if (isAsync)
        {
            inputTag.MergeAttribute("class", "async-file-upload");
            inputTag.MergeAttribute("data-saveUrl", targetUrl);
        }
        else
        {
            inputTag.MergeAttribute("class", "simple-file-upload");
        }
        inputTag.MergeAttribute("data-multiple", multipleFiles.ToString());

        return MvcHtmlString.Create(inputTag.ToString());
    }
}

This is very simplified version of what I actually have, but implementation details of the builder are not important, this is not what I want to talk about.

So having this builder, in razor view I would have this:

@Html.UploadTagBuilder("Files").Async("/path/to/fileupload/controller").MultipleFiles(false).Build()

And it would produce something like that:

<input name="Files" type="file" class="async-file-upload" data-saveUrl="/path/to/fileupload/controller" data-multiple="true" />

This is good, because this pattern gives you plenty of flexibility without monstrous methods. The only problem I had with this was to need to call .Build() every time. It annoyed me and half of the times I had to use this helper, I forgotten it, resulting the above html being html-encoded and displayed on the page as you see above, not as file input element.

After digging about for some time, I randomly found IHtmlString interface that prompts you to have string ToHtmlString(); method on your class. Out of curiosity I’ve added this interface to the builder class and added this method:

    public string ToHtmlString()
    {
        return Build().ToString();
    }

And in the view I have removed .Build() at the end:

@Html.UploadTagBuilder("Files").Async("/path/to/fileupload/controller").MultipleFiles(false)

And resulting html from the builder was not html-encoded, giving me form-element on the page, not print-out of html.

I was very surprised with this random resolution of my problem and had a deeper dig in the framework. Turns out that every string generated by helpers are passed through HttpUtility.HtmlEncode(String value) (in System.Web). And the implementation of this method looks like this:

public static string HtmlEncode(object value)
{
    // snip... 

    IHtmlString htmlString = value as IHtmlString;
    if (htmlString != null)
    {
        return htmlString.ToHtmlString();
    }
    else
        // snip.. - does the actual encoding
}

In other words, HtmlEncode method does not encode objects that implement IHtmlString interface, which explains my output above. In 2 years of working with ASP.NET MVC I have never heard of this interface. And after a good attempt in Google to find use cases for this interface, I have found no descent articles about it, only random mentions. So please use it more!