Collections and ASP.NET MVC Templated Helpers (DisplayFor/EditorFor)

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

I’ve been spending quite a lot of time with ASP.NET MVC 2 recently, talking to customers about it and working on Proof-of-concept projects. When I first looked at the templated helpers that are new in version 2 I was slightly underwhelmed, but over time they have grown on me hugely; they provide a great way to rapidly get some UI created and then allow you to gradually tweak the behaviour as you require (right down to replacing the whole of the generated HTML if you so desire). Additionally, I find that the emphasis on partial views helps to promote UI modularity and re-use. (For an introduction to templated helpers I’d recommend Brad Wilson’s series)

Anyway, I was recently working with the templated helpers and hit a stumbling block so I thought I’d write about it here so that I can find the solution when I next encounter the problem. I should probably note that despite the title of the post, this problem occurs with any property type that isn’t convertible from a string, not just collections – so even if you are experiencing this issue with a non-collection type, keep reading!

In my scenario I’ve got a model class along the lines of the following:

 public class Foo
{
    public int Id { get; set; }
    public List<Bar> Bars { get; set; }
}

and in my view I’m using the templated helpers to generate the UI:

 <%= Html.DisplayForModel() %>

When the view is displayed, only the Id field is output. This is no massive surprise as Bars is a collection. What I had expected to be able to do was to create a partial view (BarList.ascx) and annotate the model to instructed the templated helpers to use it:

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

With this in place I re-ran the web site and... no change!

Digging deeper – custom collections and type converters

Since this is ASP.NET MVC, the source code is available so my next step was to crack it open in Visual Studio and start having a dig through it. Looking at ModelMetadata there is a notion of a complex type; basically a type is considered simple if it can be converted from a string and everything else is complex. In the templated helpers, complex properties are filtered out before the UIHint is even considered, which explains the before I saw above.

Suppose for a moment that instead of List<Bar> I had used a custom type (e.g. BarList) as my collection type:

 public class Foo
{
    public int Id { get; set; }
    public BarList Bars { get; set; } // <-- Custom collection type
}
public class BarList : List<Bar>
{
}

Then through the magic of TypeConverters, I would be able to decorate BarList with a TypeConverter attribute:

 [TypeConverter(typeof(BarListTypeConverter))]
public class BarList : List<Bar>
{
}

and then provide an implementation of BarListTypeConverter:

 public class BarListTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        bool canConvertFrom = base.CanConvertFrom(context, sourceType);
        return canConvertFrom;
    }
}

With these changes when the ModelMetadata queries to see whether Foo.Bars is convertible from a string it will end up calling BarListTypeConverter which will return true. So ModelMetadata will determine that Foo.Bars is a simple type rather than a complex type and I will be able to use a UIHint to specify the template to use:

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

(In the example above, the UIHint is actually unnecessary as the templated helper will look for a template based on the type name anyway, but you could specify “SomeOtherBarList” if you wanted!)

So, we now have a way round the issue, but it requires us to use a custom collection type (so that we can attach the TypeConverter attribute). Stay tuned… next time I will post an alternative solution that will work if you don’t want to/can’t use a custom collection type!