Preface

This blog builds on information I found in Brad Wilson‘s excellent 5 Part blog on ASP.Net MVC 2.0 Templates and ModelMetadata.

The Problem

I have been working on my first MVC 2.0 web application for a few weeks now and recently hit a story where I needed a view with parent/child data on it. The situation is very similar to the classic invoice and line items scenario. The view needs to contain data from the parent object (the invoice) and a grid of data from the child objects (the line items). Simplistically the view model looks something like this.

public class Invoice
{
    public int? InvoiceNumber { get; set; }
 
    public DateTime? InvoiceDate { get; set; }
 
    public IEnumerable LineItems { get; set; }
 
    [DisplayFormat(DataFormatString = "C")]
    public decimal GrandTotal { get; set; }
}
 
public class LineItem
{
    public int Quantity { get; set; }
 
    public string Description { get; set; }
 
    [DisplayFormat(DataFormatString = "C")]
    public decimal UnitCost { get; set; }
 
    [DisplayFormat(DataFormatString = "C")]
    public decimal TotalCost { get; set; }
}

Notice that I am using DataAnotations, as described in Brad’s blog, to indicate that the decimal values should be formated as currency.

This works fine for the GrandTotal value in the main body of the invoice. Adding the following to the view displays a nicely formatted grand total.

<%= Html.DisplayFor(i => i.GrandTotal)%>

However, when it came to the children, I wanted to use the MvcContrib Grid helper to render the data as follows:

<%
var formatForUnitCost = "C"; //TODO: get this from the ModelMetadata.
var formatForTotalCost = "C";//TODO: get this from the ModelMetadata.
 
//Convert metadata format to format string of the form "{0:format}"
formatForUnitCost = "{0:" + formatForUnitCost + "}";
formatForTotalCost = "{0:" + formatForTotalCost + "}";
 
Html.Grid(Model.LineItems).Columns( column =>
    {
        column.For(item => item.Quantity);
        column.For(item => item.Description);
        column.For(item => item.UnitCost).Format(formatForUnitCost);
        column.For(item => item.TotalCost).Format(formatForTotalCost);
    }).Render();
%>

Notice the to-do items. I expected this to be an easy thing and ultimately it was, but figuring it out was not as simple as I had hoped for. And that is the reason for my blogging today. How does one get the metadata for objects in collections?

Background and Initial Attempts

To get the ModelMetadata object for the model itself is easy. Just as you can call ViewData.Model to get the model (or simply this.Model to get a strongly typed reference if you are using a strongly typed view), you can call ViewData.ModelMetadata to get the metadata. (Sorry there is no this.ModelMetadata equivelent for strongly typed views.) So if I wanted the display format for the GrandTotal property, I could get it like this:

var formatForGrandTotal = ViewData.ModelMetadata.Properties
    .Where(p => p.PropertyName == "GrandTotal")
    .Single().DisplayFormatString;

The same does does not work so well if you replace “GrandTotal” with “LineItems,” which was my first attempt. This returned a Metadata object for the LineItems enumerator. Enumerators however don’t have any properties and thus the following returns zero.

return ViewData.ModelMetadata.Properties
    .Where(p => p.PropertyName == "LineItems")
    .Single().Properties.Count;

I tried changing the type of LineItems from IEnumerable<LineItem> to LineItem[]. This was a false improvement. The ModelMetadata object for the LineItems property now had some properties of its own, but they were just the standard properties of any array type: Length, LongLength, IsFixedSize, IsReadonly, IsSynchronized, etc. There was nothing that would provide access to the metadata for the contained types. No matter what I tried, I could not find a way to go through a collection property using the ModelMetadata interface and get metadata for the contained types.

I posted an explanation of the problem and a request for help on the ASP.net MVC forum and Brad himself was kind enough to reply but couldn’t provide me with an answer.

Attempt Two

I did some more digging and found a blog on extending MVC by implementing one’s own ModelMetadataProvider. This looked promising as a way to create and serve up my own ModelMetadata subclass. The subclass would have a property that, in cases where the current model type was a collection, would be populated with the ModelMetadata for the contained types.

As I went down this road, I became less enthusiastic. Creating a subclass that had a new property on it meant I would have to cast the ModelMetadata instance returned from ModelMetadata.Properties to my new type. That seemed messy and non-standard. MVCs own ModelMetadata subclassing did not extend the interface but only modified behavior.

Worse, I would need to subclass the the DataAnnotationsModelMetadataProvider as well. That should not be a big deal; but looking at the source code, it was clear that I would have to copy and paste most of the code out of its CreateMetadata method into my override because the implementation didn’t follow the do-only-one-thing rule. CreateMetadata did lots of things and I needed to override only one of them, namely the instantiation of the ModelMetadata instance so as to instantiate an instance of my ModelMetadata subtype. I don’t like duplicate code, but I especially don’t like it when I only own one half of it. It makes keeping things synchronized all the harder. (As an aside, this is why the do-only-one-thing rule is a prerequisite to adhering to the open-closed principle. The real problem here is that DataAnnotationsModelMetadataProvider isn’t open to extension (at least not cleanly) without requiring modification.)

The Solution

All of these concerns turned out to be irrelevent. In getting into the MVC source code, I realized I could very easily get the information I wanted without implementing anything.

As it turns out, the abstract base class ModelMatadataProvider has several methods on it that are used by the MVC framework to build out the metadata graph for the given model. One of these methods is:

public abstract ModelMetadata GetMetadataForType(
    Func<object> modelAccessor, Type modelType)

All I needed to do was get a reference to the ModelMetadataProvider currently being used by the framework and call that method with typeof(LineItem) and wha-la, I would have the metadata I was looking for. Getting the reference to the current provider is easy. It is available through the static property ModelMetadataProviders.Current.

While it is true that I didn’t have to implement anything, I did write the following extension methods to make things easier.

//Usage: someModel.GetMetadata()
//Gets the metadata for the type someModel.GetType().
public static ModelMetadata GetMetadata(this TModel model)
{
    return ModelMetadataProviders.Current.GetMetadataForType(
        () => model, typeof(TModel).);
}
 
//Usage: someModel.GetMetadataForProperty(m => m.SomeProperty)
//Gets the metadata for the type someModel.SomePropery.GetType().
public static ModelMetadata GetMetadataForProperty(
    this TModel model,
    Expression> propertySelector)
{
    var propertyName = propertySelector.GetReferencedPropertyName();
    return model.GetMetadata().Properties.Where(
        p => p.PropertyName == propertyName).Single();
}
 
//Usage: ViewModel.GetMetadataForType()
//Gets the metadata for the type MyModel.
public static ModelMetadata GetMetadataForType(
   this ViewDataDictionary model)
{
    return ModelMetadataProviders.Current.GetMetadataForType(
        null, typeof(TModel));
}
 
//Usage: ViewData.GetMetadataForProperty(m => m.SomeProperty)
//Gets the metadata for the return type of MyModel.SomeProperty.
public static ModelMetadata GetMetadataForProperty(
    this ViewDataDictionary model,
    Expression> propertySelector)
{
    var propertyName = propertySelector.GetReferencedPropertyName();
    return model.GetMetadataForType().Properties.Where(
        p => p.PropertyName == propertyName).Single();
}

Some notes on the code:

  • The GetReferencedPropertyName is another extension method I wrote based on this great blog. For now just know that it takes a lambda expression of the form x => x.MyProperty and returns the string “MyProperty.” ¬†You can find the code for this method here.
  • I attached the last two methods to the ViewDataDictionary which makes them accessible as methods on the view’s ViewData property. This is just a convenience and they could be attached to most any type.

This finally allowed me to rewrite my code as follows:

var formatForUnitCost = ViewData.GetMetadataForProperty(
    item => item.UnitCost).DisplayFormatString;
 
var formatForTotalCost = ViewData.GetMetadataForProperty(
    item => item.TotalCost).DisplayFormatString;

Conclusion

In conclusion, I am not 100% satisfied with my solution. For one, any time I start working around a framework rather than through it, I begin to wonder if I am approaching something wrong. Maybe there is some reason why I should not structure my model this way, or there is a way to structure a the model that avoids this problem all together. On the other hand, this approach has the advantage of providing access to metadata in other scenarios where it is not readily accessible, such as when using the ViewData collection in lieu of an actual model.

Another concern is that passing null as the first argument to ModelMetadataProvider.GetMetadataForType may not work for all providers. It works with the DataAnnotationsMetadataProvider, at least in its current implementation. Thus I feel that the last two extension methods may be unsafe. One can sidestep this concern by using only the first two methods, though they have the minor disadvantage of requiring an instance of the model.

Ultimately, I would prefer to see a solution that is baked into the MVC implementation and not just bolted on from the outside. I like doing things the “normal” way. In this case that means accessing the metadata via the already provided ModelMetadata graph. Doing things the normal way is usually better down the road. Those who have to maintain the code will find it easier to understand if it sticks to standard approaches. Perhaps more importantly, if Microsoft makes changes to how MVC generates metadata, they probably won’t break the documented functionality, but they might break my code.

So in the end, if anyone out there has a better solution, I would love to hear from you. Until then, I have my metadata.