Collections and ASP.NET MVC Templated Helpers – Part 3

This is part of a mini-series:

  • Part 1 – Define the problem & give a workaround
  • Part 2 – Show an alternative workaround
  • Part 3 – Show a reusable, simple solution
  • Part 4 - Replacing Object.ascx

This is the third part in a mini-series and so far I’ve presented the problem and two potential workarounds. In this post I will show an alternative solution that I think gives a more satisfactory approach to the problem.

In the first post, I tried to output a Foo instance using the templated helpers:

 <%= Html.DisplayForModel() %>

where I had defined Foo as:

 public class Foo
{
    public int Id { get; set; }
    [UIHint("BarList")]
    public List<Bar> Bars { get; set; }
}

I applied the UIHint to try to inform the templated helpers how to render Foo.Bars, but the attribute was ignored because Bars is a complex type (I covered complex vs simple types in the first post). In the remainder of this post I will show how you can create a custom ModelMetadataProvider that will override the ModelMetadata behaviour.

The first step is to create our custom ModelMetadata class that will override the behaviour of IsComplexType:

 public class PropertyUiHintModelMetadata : ModelMetadata
{
    private readonly ModelMetadata _innerMetadata;

    public PropertyUiHintModelMetadata(PropertyUiHintModelMetadataProvider provider, Func<object> modelAccessor, ModelMetadata innerMetadata)
        : base(provider, innerMetadata.ContainerType, modelAccessor, innerMetadata.ModelType, innerMetadata.PropertyName)
    {
        _innerMetadata = innerMetadata;
    }

    public override Dictionary<string, object> AdditionalValues { get { return _innerMetadata.AdditionalValues; } }
    public override bool ConvertEmptyStringToNull { get { return _innerMetadata.ConvertEmptyStringToNull; } set { _innerMetadata.ConvertEmptyStringToNull = value; } }
    public override string DataTypeName { get { return _innerMetadata.DataTypeName; } set { _innerMetadata.DataTypeName = value; } }
    public override string Description { get { return _innerMetadata.Description; } set { _innerMetadata.Description = value; } }
    public override string DisplayFormatString { get { return _innerMetadata.DisplayFormatString; } set { _innerMetadata.DisplayFormatString = value; } }
    public override string DisplayName { get { return _innerMetadata.DisplayName; } set { _innerMetadata.DisplayName = value; } }
    public override string EditFormatString { get { return _innerMetadata.EditFormatString; } set { _innerMetadata.EditFormatString = value; } }
    public override IEnumerable<ModelValidator> GetValidators(ControllerContext context) { return _innerMetadata.GetValidators(context); }
    public override bool HideSurroundingHtml { get { return _innerMetadata.HideSurroundingHtml; } set { _innerMetadata.HideSurroundingHtml = value; } }
    public override bool IsReadOnly { get { return _innerMetadata.IsReadOnly; } set { _innerMetadata.IsReadOnly = value; } }
    public override bool IsRequired { get { return _innerMetadata.IsRequired; } set { _innerMetadata.IsRequired = value; } }
    public override string NullDisplayText { get { return _innerMetadata.NullDisplayText; } set { _innerMetadata.NullDisplayText = value; } }
    public override string ShortDisplayName { get { return _innerMetadata.ShortDisplayName; } set { _innerMetadata.ShortDisplayName = value; } }
    public override bool ShowForDisplay { get { return _innerMetadata.ShowForDisplay; } set { _innerMetadata.ShowForDisplay = value; } }
    public override bool ShowForEdit { get { return _innerMetadata.ShowForEdit; } set { _innerMetadata.ShowForEdit = value; } }
    public override string SimpleDisplayText { get { return _innerMetadata.SimpleDisplayText; } set { _innerMetadata.SimpleDisplayText = value; } }
    public override string TemplateHint { get { return _innerMetadata.TemplateHint; } set { _innerMetadata.TemplateHint = value; } }
    public override string Watermark { get { return _innerMetadata.Watermark; } set { _innerMetadata.Watermark = value; } }

    public override bool IsComplexType
    {
        get
        {
            if (!string.IsNullOrEmpty(TemplateHint))
            {
                return false; // if template specified then mark as simple type to allow complex properties on a model to be rendered if a UIHint is specified
            }
            return _innerMetadata.IsComplexType;
        }
    }
}

Our custom ModelMetadata class takes an existing ModelMetadata instance and wraps it. Although there’s quite a lot of code there, most of it is simply delegating functionality to the wrapped instance. The key point is the overridden implementation of IsComplexType. Here we test whether a TemplateHint has been specified and if returns false to indicate that this is a simple type, otherwise if falls back to the wrapped implementation. When using the data annotations metadata provider (which is the default, out-of-the-box provider) the UIHint attribute can be used to set the ModelMetadata.TemplateHint property.

This change in behaviour to ModelMetadata will force the templated helper to continue processing the property and look to see what template to use to render the property.

The next step is to create a class derived from ModelMetadataProvider to allow us to return our custom ModelMetadata implementation:

 public class PropertyUiHintModelMetadataProvider : ModelMetadataProvider
{
    private readonly ModelMetadataProvider _innerProvider;

    public PropertyUiHintModelMetadataProvider(ModelMetadataProvider innerProvider)
    {
        _innerProvider = innerProvider;
    }

    public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
    {
        foreach (ModelMetadata modelMetadata in _innerProvider.GetMetadataForProperties(container, containerType))
        {
            ModelMetadata metadata = modelMetadata;
            Func<object> modelAccessor = () => metadata.Model;
            yield return new PropertyUiHintModelMetadata(this, modelAccessor, modelMetadata);
        }
    }

    public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
    {
        ModelMetadata modelMetadata = _innerProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
        return new PropertyUiHintModelMetadata(this, modelAccessor, modelMetadata);
    }

    public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
    {
        ModelMetadata modelMetadata = _innerProvider.GetMetadataForType(modelAccessor, modelType);
        return new PropertyUiHintModelMetadata(this, modelAccessor, modelMetadata);
    }
}

Again, the constructor takes a ModelMetadataProvider as a parameter that we will wrap. Our provider then delegates to the wrapped provider and then wraps the returned ModelMetadata in our custom implementation from above.

All that is left is to register the custom provider in global.asax.cs

 ModelMetadataProviders.Current = new PropertyUiHintModelMetadataProvider(ModelMetadataProviders.Current);

Here we are passing the current provider to a new instance of our custom provider (so that we wrap it) and then setting our custom provider to be the current provider.

With this in place we can now use the original model type with a UIHint annotation:

 public class Foo
{
    public int Id { get; set; }
    [UIHint("BarList")]
    public List<Bar> Bars { get; set; }
}

and Foo.Bars will be rendered usign the BarList template as expected :-)

At this point it is probably worth reiterating that (despite the post title) this problem isn’t just limited to properties that are collections – it occurs with any complex typed property. I think that this solution is quite clean as it allows me to configure models in an intuitive way and doesn’t force me to change the types of my properties. It also doesn’t require me to sacrifice the automatic generation of the rendering that I partially lost with the solution I presented in my last post. And finally, by wrapping a ModelMetadataProvider this solution should work even if you aren’t using the default DataAnnotations-based provider – you should simply be able to wrap whatever provider you are using in the provider above, configure the template for a property and be good to go.

Ideally, I’d love to see even richer support for configuring the templated helpers be included out-of-the-box in a future release of ASP.NET MVC. A nice starting point would be to support applying TypeConverter attributes on properties. Having said that, I think that one of the powerful things about the framework is the number of extension points that it exposes to easily enable solutions such as this one.