Tim Scott's Blog

November 24, 2008

MvcFluentHtml – Fluent HTML Interface For MS MVC

Filed under: C#, FluentHtml, MS MVC — Tags: , — Tim Scott @ 6:45 pm

UPDATE: MvcFluentHtml has been moved to MvcContrib where it lives in its own assembly, MvcContrib.FluentHtml.

A few weeks ago I attended a presentation at the KaizenConf by Jeremy Miller and Chad Myers in which they showed their “opinionated” approach to MS MVC.  One of the things that caught my attention was the fluent interface they had created for HTML generation.   So I went home and started working on this for my own application.

I have put my work in progress here: MvcFluentHtml.

Incidentally, since I started working on MvcFluentHtml, Chad and Jeremy have put out some bits of their code as opinionatedmvc.  And Karl Seguin, who also saw their presentation, has created some fluent HTML stuff which he shows in this blog post.

So what problem does MvcFluentHtml solve?  Can’t I be happy with the HtmlHelpers that come with the framework?

I have come to truly hate the overloading approach taken by the out-of-the-box Html helpers.  Methods with long signatures are hard to read, and it takes investigation to see what’s happening.  What’s worse, you must worry about problems with overload resolution, especially when some parameters are typed as object.  As a result, HtmlHelper is not easily extensible.  It’s hard to bend it to do new things without breaking existing functionality.  We saw an example of this when Beta1 was released with breaking changes.  With a fluent interface, it’s much easier to extend with new behaviors while leaving existing behaviors closed.

So how does it work?  Let’s start with some examples of how you might use it in your views:

<%=this.TextBox(x => x.FirstName).Class("required").Label("First Name:")%>
<%=this.Select(x => x.ClientId).Options((SelectList)ViewData["clients"]).Label("Client:")%>
<%=this.MultiSelect(x => x.UserId).Options(ViewModel.Users)%>
<%=this.CheckBox("enabled").LabelAfter("Enabled").Title("Click to enable.").Styles(vertical_align => "middle")%>

As you can see in this example, the helpers are methods of the view itself, not of some helper class.  You will also notice two different kinds of methods.  One kind takes the name as a string which is meant to evaluate to an object in the ViewData dictionary.  These methods are extensions of IViewDataContainer, and as such they can be used in any type of view (ViewPage, ViewPage<T>, ViewUserControl, ViewUserControl<T>, ViewMasterPage, ViewMasterPage<T>).   The other kind takes a lambda expression that refers to a member of ViewData.Model.  This kind of method extends a new interface that is defined in MvcFluentHtml MvcContrib.FluentHtml:

public interface IViewModelContainer<T> where T : class
{
    T ViewModel { get; }
    IEnumerable<IMemberBehavior> MemberBehaviors { get; }
}

Both kinds of methods set the value (value attribute, inner text, etc.) from ViewData.  Both kinds set the name of form elements so that most binders will pick up the value on post.

So what must you do before you can use these methods in your views?  To use the methods that extend IViewDataContainer, nothing.  To use the methods that extend IViewModelContainer<T>, you have some options:

  1. Derive your strongly-typed views from ModelViewPage<T>, ModelViewUserControl<T> or ModelViewMasterPage<T> which are bits of MvcFluentHtml.
  2. Implement IViewModelContainer<T> directly on your views.
  3. Define your own base view classes that implement IViewModelContainer<T>.

There is a fourth option.  MvcFluentHtml contains a set of classes, ConventionModelViewPage<T>, ConventionModelViewUserControl<T> and ConventionModelViewMasterPage<T>.  These classes apply some default behaviors.  These behaviors operate on certain model attributes, which are also part of MvcFluentHtml MvcContrib.

So given this model:

public class Person
{
    [Required]
    [MaxLength(200)]
    public string Name { get; set; }
}

And this in the view:

<%this.TextBox(x => x.Name)%>

The resulting HTML would be something like:

<input type="text" maxlength="200" id="Name" name="Name" class="required" value="Pete"/>

These attributes are found in a separate assembly, MvcFluentHtml.Attributes MvcContrib.ModelAttributes.  So in case your models are in a separate assembly from your MVC web UI, you don’t have to reference the UI specific bits in your model assembly.

But what if you want different behaviors and different attributes?  You can create custom behaviors by implementing IMemberBehavior.

public interface IMemberBehavior
{
     void Execute(IMemberElement element);
}

For example:

public class MyMemberBehavior1 : IMemberBehavior
{
    public void Execute(IMemberElement element)
    {
        var helper = new MemberBehaviorHelper<RequiredAttribute>();
        var attribute = helper.GetAttribute(element);
        if (attribute  != null)
        {
            element.SetAttr("class", "req");
        }
    }
}

Then create your own “opinionated” views something like this:

public class MyOpinionatedModelViewPage<T> : ModelViewPage<T> where T : class
{
     public MyOpinionatedModelViewPage() : base(new MyMemberBehavior1(), new MyMemberBehavior2()) { }
}

Or using your favorite IoC container:

public class MyOpinionatedModelViewPage<T> : ModelViewPage<T> where T : class
{
     public MyOpinionatedModelViewPage(IMyMemberBehavior1Marker myBehavior1, IMyMemberBehavior2Marker myBehavior1) : base(myMemberBehavior1, myMemberBehavior1) { }
     public MyOpinionatedModelViewPage() : base(IoC.Resolve<IMyMemberBehavior1Marker>(), IoC.Resolve<IMyMemberBehavior2Marker>()) { }

}

One final point.  The goal of MvcFluentHtml was to leave the opinions to you.  We did this by allowing you to define your own behaviors.  However, it is not without opinions regarding practices. For example the Select object does not have any “first option” functionality. That’s because in my opinion adding options to selects is not a view concern. Also, while we allow you to set inline styles we do not expose any methods that abstract styling (e.g., Width, TextAlign).  That’s because we want to encourage semantic HTML and leave the styling to CSS.  For the same reason we do not render any layout tags (e.g. <br/>).  Also we generally don’t try to abstract the language of HTML (e.g., we use “Select” instead of “DropDownList”).  In my opinion, MvcFluentHtml is more of a helper library than an abstraction over HTML, and moving developers away from understanding what’s going on with the HTML is not a good thing.

I heartily welcome critique, especially if you want to contribute! Areas that I know need some work are: radio button, radio button list, checkbox list, performance tweaks, XML comments, and CI.

Create a free website or blog at WordPress.com.