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:
- 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):
<strong>Customer name: <%= ((Customer)ViewData["Customer"]).FullName %></strong>
- 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
<strong>RenderView("ShowCustomer", customerRecord)</strong>
), 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
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
When you derive a ViewPage from AutoTypeViewPage
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!