Conventional HTML in ASP.NET MVC: Replacing form helpers

Other posts in this series:

Last time, we ended at the point where we had a baseline behavior for text inputs, labels and outputs. We don’t want to stop there, however. My ultimate goal is to eliminate (as much as possible) using any specific form helper from ASP.NET MVC. Everything we need to determine what/how to render input elements is available to us on metadata, we just need to use it.

Our first order of business is to catalog the expected elements we wish to support:

  • Button (no)
  • Checkbox (yes)
  • Color (yes)
  • Date (yes)
  • DateTime (yes)
  • DateTime Local (yes)
  • Email (Yes)
  • File (No)
  • Hidden (Yes)
  • Image (No)
  • Month (No)
  • Number (Yes)
  • Password (Yes)
  • Radio (Yes)
  • Range (No)
  • Reset (No)
  • Search (No)
  • Telephone (Yes)
  • Text (Yes)
  • Time (Yes)
  • Url (Yes)

And the other two input types that don’t use the <input> element, <select> and <textarea>. This is where convention-based programming and the object model of HtmlTags really shines. Instead of us needing to completely replace a template as we do in MVC, we only need to extend the individual tags, and leave everything else alone. We know that we want to have a baseline style on all of our inputs. We also want to configure this once, which our HTML conventions allow.

So how do we want to key into our conventions? I like to follow a progression:

  • Member type
  • Member name
  • Member attributes

We can infer a lot from the type of a member. Boolean? That’s a checkbox. Nullable bool? That’s not a checkbox, but a select, and so on. Let’s look at each type of input and see what we can infer to build our conventions.

Labels

Labels can be a bit annoying, you might need localization and so on. What I’d like to do is provide some default, sensible behavior. If we look at a login view model:

public class LoginViewModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [Display(Name = "Remember me?")]
    public bool RememberMe { get; set; }
}

We have a ton of display attributes, all basically doing nothing. These labels key into a couple of things:

  • Label text
  • Validation errors

We’ll get to validation in a future post, but first let’s look at the labels. What can we provide as sensible defaults?

  • Property name
  • Split PascalCase into separate words
  • Booleans get a question mark
  • Fallback to the display attribute if it exists

A sensible label convention would get rid of nearly all of our “Label” attributes. The default conventions get us the first two, we just need to modify for the latter two:

Labels.ModifyForAttribute<DisplayAttribute>((t, a) => t.Text(a.Name));
Labels.IfPropertyIs<bool>()
    .ModifyWith(er => er.CurrentTag.Text(er.OriginalTag.Text() + "?"));

With this convention, our Display attributes go away. If we have a mismatch between the view model property and the label, we can use the Display attribute to specify it explicitly. I only find myself using this when a model is flattened. Otherwise, I try and keep the label I show the users consistent with how I model the data behind the scenes.

Checkbox

This one’s easy. Checkboxes represent true/false, so that maps to a boolean:

Editors.IfPropertyIs<bool>().Attr("type", "checkbox");

// Before
@Html.CheckBoxFor(m => m.RememberMe)
@Html.LabelFor(m => m.RememberMe)

// After
@Html.Input(m => m.RememberMe)
@Html.Label(m => m.RememberMe)

Not very exciting, we just tell Fubu for bools, make the “type” attribute a checkbox. The existing MVC template does a few other things, but I don’t like any of them (like an extra hidden etc).

Color

With some model binding magic, we can handle this by merely looking at the type:

Editors.IfPropertyIs<Color>().Attr("type", "color");

Date/Time/DateTime/Local DateTime

This one is a little bit more difficult, since the BCL doesn’t have a concept of a Date. However, NodaTime does, so we can use that type and key off of it instead:

Editors.IfPropertyIs<LocalDate>().Attr("type", "date");
Editors.IfPropertyIs<LocalTime>().Attr("type", "time");
Editors.IfPropertyIs<LocalDateTime>().Attr("type", "datetime-local");
Editors.IfPropertyIs<OffsetDateTime>().Attr("type", "datetime");

Email

Email could go a number of different ways. There’s not really an Email type in .NET, so we can’t key off the property type. The MVC version uses an attribute to opt-in to an Email template, but I think that’s redundant. In my experience, every property with “Email” in the name is an email address. Why not key off this?

Editors.If(er => er.Accessor.Name.Contains("Email"))
    .Attr("type", "email");

This one could go both ways, but if I want to also/instead go off the DataType attribute, it’s just as easy. I don’t like being too explicit or too confusing, so you’ll have to base this on what you actually find in your systems.

Hidden

Hiddens can be a bit funny. If I’m being sensible with Guid identifiers, I know right off the bat that any Guid type should be hidden. It’s not always the case, so I’d like to support the attribute if needed.

Editors.IfPropertyIs<Guid>().Attr("type", "hidden");
Editors.IfPropertyIs<Guid?>().Attr("type", "hidden");
Editors.IfPropertyHasAttribute<HiddenInputAttribute>().Attr("type", "hidden");

Number

Number inputs are a bit complicated. I actually tend to avoid them, as I find they’re not really that usable. However, I do want to provide some hints to the user as well as some rudimentary client-side validation with the “pattern” attribute.

Editors.IfPropertyIs<decimal?>().ModifyWith(m =>
    m.CurrentTag
    .Data("pattern", "9{1,9}.99")
    .Data("placeholder", "0.00"));

I’d do similar for other numeric types (integer/floating point).

Password

We’ll use the same strategy as our hidden input – key off the name if we can, otherwise check for an attribute.

Editors.If(er => er.Accessor.Name.Contains("Password"))
    .Attr("type", "password");
Editors.If(er =>
{
    var attr = er.Accessor.GetAttribute<DataTypeAttribute>();
    return attr != null && attr.DataType == DataType.Password;
}).Attr("type", "password");

We had to get a little fancy with our attribute check, but nothing too crazy.

Radio

Radio buttons represent a selection of a group of items. In my code, this is represented with an enum. Since radio buttons are a bit more complicated than just an input tag, we’ll need to build out the list of elements manually. We can either build up our select element from scratch, or modify the defaults. I’m going to go the modification route, but because it’s a little more complicated, I’ll use a dedicated class instead:

Editors.Modifier<EnumDropDownModifier>();

// Our modifier
public class EnumDropDownModifier : IElementModifier
{
    public bool Matches(ElementRequest token)
    {
        return token.Accessor.PropertyType.IsEnum;
    }

    public void Modify(ElementRequest request)
    {
        var enumType = request.Accessor.PropertyType;

        request.CurrentTag.RemoveAttr("type");
        request.CurrentTag.TagName("select");
        request.CurrentTag.Append(new HtmlTag("option"));
        foreach (var value in Enum.GetValues(enumType))
        {
            var optionTag = new HtmlTag("option")
                .Value(value.ToString())
                .Text(Enum.GetName(enumType, value));
            request.CurrentTag.Append(
                optionTag);
        }
    }
}

Element modifiers and builders follow the chain of responsibility pattern, where any modifier/builder that matches a request will be called. We only want enums, so our Matches method looks at the accessor property type. Again, this is where our conventions show their power over MVC templates. In MVC templates, you can’t modify the matching algorithm, but in our example, we just need to supply the matching logic.

Next, we use the Modify method to examine the incoming element request and make changes to it. We replace the tag name with “select”, remove the “type” attribute, but leave the other attributes alone. We append a child option element, then loop through all of the enum values to build out name/value options from our enum’s metadata.

Why use this over EnumDropDownListFor? Pretty easy – it gets all of our other conventions, like the Bootstrap CSS classes. In a system with dozens or more enumerations shown, that’s not something I want to repeat all over the place.

Telephone

We’ll treat the telephone just like our password element – check for a property name, and fall back to an attribute.

Editors.If(er => er.Accessor.Name.Contains("Phone"))
    .Attr("type", "tel");
Editors.If(er =>
{
    var attr = er.Accessor.GetAttribute<DataTypeAttribute>();
    return attr != null && attr.DataType == DataType.PhoneNumber;
}).Attr("type", "tel");

If we want to enforce a specific pattern, we’d use the appropriate data-pattern attribute.

Text

This is the default, so nothing to do here!

Url

Just like our password, we’ll look at the property name, then an attribute:

Editors.If(er => er.Accessor.Name.Contains("Url"))
    .Attr("type", "url");
Editors.If(er =>
{
    var attr = er.Accessor.GetAttribute<DataTypeAttribute>();
    return attr != null && attr.DataType == DataType.Url;
}).Attr("type", "url");

If we get tired of typing that attribute matching logic out, we can create an extension method:

public static class ElementCategoryExpressionExtensions
{
    public static ElementActionExpression HasAttributeValue<TAttribute>(
        this ElementCategoryExpression expression, Func<TAttribute, bool> matches)
        where TAttribute : Attribute
    {
        return expression.If(er =>
        {
            var attr = er.Accessor.GetAttribute<TAttribute>();
            return attr != null && matches(attr);
        });
    }
}

And our condition becomes a bit easier to read:

Editors.If(er => er.Accessor.Name.Contains("Url"))
    .Attr("type", "url");
Editors
    .HasAttributeValue<DataTypeAttribute>(attr => attr.DataType == DataType.Url)
    .Attr("type", "url");

Wrapping up

We knocked out most of the HTML5 input element types, leaving out ones that didn’t make too much sense. We can still create conventions for those missing elements, likely using property names and/or attributes to determine the right convention to use. Quite a bit more powerful than the MVC templates!

Next up, we’ll look at more complex element building example where we might need to hit the database to get a list of values for a drop down.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in ASP.NET MVC. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Joe

    Another fantastic blog post. As a .NET newbie these types of posts are eye opening. Do you have any plans to release this as a Nuget package?

  • Marco

    Nice! Please continue with these blog posts!

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1657

  • Iain Galloway

    What do you do if you need to globalise the label values?

    • jbogard

      Good question! I’d use an attribute that signified a resource name, then use my label convention to grab that text from the resource file.

  • Rodrigo

    What about performance using many html helpers?

    • jbogard

      What performance concerns do you have?

      • Rodrigo

        performance to render page

        • jbogard

          Have you been seeing performance problems?

          • Rodrigo

            Hi jbogard to render the html generates request from the server, the future of applications is SPA, why use HTML helpers?what advantage?

          • jbogard

            That’s my next post :)

            But otherwise, no, we don’t see performance problems as these techniques are things we’ve used in enterprise applications where SPAs don’t really hold any competitive advantage, and are measurably more expensive to build (today).

          • Rodrigo

            I do not think expensive

            Ok, will wait for the next article

            Thanks

          • jbogard

            You know, we didn’t think so either, until we measured against a couple projects using Backbone and Angular. When we had to give up a lot of the techniques we use on the server side to have highly efficient development, productivity suffered (being in consulting, we have the fun of measuring effort).

  • Ciel

    Hey Jimmy, I was wondering if you’ll be willing to place this project in an exposed github for viewing, in the future, or if there is any way it could be done without strapping FubuMVC into the project?

    I am loving this. You’ve hit on every nail for why I hate the usual Html Helpers. This is exactly what is needed in every way. Thank you so much for doing all of this.

    • jbogard

      Yep, my plan is to pull the pieces out and roll them into either HtmlTags or a separate library not tied to Fubu (and hopefully streamlined).

      • Ciel

        You will be my favorite person in the entire world. I’m not fond of Fubu because of how much it takes to get it ‘Installed’, and I don’t use StructureMap, I use Ninject. I don’t enjoy having two IoC libraries in my applications.

        • jbogard

          lol well I’ll give it my best shot at least :P

          • Ciel

            Is there any specific reason you didn’t just use the HtmlTags library and extend it? I’ve read through your blog posts and I’m not entirely clear on what FubuMVC is bringing to the table that HtmlTags didn’t.

          • Ciel

            In some of my projects, I’ve simply installed HtmlTags and used this class to get them usable in my Views.

            https://gist.github.com/ciel/111059420b95918eedb1

          • jbogard

            It’s those Display/Editor template conventions, those things are in Fubu.

          • Ciel

            My only real issue so far with the method you’ve chosen is the “Validation” system. It doesn’t feel very agnostic, and feels like it could be pretty much tethered to a specific one.

            However, I will state a few things for the record;
            1) when I tried to do this myself a few years ago, that was my biggest problem.

            2) I’m nowhere near your skill level. I consider you to be on god-tier in comparison, so I guess my “disagreement” comes from the fact that what you have in this demo isn’t that much different than the best I was able to do.

            3) I thought it was generally accepted that it was more appropriate to try and use validation that was as much HTML5 as possible without having to keep its logic constrained to a specific framework? I’m unclear about how you’re achieving this with the method you chose.

            4) I don’t even pretend to understand the method you finally went with. When I use the term “yours is not much better than mine”, what I am literally saying is “My code was the same for about the first half.”

            Please do not misunderstand me, I am not questioning your skill or decisions, I am questioning the decision in as much as I don’t understand it, is all. I’m saying “Hey! That thing! Wow, it works. I don’t understand why. Can you tell me why that works? I don’t get it. It doesn’t look like it should work. Where did you hide the duct tape?”

          • jbogard

            2) Flattering, but I’m standing on the shoulders of giants. All my ideas (except AutoMapper) were shamelessly ripped off :)

            3) You can still do that with Fubu – and use the FluentValidation as a backup. We use the validation framework to drive data attributes on our input elements.

            I’m hoping that pulling the framework out will make it easier to use/understand. There are a lot of moving parts, and it was QUITE A LOT of trial and error to get everything going (validation included). Validation was easy to hook up to MVC and Web API – it was just a NuGet package. From there, we just needed to figure out how to query the validators for metadata.