Site Meter
 
 

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

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

  1. Pingback: Daily Bits - February 23, 2008 | Alvin Ashcraft's Daily Geek Bits

  2. Have you considered adding this to MVCContrib?

  3. Steve

    Ben, thanks for the suggestion – I’ve submitted it.

    I also changed the name FlexiViewPage to AutoTypeViewPage because it’s more descriptive.

  4. James Dunne

    ViewData as an IDictionary is obviously not a strongly-typed collection. You’re exposing a misleading view of implied strongly-typed collection behavior over an underlying implementation that is obviously not strongly-typed. This is syntactic sugar and convenience methods, nothing more. Please don’t falsely advertise.

  5. Tommy Skaue

    Sweet. Thanks for sharing, Steve! :-)

  6. Nick

    It seems to me the data container object is defined in the wrong place. The fact that you define it in the view means that the controller is dependent on the view, whereas it seems to me the dependency should go the other way – that is, downstream. In many senses this is similar to a DTO being passed in an SOA scenario. For that reason, I’d either define the data container object in the controller or in a location lateral to both the view and controller.

  7. Steve

    Nick, that’s an interesting point. The thing is, the controller is effectively dependent on the view whichever way round you do it. The controller is choosing to render a specific view for a specific purpose; it can hardly be agnostic of the view.

    I often think of a view as being like a ‘function call’ that the controller can make. It chooses to call a certain view, and that view requires a specific data structure (yes, like a DTO) to be passed as a parameter. So the DTO definition is really owned by the view more than it’s owned by the controller.

    You could define the DTO in your controller if you prefer. It just makes more sense to me for it to go with the view.

  8. Great! That is the thing we’ve been looking for. Thanks from russian students.

  9. I hate to say it but this smells a bit like MVP where the view sets up some plumbing for something (in this case the controller) to interact with it. I totally agree with the fact that the controller must be aware of the view…how else would it know how to call it? By defining this object in the view like you have I don’t think any rules are broken since the view still has no idea about the controller. And given that a controller could interact with a multitude of views it wouldn’t be clean to define these objects in the controller…you would have a library of items defined there (or could). I might consider stashing these objects into your presentation model somewhere in a similar directory structure to the controllers and views…might be cleaner and not piss off so many purists. I think this is a very clean way to sugar coat some current pains…call it syntactic candy or what have you. If it works…it works! Thanks.

  10. Denis

    MVC 2 gives some errors:
    –> protected override void SetViewData(ViewDataDictionary viewData)
    ‘Dib.Web.Models.AutoTypeViewPage.SetViewData(object)’: no suitable method found to override

    –> base.SetViewData(data);
    Argument 1: cannot convert from ‘TViewData’ to ‘System.Web.Mvc.ViewDataDictionary’

  11. I agree its going to MVP. Thats logical. When you have partials that are used on several places in your Solution you want some logic so they can be slightly different. Model stays the same, ViewData makes the difference. I have partials that are the same but have to be positioned on different places. I need to “control” the view so they can be positioned. I choose to “reuse” my partials in stead of making new partials for every difference but you do this with care because if to much “controlling” logic is needed in the view it feels not right. By the way Great Book Steve! I learnt a Lot of It :-)

  12. thanks for ideas, I’m working on a solution that’s strongly typed although I haven’t worked out how they do it yet! It’s quite large but this article is very useful..
    Thanks,

  13. I might consider stashing these objects into your presentation model somewhere in a similar directory structure to the controllers and views…might be cleaner and not piss off so many purists. I think this is a very clean way to sugar coat some current pains…call it syntactic candy or what have you. If it works…it works! Thanks.