Convention-Based Templating in MVC 3

Today I’m going to explore the templating options available in MVC and try to create a framework to allow you to easily view multiple types of objects.  I’ve never really used the templating options in MVC and I’ve been really impressed with the options available, although some of the lesser-used features are a little counter-intuitive.

What is Templating?

You’re probably already using templating without knowing it.  All of the strongly typed helpers available in MVC 2 use some form of templating.  Take a look at the following snippet.

@Html.LabelFor(p => p.Name)

This will render a label with the actual value of the model’s Name property encoded as the value.  Pretty much all of these templates can be overridden.  Where templating becomes really useful is with the following snippet.

@Html.DisplayForModel()

In this case MVC will run through all the properties in the model and generate the different labels and values.  Unfortunately this is often not what you want – for example, you probably don’t want your Id fields being displayed in your view.  You can modify this behaviour by using certain attributes in your view – I’m not going into the details right now.

Convention-Based Meta Data

What I would like to do is to modify the default MVC behaviour to use conventions – for example, if the model contains a field named ‘Id’ I want it to be rendered as a hidden field.  To accomplish this I’m going to create my own Metadata provider.  This is the class that interrogates the model to figure out which template to use.

Keep in mind that we can either define a template for an individual field (for example for a string or integer field) or for an entire complex object.  What I’m trying to do is tweak the templates for individual fields where necessary to avoid having to create any templates for complex objects.

Let’s take a look at an example.

public class User
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public string Email { get; set; }
}

I am displaying a list of users and loading the details for a user with Ajax.

UserDetails

This is what my detail view looks like.

@Html.DisplayForModel()

So the first I want to do is to create the convention in my Metadata provider to specify that a property called ‘Id’ should be rendered as a hidden field.

public class ConventionMetadataProvider : AssociatedMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var metadata = new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName);
        if (propertyName != null)
        {
            if (propertyName.Equals("Id", StringComparison.OrdinalIgnoreCase))
            {
                metadata.TemplateHint = "HiddenInput";
                metadata.HideSurroundingHtml = true;
            }
        }

        return metadata;
    }
}

Instead of having to apply the HiddenInput to our Id field we are simply creating the convention that fields called Id should be rendered as hidden inputs.  So far so good – our Id field now renders as a hidden input.

Create a convention for long Property names

I have also created another object called ‘Product’ – let’s see how that renders with my convention-based meta data.

ProductDetails

There are a few problems here – the category (which is a related object) is simply displaying the Id value and the ‘InStock’ property doesn’t look very nice – we would ideally like a nicer display name.  Let’s start with this one since it seems like an easier problem to solve.

public static string Wordify(this string str)
{
    string newString = "";
    foreach (var character in str)
    {
        newString += char.IsUpper(character) ? " " + character : character.ToString();
    }
    return newString;
}

Here I am simply splitting all properties on capital letters – this will solve the problem with the ‘InStock’ property.  The Category display is a little trickier.

Create a Convention for Lists

There are probably quite a few ways to solve this, but what I would like to do is to pass both the product’s current category and a list of possible categories to the view model and then render a dropdown list.

public class ProductViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public IEnumerable CategoryOptions { get; set; }
    public bool InStock { get; set; }
}

I would like to create the convention that a reference field called WidgetId and a list called WidgetOptions should combine into a single dropdown list.

if (propertyName.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
{
    var referenceName = propertyName.Substring(0, propertyName.Length - 2);
    var optionsProperty = containerType.GetProperty(referenceName + "Options");
    if (optionsProperty != null)
    {
        var options = optionsProperty.GetValue(model, null) as IEnumerable;
        if (options != null)
        {
            metadata.TemplateHint = "DropDownList";
            metadata.AdditionalValues.Add("SelectList", options.SelectList(modelAccessor.Invoke()));
            metadata.DisplayName = referenceName.Wordify();
        }
    }
}

There is quite a bit left to do in order to get this to work.  Firstly, our CreateMetaData method seems to be invoked first for the entire model object and then individually for the different properties.  Unfortunately once we get to the CategoryId field we don’t have access to the entire model object which means we can’t get the list of categories.  This simply means when our method is invoked for the entire model object we need to keep a reference to it.  It works, but it’s a little messy in my opinion – I don’t understand why the method we’re implementing passes us the type of the container but not the instance.  Maybe something for MVC 4?

I also need to implement the template called DropDownList.  I first added a folder called DisplayTemplates to my Views/Shared folder and then added the DropDownList template into this folder.

@{
    var propertyName = ViewData.ModelMetadata.PropertyName;
    var selectList = (SelectList)ViewData.ModelMetadata.AdditionalValues["SelectList"];
}

@Html.DropDownList(propertyName, selectList, new { disabled = "disabled" })

I implemented creating the SelectList in an extension method – I used convention to choose the key and value fields.

FinalProductDetails

Pretty cool.

Conclusion

The idea here is to keep the actual code we have to write down to a minimum and use conventions to dictate the views being created.

It’s definitely not a polished solution by any stretch of the imagination, but I think there’s some definite promise in the concept.

Happy coding.