Yesterday I was able to “push” into Github the work I’ve been doing for convention based Html generation in FubuMVC. What the hell is “convention based Html generation?” you ask? It’s simple in concept. Your team has policies, formal or informal, for how you build Html elements for the fields in a ViewModel. If your familiar with MvcContrib’s Opinionated Input Builders or MVC2’s Templates feature (very well described by Brad Wilson here), we’re doing the same thing conceptually but with much different mechanics. Right now, I’m focused on the very basic building block methods that appear on FubuMVC view’s (remember that we basically force all views in FubuMVC to be strongly typed with a single ViewModel):
- InputFor( expression ) – write an Html element to edit a single property on the ViewModel. Instead of calling TextboxFor(x => x.Something) or DropdownListFor(x => x.Other) we can just say InputFor(x => x.Some.Property.On.The.ViewModel) and let the conventions figure out what to do from there. If we need to change the model property to something else, or change our conventions about how elements should be constructed, no big deal.
- DisplayFor( expression ) – write an Html element to display the value of a single property value on the ViewModel. String fields are just displayed. Entity fields may be displayed as a link to more information on that link. Date fields are shown in short date format, etc.
- LabelFor( expression ) – write an Html “label” element for the field. Out of the box, FubuMVC will just put in the field name, but that’s not all that useful. In our Dovetail project we have this tied into our localization subsystem to pluck out a localized header text for the desired property.
I’m dubious that the form auto-generation stuff that MVC2 does has much real world value, but it would certainly be possible to build on top of what we already have. In a follow up post I’ll show more of the DSL-ey approach for creating simple forms that uses InputFor/DisplayFor/LabelFor as building blocks.
In our system we have a small Entity class named Address (all systems of any complexity have an “Address” class, but they’re all different) with a property for the first line of the address called Address1:
Leaving the appropriateness of the approach aside (it’s working very well), we use attributes for our simple validation rules (and also use those to generate “not null” / string length types of rules in our database). On the client side, we use jQuery Validation to do input validation in the browser. jQuery Validation is very simple to use (as long as you color within the lines). All I need to do is add classes and some attributes to the <input> and <select> elements to declaratively apply validation rules.
In usage, we could build the input Html element for a field by just calling InputFor() like this:
In our Dovetail system the above call generates this Html:
Did you see what happened there? When we create an input element for the Address.Address1, the conventions:
- Built a textbox element. If the property was a boolean, the conventions would build a checkbox instead. Likewise, if the property is a DateTime or DateTime? field the conventions would build a date picker.
- Set a “name” attribute” that matches our naming convention so that model binding works smoothly from client to server and in automated testing scenarios. In our case we just take the “path” to a property from the ViewModel and tear out the “.” characters
- Adds the “label” attribute with the localized header name for this field. This is used in conjunction with the jQuery validation to present a clean, localized validation summary on the screen
- Recognizes the presence of the [MaximumStringLength] attribute and sets the “maxlength” attribute
- Puts the property value into the “value” attribute. The example I used happens to come from a “New” screen, so the value is blank
- Recognizes the presence of the [Required] attribute and adds the “required” class to the element
By the way, did you see how I overrode the “id” property of that textbox? I’ll talk more about that at the bottom, but that represents a huge advantage that FubuMVC’s model has over both MvcContrib and MVC2. You can happily modify or enhance the convention-generated Html input on a case by case basis.
Can I make my own Conventions?
One of the overarching principles of FubuMVC is to push the usage of Convention over Configuration as far as it can go while still allowing FubuMVC adopters to create and add their own conventions. The Html conventions are no different. When you register / configure conventions for the Html you can register either “Builders” that know how to generate the HtmlTag (I’ll explain HtmlTags later) for a certain class of properties:
or “Modifiers” that enrich HtmlTag’s:
These ElementBuilder’s and ElementModifier’s can either be classes that you build yourself and “plug” in, or you can use the Html convention DSL shown below to create common, simple types of policies (think, if this attribute exists, add this class). A custom ElementBuilder might look like this:
Alrighty, let’s move on to registering our conventions. From the top, I create a class to hold my project’s Html conventions like so:
The basis for FubuMVC’s Html conventions, and the big differentiating feature from the MVC2 and MvcContrib approach, is the usage of the “HtmlTags” library as a semantic model. All the Html generation in FubuMVC works by building up HtmlTag objects that represent an Html element graph (HtmlTag’s can have children). Think of the old TagBuilder class on steroids. A picture’s worth a thousand words, so let’s see some samples of HtmlTag in action:
The HtmlTag model has been hugely advantageous because:
- It greatly simplifies Html construction. No ugly string bashing, class and attribute merging is handled internally by HtmlTag itself, and we’ve got plenty of convenience shortcuts. Compare this model to using user controls with tag soup or brute force string manipulation
- Allows us to create an incremental model of “policies” to add classes and attributes to elements rather than having to make one big blob of code to create the Html (something that plagues both the MvcContrib and MVC2 designs)
- The InputFor(), DisplayFor(), and LabelFor() methods all return the HtmlTag object created by the convention, and HtmlTag itself has fluent builders so that you can override or extend the Html created by the conventions on a case by case basis. We’ve gotten some great feedback and help with this feature from the MvcContrib guys, and this is their main complaint with using User Controls.
- The conventional Html generation is pretty testable in basic xUnit tests without ever having to mess with WatiN or Selenium. That’s a huge plus in my book
HtmlTags is a separate library within FubuMVC that can be used independently (I use it in StoryTeller now).
Select Box Example
I’ve had this feature, and even the technical direction, in mind since last summer. I finally got to build it out for Dovetail usage because we needed to change the way we do configurable lists in our system. We keep list data in a database table where it’s easily editable and extensible (even to the point of adding cascading relationships after the fact for customizations). We need to validate certain fields against that list data, so we mark those fields like this:
Anytime a property is decorated with the [ValueOf] attribute we know that we need to build a dropdown list to edit that field. All the information that we absolutely need to know to construct that dropdown is available on the property itself, so it made perfect sense to move to convention based Html construction instead of all the repetitive “put list data on ViewModel in Controller, transmit to View, fill options in View code” work we were doing before. We cache the list data in a static class called ValueObjectRegistry that can give us list data. Using ValueObjectRegistry and the Html Conventions gave us this:
Now that we have ValueObjectDropdownBuilder hooked into our Html conventions, all I need to do on the view is just say: “InputFor(x => x.Case.Origin)” and let the conventions handle everything.