7.23.2010

What we are doing with HtmlTags Part 2 : Form Fields

Most simple forms we write, especially in LOB applications, are repetitive sections of inputs and/or displays. Take a look at an exmaple from the MVC Music Store sample, the album editor template, notice anything repetitive?

<%@ Import Namespace="MvcMusicStore"%>

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MvcMusicStore.Models.Album>" %>

<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>

<p>
    <%: Html.LabelFor(model => model.Title)%>
    <%: Html.TextBoxFor(model => model.Title)%>
    <%: Html.ValidationMessageFor(model => model.Title)%>
</p>
<p>
    <%: Html.LabelFor(model => model.Price)%>
    <%: Html.TextBoxFor(model => model.Price)%>
    <%: Html.ValidationMessageFor(model => model.Price)%>
</p>
<p>
    <%: Html.LabelFor(model => model.AlbumArtUrl)%>
    <%: Html.TextBoxFor(model => model.AlbumArtUrl)%>
    <%: Html.ValidationMessageFor(model => model.AlbumArtUrl)%>
</p>
<p>
    <%: Html.LabelFor(model => model.Artist)%>
    <%: Html.DropDownList("ArtistId", new SelectList(ViewData["Artists"] as IEnumerable, "ArtistId", "Name", Model.ArtistId))%>
</p>
<p>
    <%: Html.LabelFor(model => model.Genre)%>
    <%: Html.DropDownList("GenreId", new SelectList(ViewData["Genres"] as IEnumerable, "GenreId", "Name", Model.GenreId))%>
</p>

It didn’t take me long to notice that the duplication was driving me absolutely crazy! Yet, the community is hailing these template helpers as the next best thing since web forms and for the life of me I have no idea why! Why stop at the abstraction of DisplayFor, InputFor and LabelFor? Our views were suffering the same problem, even with a spark and HtmlTags spin:

<div class="fields">
  !{Html.LabelFor(m => m.Quantity).AddClass("label")}
  !{Html.InputFor(m => m.Quantity).AddClass("field")}
</div>

<div class="fields">
  !{Html.LabelFor(m => m.Price).AddClass("label")}
  !{Html.InputFor(m => m.Price).AddClass("field")}
</div>

Template Convention

Both frameworks (HtmlTags or MVC2 Templates) can be extended to support the abstraction we produced, but HtmlTags is geared to do this in a much cleaner and flexible fashion. We created the idea of an edit template that includes the label and input (and validation if you so desire), all of which gets nested in some container tag. We rolled all of this up into two conventions: EditTemplateFor and DisplayTemplateFor. The difference being whether or not the field on the form is editable. I’ll admit that we could probably roll this up further into a TemplateFor convention that has configurable builders much like DisplayFor/InputFor/LabelFor in HtmlTags, but right now these are static conventions in that they don’t have hot swappable builders. That is an effort for another day :)

So here is what the views would look like now (sigh of relief):

<%: Html.EditTemplateFor(m => m.Title) %>
<%: Html.EditTemplateFor(m => m.Price) %>
<%: Html.EditTemplateFor(m => m.AlbumArtUrl)%>
<%: Html.EditTemplateFor(m => m.Artist)%>
<%: Html.EditTemplateFor(m => m.Genre)%>

!{Html.EditTemplateFor(m => m.Quantity)}
!{Html.EditTemplateFor(m => m.Price)}

How it works

The following is the EditeTemplateFor convention, the display version only varies by calling DisplayFor instead of InputFor. We simply build our label, adding a class that is statically configurable. Then we take a list of fields, in the event we have more than one falling under a single label, and for each we wrap it in a div with the class "field" and then we put the label and inputs into a div with the class "fields."
public static class TemplateHelpers
{
  public static string FieldDivClass = "field";
  public static string FieldsDivClass = "fields";
  public static string LabelClass = "label";

  public static HtmlTag EditTemplateFor<T>(this T model, params Expression<Func<T, object>>[] fieldSelectors) where T : class
  {
    Contract.Requires(fieldSelectors.Any());

    var label = model.LabelFor(fieldSelectors[0]).AddClass(LabelClass);

    var fields = fieldSelectors
      .Select(f => Tags.Div
              .AddClass(FieldDivClass)
              .Nest(model.InputFor(f)));

    return Tags.Div
      .AddClass(FieldsDivClass)
      .Nest(label)
      .Nest(fields.ToArray());      
  }

  public static HtmlTag EditTemplateFor<T>(this HtmlHelper<T> helper,
                       params Expression<Func<T, object>>[] fieldSelectors) where T : class
  {
    return EditTemplateFor(helper.ViewData.Model, fieldSelectors);
  }
Here is an alteration of the above to work with the MVC Music store ablum editor:
public static class TemplateHelpers
{
  public static string FieldDivClass = "field";
  public static string FieldsDivClass = "fields";
  public static string LabelClass = "label";
  public static string ValidationMessageClass = "validation-message";

  public static HtmlTag EditTemplateFor<T>(this HtmlHelper helper, Expression<Func<T, object>> field) where T : class    {
    var label = helper.LabelFor(field);
    var input = helper.InputFor(field);
    var validation = Tags.Span.AddClass(ValidationMessageClass)

    return Tags.Paragraph
      .Nest(label,field,validation);      
  }
}

Reproducing this with MVC2 Templates

 public static MvcHtmlString EditTemplateFor<T>(this HtmlHelper<T> helper, Expression<Func<T, object>> field) where T : class
  {
    var label = helper.LabelFor(field);
    var input = helper.EditorFor(field);
    var validation = helper.ValidationMessageFor(field);

    var template = String.Format("<p>{0}{1}{2}</p>", label, input, validation);
    return MvcHtmlString.Create(template);
  }

  public static MvcHtmlString EditTemplateFor2<T>(this HtmlHelper<T> helper, Expression<Func<T, object>> field) where T : class
  {
    var label = helper.LabelFor(field);
    var input = helper.EditorFor(field);

    var template = String.Format("<div class=\"fields\">{0}<div class=\"field\">{1}</div></div>", label, input);
    return MvcHtmlString.Create(template);
  }

The Verdict

This was my best attempt at producing the same behavior with ASP.Net MVC templates, and it's already run into some serious issues.

  1. I have to work with strings.
    1. Html is not a string, so why should I have to work with it as such.
    2. The MVC2 template return an MvcHtmlString unlike HtmlTags which returns HtmlTag
    3. I could improve this with an HtmlTextWriter but that’s not part of the template abstraction and is yet another burden to include. HtmlTags supports a model OOB!
  2. I cannot modify my label to add the class “label”. Instead, I would have to wrap the label with another tag to get a class applied or I would have to alter my label template and then that class would be applied anytime I used a label in my entire application, not just in a form field.
  3. The HtmlTags version is much more readable, maintainable and representative of the structure of the html.
  4. I cannot plug this into the scaffolding in ASP.Net MVC with EditorForModel, this is the failure in their approach to scaffolding, since it’s built on an unchangeable model with no configuration and modification, it only works for the narrow out of the box case (smells of web forms days if you ask me). In a subsequent blog post I’ll show how we can continue to build up, instead of top down, to produce flexible scaffolding on top of HtmlTags!
  5. I can literally “unit” test the HtmlTags version (if it added value to test it) without spinning up a view engine to render the result, and I don’t have to parse html!

-Wes





comments powered by Disqus