Jaa


Pluggable Model Conventions

One of the things that I really like about ASP.NET MVC is its extensibility story.

In the recent deliveries of the ASP.NET MVC Ramp Up I’ve shown a sample project with a few extension applied. Some of these have already been published on the blog: Html.EnumDropDownFor and integrating the jQuery UI datepicker.

A lot of the time, my motivation for using the extensibility points is to remove repeated code. Essentially, if I find myself repeatedly going through the same process then I like to find ways to remove some of the repetition. Within ASP.NET MVC, there are ways to use model binding, action filters, etc to achieve this. In this post I’ll walk through creating a ModelMetadataProvider that allows additional conventions to be used in models that will influence how the view is rendered, and the validation that is applied.

Setting the scene

Before looking at the extension, let’s take a look at the scenario that it will be applied in. I’ve created an ASP.NET MVC 3 project that will allow us to work with a (slightly contrived) Product class:

    public class Product     {
[HiddenInput(DisplayValue = false)]
public int Id { get; set; }

public string Name { get; set; }

[DisplayName("Unit Price")]
public decimal UnitPrice { get; set; }

[DataType(DataType.Date)]
[DisplayName("Launch Date")]
public DateTime LaunchDate { get; set; }

[AllowHtml]
[ValidateHtml]
[DataType(DataType.Html)]
public string DescriptionHtml { get; set; }
}

My Edit.cshtml view simply uses the EditorFor helper to render the editor for a product.

        @Html.EditorFor(m => m.Product)

Here we can see that I’ve annotated the class to customise the metadata that ASP.NET MVC uses. I’ve specified DisplayName so that my editor uses “Unit Price” rather than “UnitPrice” as the display name.

The DataType annotations combine with some editor templates that I’ve created for date and html (see Integrating with the jQuery UI datepicker for more details on this) to give me a simple and consistent way to handle date and html data entry as you can see in the screenshot below.

image

It’s also worth noting that I’ve added the AllowHtml attribute on the DescriptionHtlm property to allow this field to contain html when submitted, and have created a ValidateHtml attribute to ensure that it only allows safe HTML. (NOTE: if at all possible don’t allow HTML to be entered, but if you choose to then perform proper security reviews and use something like https://antixss.codeplex.com to validate/sanitize the html).

So far we’re using the standard mechanisms for annotating to change the way that data is rendered and validated. However, there are DisplayName attributes that are used to set a user-readable display name by adding spaces into the property name. The “Date” suffix on the LaunchDate property could be viewed as an indicator that the property is a Date rather than a DateTime.

So what if these are conventions that I decide I want to follow? Wouldn’t it be nice to have ASP.NET MVC respond to those conventions? The remainder of the blog post will look at how we can achieve this…

Pluggable Conventions

All of the attributes that we saw above are fed into ModelMetadata that EditorFor and the validation feed off of. By default this is driven by reflection and the data annotations, but this is completely extensible via ModelMetadataProviders. The aim is to create a custom ModelMetadataProvider that allows different behaviour to be added so that it can be re-used with different sets of conventions.

public class PluggableConventionsModelMetadataProvider
: ModelMetadataProvider

{
private readonly ModelMetadataProvider _innerProvider;
private readonly List<Action<WrappedModelMetadata>>
_conventions = new List<Action<WrappedModelMetadata>>();
public PluggableConventionsModelMetadataProvider(
ModelMetadataProvider innerProvider)
{
_innerProvider = innerProvider;
}
public override IEnumerable<ModelMetadata> GetMetadataForProperties(
object container, Type containerType)
{
return _innerProvider.GetMetadataForProperties(container, containerType)
.Select(CreateMetadata);
}
public override ModelMetadata GetMetadataForProperty(
Func<object> modelAccessor, Type containerType, string propertyName)
{
ModelMetadata innerMetadata =
                 _innerProvider.GetMetadataForProperty(
modelAccessor, containerType, propertyName);
return CreateMetadata(innerMetadata);
}
public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
{
ModelMetadata innerMetadata = _innerProvider.GetMetadataForType(
modelAccessor, modelType);
return CreateMetadata(innerMetadata);
}
public void AddConvention(Action<WrappedModelMetadata> convention)
{
_conventions.Add(convention);
}
private ModelMetadata CreateMetadata(ModelMetadata innerMetadata)
{
WrappedModelMetadata metadata = new WrappedModelMetadata(this, innerMetadata);
foreach (var convention in _conventions)
{
convention(metadata);
}
return metadata;
}
}

This class is mostly just wrapping the base ModelMetadataProvider type. It takes an inner provider so that we can use it in conjunction with another provider, and it uses WrappedModelMetadata to wrap the metadata that the inner provider returns. WrappedModelMetadata is pretty much just a wrapper around an existing ModelMetadata instance that allows us to override properties and change the associated provider ( see the attached downloadable solution for the implementation)

To register our new provider and set up the conventions we can use the following code is global.asax.

            ModelMetadataProvider current = ModelMetadataProviders.Current;
var conventionsProvider =
new PluggableConventionsModelMetadataProvider(current);
conventionsProvider.AddConvention(m =>
{
if (m.PropertyName == "Id")
{
m.TemplateHint = "HiddenInput";
m.HideSurroundingHtml = true;
}
});
conventionsProvider.AddConvention(m =>
{
if (m.DisplayName == null)
{
m.DisplayName =
PascalStringHelpers.SplitPascalCasedString(
m.PropertyName);
}
});
conventionsProvider.AddConvention(m =>
{
if (m.PropertyName != null && m.PropertyName.EndsWith("Date"))
{
m.DataTypeName = "Date";
}
});
conventionsProvider.AddConvention(m =>
{
if (m.PropertyName != null && m.PropertyName.EndsWith("Html"))
{
m.DataTypeName = "Html";
m.RequestValidationEnabled = false;
m.AddValidator(new ValidateHtmlAttribute());
}
});
ModelMetadataProviders.Current = conventionsProvider;

Here we create the PluggableConventionsModelMetadataProvider and setup our conventions before telling ASP.NET MVC to use it in the final line. With this custom provider in place, we can strip our Product class back to

    public class Product     {
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public DateTime LaunchDate { get; set; }
public string DescriptionHtml { get; set; }
}

This is a bit lighter-weight, but the conventions we’ve created will

  • automatically
  • add spaces to the display names for us (i.e. to render “Unit Price” rather than “UnitPrice” etc)
  • set the data type for LaunchDate to Date so that we pick up our custom Date template
  • set the data type to Html for DescriptionHtml, specify that the property is allowed to have HTML and also add our ValidateHtml check

Wrapping up

Here we’ve been able to stream-line our model type by adhering to a set of conventions that we can plug in to ASP.NET MVC that give a more rapid workflow. A nice side effect is that we can use a single trigger (e.g. the “Html” suffix) to add multiple rules: setting the data type, allowing HTML and validating that the HTML doesn’t contain nasty elements.

You might not want to add these specific conventions, but the beauty of this is that you can customise the conventions that you want to use. What rules to you consistently follow? Embody those in conventions so that ASP.NET MVC can automatically give you your desired behaviour :-) 

PluggableConventions.zip

Comments

  • Anonymous
    January 08, 2012
    Hi, -added your blog to my reader. Like your posts and idea about the whole mvc framework. Keep up the good work :)

  • Anonymous
    October 11, 2012
    This is fantastic! I've taken this as a starting point and given it a fluent interface and several default conventions to choose from, and it's an absolute joy to use.