Blog Archives

Applying Conventions in ASP.NET MVC

I recently came across a interesting post from @ntcoding demonstrating the flexibility and power of FubuMvc’s HTML conventions. The post demonstrates the benefits of applying custom conventions, and provides examples such as displaying a dropdown list of Enums whenever a view model has a Enum property.

Applying these types of conventions really help to DRY-up your code base, and typically this is something that FubuMVC shines at. That said, if you’re stuck using ASP.NET MVC, not all is lost; There are some handy extension points you can use to keep things DRY.

Keeping the Context

Let’s keep Nick’s example view model, as it works for a nice comparison for the different approaches. As a reminder, we’re using a model called CreateBookInputModel that looks like this:

[gist id=1384709]
    public class CreateBookInputModel
    {
        public String Title { get; set; }

        public String Genre { get; set; }

        public String Description_BigText { get; set; }

        public BookStatus BookStatus { get; set; }

        public IList<string> Authors { get; set; }

        public HttpPostedFileBase Image { get; set; }
    }

The great thing about Nick’s example is that it provides a nice variety of opportunities to demonstrate the application of conventions.

Editor Templates

The first variation I’m going to make from Nick’s Fubu MVC example is that rather than using Spark and listing out a label and input for each property on the model, I’ll call into ASP.NET MVC’s Editor Template Html helper to build up a view:

@Html.EditorForModel()

This helper will attempt to resolve a view for the model object. In this case, since there is no custom view for the CreateBookInputModel, ASP.NET MVC will use the default editor template for Object. Brad Wilson from the MVC team does a great job of summarizing the behavior and responsibilities of the default object template:

The Object template’s primary responsibility is displaying all the properties of a complex object, along with labels for each property. However, it’s also responsible for showing the value of the model’s NullDisplayText if it’s null, and it’s also responsible for ensuring that you only show one level of properties (also known as a “shallow dive” of an object).

Using the default ASP.NET MVC editor template this helper will render out an input for each of the properties on our model, wrapped in some additional markup to provide hooks for styling with css.

To keep the markup compariable, and to demostrate the first ASP.NET MVC extension point, let’s take a look at providing our own template:

Shared/EditorTemplates/Object.cshtml

[gist id=1384742]
@if (Model == null) {
    <text>@ViewData.ModelMetadata.NullDisplayText</text>
} else if (ViewData.TemplateInfo.TemplateDepth > 1) {
    <text>@ViewData.ModelMetadata.SimpleDisplayText</text>
} else {
        foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) {
            if (prop.HideSurroundingHtml) {
                <text>@Html.Editor(prop.PropertyName)</text>
            } else {
                <p>
                    @Html.Label(prop.PropertyName)
                    @Html.Editor(prop.PropertyName)
                    @Html.ValidationMessage(prop.PropertyName)
                </p>
            }
        }
}

The only deviation from the default template here, is that I’ve replace the "wrapper" markup to be a paragraph tag.

String Template

Much like Fubu MVC, simple strings work straight out of the box, providing a simple input box for the user. You can customize this default freely by dropping your own Editor Template for string into your views. The default string template looks something like this:

Shared/EditorTemplates/String.cshtml

@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue)

Custom Conventions

The next interesting case Nick uses, is to override the default String convention to instead display a textarea for any property with a "_BigText" suffix. It is fairly typical in ASP.NET MVC so see a UIHintAttribute applied to each property that requires a custom view to be rendered:

[UIHint("BigText")]
public String Description_BigText { get; set; }

By applying this attribute, ASP.NET MVC will try to find a view called "BigText" to use for this property, before falling back to the default String template:

Shared/EditorTemplates/BigText.cshtml

@Html.TextArea("", ViewData.TemplateInfo.FormattedModelValue.ToString(),
                  0, 0, new { @class = "text-box multi-line" })

This approach works just fine, however we now need to litter our model with this attribute for every property with a "_BigText" suffix – an approach that is a little too error prone for my liking. Lets see if we can do better.

ModelMetadataProvider

Using the UIHintAttribute is one way we can provide the ASP.NET MVC additional metadata to guide it to finding a view to render the view model property. ASP.NET MVC internally uses the DataAnnotationsModelMetadataProvider (by default) to provide all the information about a view model, including the metadata from the attributes that a view model may be decorated with. Although attributes provide a easy entry point for providing metadata, it’s easy enough to provide an overridden implementation to also include additional metadata, based on some custom convention:

[gist id=1384744]
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var attributeList = attributes.ToList();
        var modelMetadata = base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);

        ProvideTextAreaForBigText(modelMetadata, propertyName);

        return modelMetadata;
    }

    private void ProvideTextAreaForBigText(ModelMetadata modelMetadata, string propertyName)
    {
        if (propertyName != null && propertyName.EndsWith("_BigText") && string.IsNullOrEmpty(modelMetadata.TemplateHint))
            modelMetadata.TemplateHint = "BigText";
    }
}

By inheriting DataAnnotationsModelMetadataProvider, we continue to support custom attributes to add metadata about our model (such as UIHint), but I’ve added an addition step here to provide a "TemplateHint" to any property with the "_BigText" suffix. The TemplateHint tells ASP.NET MVC to try to find a view called "BigText" to use to render this property. If the view is not found, it will simply falling back to the default template for this object (String). The last requirement to use this metadata provider is that it will need to be either registered in ASP.NET MVC’s DependecyResolver, or in the static registration point ModelMetadataProviders.Current on application startup.

 

Getting smart with Enums

One particularly nice part in Nick’s post, is where he provides a convention such that if a view model property is an Enum, a select input will be provided with each of the Enum items.

Doing this with ASP.NET MVC isn’t too hard either mind you. Again, we can fall back on the ModelMetadataProvider extension point to apply this convention:

[gist id=1384749]
private void ProvideSelectListForEnums(ModelMetadata modelMetadata, Type modelType)
{
    if (modelType != null && modelType.IsEnum && string.IsNullOrEmpty(modelMetadata.TemplateHint))
    {
        modelMetadata.TemplateHint = "SelectList";
        var values = Enum.GetValues(modelType).Cast<object>();
        var items = values.Select(entry => new SelectListItem { Text = Enum.GetName(modelType, entry), Value = entry.ToString() });
        var selectList = new SelectList(items, "Value", "Text", modelMetadata.Model);
        modelMetadata.AdditionalValues.Add("SelectList", selectList);
    }
}

This is slightly more involved that the previous convention, but let me walk you though it. First of all, we only want to apply the convention if the model property is an Enum. If it is, provide a "TemplateHint" to tell ASP.NET MVC to try to find an appropriate view for the select list – lets call this view "SelectList" so that it’s discoverable by other developers. Next, retrieve the values for the select list, and use them to create SelectListItems. Finally, add the select list values as additional metadata for our model, so that it is available in the view.

Speaking of the view, it looks a little like this:

[gist id=1384753]
@{
    var modelMetadata = ViewContext.ViewData.ModelMetadata;
   
    var values = modelMetadata.AdditionalValues.ContainsKey("SelectList")
        ? modelMetadata.AdditionalValues["SelectList"] as IEnumerable<SelectListItem>
        : Enumerable.Empty<SelectListItem>();
}
@Html.DropDownList("", values, "Choose..")

Let’s wrap this up – Comparison and Final Thoughts

So we’ve pretty much covered off the building blocks necessary to apply a DRY-er, convention-driven approach to building MVC apps. But wait….what about file uploads and string collections – Nick provided these in his example! You’re right – I didn’t bother implementing these, mainly because they follow exactly the same pattern already laid out above. I’ll leave these as an exercise for the reader (and am happy to take pull requests).

What I do find interesting is that following this approach in ASP.NET MVC, a few of Nicks concerns are addressed:

  • What happens if the markup is complex and is not so easily created in code?
  • What happens if need to apply specific classes?
  • What about when I want to override conventions – what pitfalls await me?

As views can be used to specify the markup for our conventions, complex markup is not so much of an issue, as we have the full power of the MVC view engine at our disposal. Additionally, since the existing ASP.NET MVC extension points still apply, it is not too difficult to handle specific cases where the conventions should not apply. For example, we can override our convention for a given property by providing a UIHintAttribute – it will take precedence over the convention, and all is happy in the world.

I will mention however, that something much, much more favorable about using FubuMVC’s html conventions is that it is easier to break out out conventions into standalone, pluggable parts.

This gets rather tricky in ASP.NET MVC since the ModelMetadataProvider is a designed to be a singly registered component. Of course, as Nick points out, you can always just use fubu mvc’s html conventions in ASP.NET MVC 😉