Site Meter
 
 

Monthly Archives: February 2008

ASP.NET MVC: Making strongly-typed ViewPages more easily

When I first started using ASP.NET MVC, I had a problem with ViewData. It seemed like a bit of a pain, because you have two options:

1. Treat it as a loosely-typed dictionary, in which case you get no help from intellisense and have to keep writing typecasts all over your views (which is error-prone and ugly):

Customer name: <%= ((Customer)ViewData["Customer"]).FullName %>

2. Treat it as a specific strongly-typed object – but which type? It’s all very well to pass in one of my domain objects (as in RenderView("ShowCustomer", customerRecord)), but the next minute I want to add a status message or something, and the model breaks down.

Jeffrey Palermo came up with a great idea called SmartBag: it solves the problem – at least partially – by supplying an interface like ViewData.Get<CatOwner>() (which returns the first CatOwner object in the collection). This certainly reduces the scope for typos and the need for typecasts. Still, I wasn’t satisfied, because it gets awkward when you have more than one object of a given type (you have to revert to using string keys).

A helpful convention

Fairly soon, I settled down on a convention that seems effective (so far). In the code-behind file for every ViewPage, right at the top, I define a very simple data container object specific to that ViewPage:

public class ShowCatViewData
{
    public string Name { get; set; }
    public int Age { get; set; }
    public bool HasFunnyFace { get; set; }
    public CatOwner Owner { get; set; }
}
 
public partial class ShowCat : ViewPage<showCatViewData>
{
}

… and then of course render the view like this:

[ControllerAction]
public void Index()
{
    RenderView("ShowCat", new ShowCatViewData {
        Name = "Moo-moo",
        Age = 6,
        HasFunnyFace = true
    });
}

Now we have strongly-typed ViewData so we get intellisense and no need for typecasts. Because the ShowCatViewData class is specific to the ShowCat view, I don’t mind adding in extra fields (e.g. for status messages or whatever) whenever they’re needed. It doesn’t interfere with my database model.

But what if my controller prefers loosely-typed dictionaries?

Admittedly, sometimes it seems nice to use the dictionary syntax to construct ViewData. You can add fields incrementally, and the controller doesn’t need to know about any particular .NET class specific to the view, like this:

[ControllerAction]
public void Index()
{
    ViewData["Age"] = 25;
    ViewData["HasFunnyFace"] = false;
    if (nameIsKnown)
        ViewData["Name"] = "Frankie";
    RenderView("ShowCat");
}

… but you can’t do that if the ViewPage demands a strongly-typed ViewData object, right?

Introducing AutoTypeViewPage<T>

When you derive a ViewPage from AutoTypeViewPage<T>, your ViewPage suddenly gets a little bit smarter.

Just change this:

public partial class ShowCat : ViewPage<showCatViewData> { ... }

… to this:

public partial class ShowCat : AutoTypeViewPage<showCatViewData> { ... }

and now you can use any of following three syntaxes to send the ViewData, and you’ll get the exact same strongly-typed ViewData in the ASPX page:

Syntax #1: Old-school dictionary

ViewData["Name"] = "Moo-moo";
ViewData["Age"] = 6;
ViewData["HasFunnyFace"] = true;
RenderView("ShowCat");

Syntax #2: Explicitly-typed ViewData object

RenderView("ShowCat", new ShowCatViewData {
    Name = "Moo-moo",
    Age = 6,
    HasFunnyFace = true
});

Syntax #3: Anonymously-typed object

RenderView("ShowCat", new {
    Name = "Moo-moo",
    Age = 6,
    HasFunnyFace = true
});

How interesting! Show me the code

OK, chill out. It’s very simple:

public class AutoTypeViewPage<tviewData> : ViewPage<tviewData>
{
    protected override void SetViewData(object viewData)
    {
        if ((viewData == null) || (typeof(TViewData).IsAssignableFrom(viewData.GetType())))
            // The incoming object is already of the right type
            base.SetViewData(viewData);
        else
        {
            // Convert the incoming object to a dictionary, if it isn't one already
            IDictionary suppliedProps = viewData as IDictionary;
            if (suppliedProps == null)
                suppliedProps = viewData.GetType().GetProperties()
                                .ToDictionary(pi => pi.Name, pi => pi.GetValue(viewData, null));
            // Construct a TViewData object, taking values from suppliedProps where available
            TViewData data = Activator.CreateInstance<tviewData>();
            foreach (PropertyInfo allowedProp in typeof(TViewData).GetProperties())
                if (suppliedProps.Contains(allowedProp.Name))
                    allowedProp.SetValue(data, suppliedProps[allowedProp.Name], null);
            base.SetViewData(data);
        }
    }
}

Final notes

Apparently MonoRail has something vaguely related: DictionaryAdapter. It’s a bit different because what that does is take a pure interface and create a basic implementation via run-time code gen, so you don’t need a concrete implementation of the ViewData class – just an interface for it. If you were clever, you could combine that with the AutoTypeViewPage technique so that arbitrary incoming objects were converted to IMyInterface.

If you have any feedback, you know where to post!

kick it on DotNetKicks.com