.Net Development28 Nov 2009 11:15 pm

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.

General28 Nov 2009 04:10 pm

Well I have finally found the time to begin a blog.  Actually, I am not sure it was so much about finding the time as finding the topics.  For the last five years now I have been working on a big legacy project.  It wasn’t the sort of place where you do not do a lot of interesting work.  It was more the kind of job you read about in the anti-patterns section of software development books.  Nonetheless there was enough stuff going on to provide ample learning opportunities.  It was a very good place to think and study and hopefully grow wise enough to know when I have something worth saying and when I don’t.  We shall see.

In any event, I have been writing software, in one form or another since I was eight years old.  My first program computed Ohms Law on the TRS-80.  (I was never one to settle for Hello World.)  My first “commercial” application was a subscription management and billing application for my paper route.  It was written in Commodore Basic on the venerable C64 and served me well for several years.

Professionally, I have been doing software development since 1993, starting out in FoxPro 2.0, then Visual Basic, and starting in 2004, C#.  I have done everything from DOS based UI design to windows API programing, COM services to Web Applications, Perl CGI to .Net MVC.

Things I have not done are OS and driver development, no Cobol nor main frame programming.  I have never done any deep math stuff like simulations or modeling.  Part of this is because my degree is in Philosophy not Computer Science.  But Philosophy is great for critical thinking and communication skills so its not been too much of a handicap.

Living in D.C. suburbia, I have generally worked on very large long-running federal projects where I often gravitate towards tackling the messy problems others don’t want to and towards trying to bring discipline to cowboy programming practices.  Early on, I would often be used as a firefighter, sent in to fix the crisis of the day.

What I have gotten good at over the years is object oriented development, writing flexible, reasonably clean, self-documenting code and the like.  I am keen on agile for both technical and non-technical reasons but don’t have a lot of formal experience in it.  I discovered agile several years back, and in the process realized that a lot of the tactics and tricks that I was using to make software development work in my waterfall world, and thought of as my shortcuts for not being able to do things the right way, were actually espoused practices in Agile.  It was sort of an awakening to the idea that I really did know what I was doing after all. In a sense, I have been doing gorilla agile for a long time without know it.

Very recently, I have been given the opportunity to lead a new team doing agile development on a non-government greenfield .Net application.  So this is my big chance and as such and I am very excited to see how it goes and pass on anything that I learn.  It is also my first chance to use .Net 3.5, various open source tools and the like so expect to see some blogging about those topics too.

« Previous Page