ASP.NET MVC: Strongly Typed MasterViewPage
I am a big advocate of the viewmodel and it’s application not only in ASP.NET MVC applications but also in Silverlight, WPF, and even WinForm applications. I always find using the technique of a viewmodel really get’s you thinking about how to structure your entire application. The issue most ASP.NET MVC developers face is the following scenario. We have a site that has recurring functionality on each page; for example, we have a twitter roll which displays all our latest twitters. We would like this to appear on each page, but we don’t want to repeat ourselves in code (usually by copying and pasting code, blech!). So how do we accomplish our goal? An ASP.NET article suggests that you have a base master controller with recurring functionality. I agree with this approach, but I don’t like stuffing objects into ViewData. “Khalid, what is it about ViewData that you are so against?” Let me explain.
ViewData is a string based dictionary, which is dangerous on two levels. From a development standpoint, a dictionary can easily become a dumping ground for data. Before you know it, you have every aspect of your application dependent on values in ViewData. This inherently is bad because to retrieve a value, you need to know the key. This can slow down development for large teams or teams with less than ideal communication. Secondly, accessing a dictionary in the view can lead to really dirty views. I’m sure there are more arguments against and even for ViewData, but as a rule of thumb I try and avoid using this grab bag approach.
Solution
As I mentioned about the referenced ASP.NET article, I agree with the idea of a master controller. It makes sense that recurring functionality would be inherited into new controllers. To put this solution together, let us think about what specifically we need to reach our goal.
- We need a base controller with repeating functionality
- We need an inheriting controller
- We need a viewmodel for the MasterPage
- We need a viewmodel for the ViewPage
- The MasterPage viewmodel doesn’t need to know about the ViewPage viewmodel
Stating and believing these assumptions gives us a clear path to go down to accomplish our goal of a strongly typed MasterPage/
Addressing Assumptions 1 through 5
We are going to create a base controller that passes a master viewmodel up to the view. In addition to passing the master viewmodel, this base controller has to pass the view’s viewmodel. So we have two models that need to be merged or passed as a pair up to the view. My approach was to create two wrapper classes that will encapsulate the developers viewmodels. I felt this approach would let me reuse this functionality in future projects.
public class ViewModelForMasterWrapper<TMasterModel>
{
public ViewModelForMasterWrapper(TMasterModel masterModel)
{
Master = masterModel;
}
/// <summary>
/// Gets or sets the master model.
/// </summary>
/// <value>The master model.</value>
public TMasterModel Master { get; set; }
}
public class ViewModelForViewWrapper<TMasterModel, TViewModel>
: ViewModelForMasterWrapper<TMasterModel>
{
public ViewModelForViewWrapper(TMasterModel masterModel, TViewModel viewModel)
:base(masterModel)
{
View = viewModel;
}
/// <summary>
/// Gets or sets the view model.
/// </summary>
/// <value>The view model.</value>
public TViewModel View { get; set; }
}
You’ll see that the wrapper for the View inherits from the Master wrapper. This is necessary as you’ll see later. Now we need to populate these wrappers with the viewmodels that our application will need. I created a base master controller called BaseMasterController (really creative). The controller will create the ViewModelForViewWrapper and populate the View property and the Master property on the wrapper instance. You’ll see this class below.
public abstract class BaseMasterController<TMasterViewModel>: Controller
{
/// <summary>
/// Views this instance.
/// </summary>
/// <returns></returns>
protected virtual new ActionResult View()
{
return View(ViewData.Model);
}
/// <summary>
/// Views the specified model.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
protected virtual new ActionResult View(object model)
{
var masterModel = GetMasterViewModel();
object wrapper = CreateModel(model, masterModel);
return base.View(wrapper);
}
/// <summary>
/// Gets the master view model.
/// override this in your master controller.
/// </summary>
/// <returns></returns>
protected virtual TMasterViewModel GetMasterViewModel()
{
return default(TMasterViewModel);
}
/// <summary>
/// Creates the model.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="masterModel">The master model.</param>
/// <returns></returns>
private static object CreateModel(object model, TMasterViewModel masterModel)
{
var modelType = typeof (object);
if (model != null)
modelType = model.GetType();
var types = new[] { typeof(TMasterViewModel), modelType };
Type generic = typeof (ViewModelForViewWrapper<,>).MakeGenericType(types);
return Activator.CreateInstance(generic, masterModel, model);
}
}
This is an abstract base class. The functionality here is generic so it can be reused in multiple different projects. We now have the majority of our solution, but we need to put it together. To finish this solution we need to create business functionality. We’ll need our own master controller, our own controller, our own master viewmodel, and our own viewmodel. Here are our classes.
public class HomeIndexViewModel
{
public string MessageFromHome { get; set; }
}
public class MasterViewModel
{
public string MessageFromMaster { get; set; }
}
public class MyMasterController : BaseMasterController<MasterViewModel>
{
protected override MasterViewModel GetMasterViewModel()
{
return new MasterViewModel {MessageFromMaster = "Hello from the master controller."};
}
}
[HandleError]
public class HomeController : MyMasterController
{
public ActionResult Index()
{
var model = new HomeIndexViewModel {MessageFromHome = "Hello from the Home controller's Index."};
return View(model);
}
}
After reusing the classes from above, you’ll see that the implementing the actual business functionality is trivial. No more than ten lines (granted this is a simple example). Now, before we can use these we have to strongly type our MasterPage and Views. Let’s take a look.
Typing Our Views
Here you will see why the view wrapper inherits from the master view wrapper. The declarations for the MasterPage and the View will look like the following.
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage<ViewModelForMasterWrapper<MasterViewModel>>" %> <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ViewModelForViewWrapper<MasterViewModel,HomeIndexViewModel>>" %>
If the ViewModelForViewWrapper class was not an instance of the ViewModelForMasterWrapper class we would get runtime errors when the viewmodel is passed to the view. Now all we have to do is use the models in both our MasterPage and View.
Our MasterPage looks like the following.
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage<ViewModelForMasterWrapper<MasterViewModel>>" %>
<%@ Import Namespace="AquaBird.StrongTypeMasterPage.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="page">
<div id="header">
<div id="title">
<h1>Strongly Typed Master Page View</h1>
</div>
<div id="main">
<p id="master"><%=Model.Master.MessageFromMaster %></p>
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
<div id="footer">
</div>
</div>
</div>
</body>
</html>
And our View.
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ViewModelForViewWrapper<MasterViewModel,HomeIndexViewModel>>" %>
<%@ Import Namespace="AquaBird.StrongTypeMasterPage.Models" %>
<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
Home Page
</asp:Content>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
<p id="view">
<%= Model.View.MessageFromHome %>
</p>
</asp:Content>
After running the solution, my final output looks like this.
Conclusion
This solution is only one way of strongly typing your master pages and it surely works well for me. This solution doesn’t fully address the issue of widgetized pages. Complexly composed pages take a lot of time and effort design and are unique to each project. If this solution meets your needs the that is great, but always evaluate solutions to make sure they work for you. Download the solution and see how it will work in your project. Hope this helps you in your ASP.NET MVC project.


December 9, 2009 - 3:41 pm
My concern here is: what if I want to pass data to the Master page without instantiating specific ViewModel, just using the default ViewModel.
December 9, 2009 - 4:00 pm
I don’t know what you mean by specific ViewModel and default ViewModel? Could you give me a scenario.
If I can interpret your concern correctly, you still have access to ViewData and TempData, but that’s gross. And if you want a controller to have complete control over a specific view, then don’t inherit from the BaseMasterController. This setup is specifically for scenarios where you have repetitive code on an otherwise static master page.
i.e. Latest Tweets, Latest News, and other stuff like that.
January 27, 2010 - 7:25 pm
It is grateful for the help in this question how I can thank you?
May 26, 2010 - 11:20 am
Really really great article. Thanks for all the help. This is by far the best way to do it.
Just wish I’d seen this before I started as I’m going to have to go back through all my views and update references
May 26, 2010 - 1:01 pm
Quick question as I’ve come across a problem with this:
If I have a strongly typed Insert action for some ViewModel -
public ActionResult Insert(ViewModel model)
- the default binder is not working with the Html.TextBoxFor methods as they now render “input” s with names beginning View.PropertyX
Any ideas??
May 26, 2010 - 8:41 pm
Hmm… sounds like an interesting problem. I think there might be a way to override your modelbinder to account for the extra prefix or I think you can specify Prefix on your Action’s parameter.
July 15, 2010 - 5:36 am
Hi,
I seem to be having trouble when I am using another method on my controller that returns the same view. I have some tabs on my page, when the user clicks one it simply returns the view with a list of data re-ordered.
e.g.
[HandleError]
public class HomeController : MyMasterController
{
public ActionResult Index()
{
var model = new HomeIndexViewModel {List = service.GetItems() };
return View(model);
}
public ActionResult Recent()
{
var model = new HomeIndexViewModel {List = service.GetRecentItems() };
return View("Index", model);
}
}
Any assistance would be much appreciated.
July 15, 2010 - 6:10 am
Hi,
I figured it out by adding a new virtual method in the BaseMasterController:
///
/// Views this instance.
///
///
protected virtual new ActionResult View(String view, object model)
{
var masterModel = GetMasterViewModel();
object wrapper = CreateModel(model, masterModel);
return base.View(view, wrapper);
}
July 15, 2010 - 8:24 am
Oh cool, another solution would be to name your method something different and create a route to direct to the new method.
July 22, 2010 - 6:09 pm
Hi Khalid!
Thanks a lot for this really cool implementation. I’m using it on my own ASP.NET MVC 2 web application and it works perfectly. However I’m running into a problem…
I’m trying to implement the built-in Account Models for logging in, registering and changing password. They directly access the LogOnModel, however I obviously have this masterpage wrapper implemented that asks for 2 ViewModels. I tried the following on my LogOn.aspx:
Inherits=”System.Web.Mvc.ViewPage<ViewModelForViewWrapper>”
However, this yields the following error:
CS1061: ‘Patio20.ViewModels.ViewModelForViewWrapper’ does not contain a definition for ‘UserName’ and no extension method ‘UserName’ accepting a first argument of type ‘Patio20.ViewModels.ViewModelForViewWrapper’ could be found (are you missing a using directive or an assembly reference?)
Any ideas how I can resolve this? I’m a little bit lost with this unfortunately :/
July 22, 2010 - 10:18 pm
by default, Asp.Net MVC will pass the current model into what ever control you render partial on. I think all you need to do is pass in a model of LogOnModel to your RenderPartial. You could also write an extension method called “Html.RenderLogOn()” which would do this seemlessly so you don’t have something like “Html.RenderPartial(new LogOnModel())” in your master page. Hope this helps.