A while ago I took a lull at work as an excuse to muck around with the then new (ish) ASP Mvc Framework. I'd read alot about it, investigated it for a bit, borrowed ideas from it for existing projects, and been disapointed when it arrived just too late to be adopted for a major project I became embroiled in. To me the Mvc Framework was the magic bullet I was praying for the slay the evils of Franken-Postback and Count Business-Logic-In-Gui-Code-ula. It worked; it was easy; it was peasy; it was lemon-squeezey. Everything was going great until I tried to tie masterpages to my viewdata.
The situation was this: I had a masterpage that I wanted to display data controlled by a simple CMS (for CMS read web.config file - this was a prototype afterall), and I wanted said data to be fetched using the same Mvc pattern that data bound in regular pages is; in short I wanted my controllers to get data that the masterpage would use.
As always my good friend google was the first port of call and while I found several good solutions none were as neat as I'd like. The best solution (in my mind) was offered by Mike Linnen, he basically proposed defining a base Viewdata class which contained the masterpage data - all concrete viewdata would inherit from this. This solution defined a controller that fetched the base Viewdata and would be called from every other controller. I liked this solution but it had two things that made me seek another way:
- You had to explicitly call the base controller from your concrete controller.
- If you wanted to have a page that rendered say a simple IList<string> then you'd have to define a specific class for that purpose.
My Way My way invloves defining our very own Viewdata class; said Viewdata class is setup to contain our masterpage data as well as any other data required for a subpage. A base controller (from which all controllers should inherit) then ensures that this base Viewdata class is used at all times. Rather than try and explain I'll post some code.
BaseViewdata.cs/// <summary> /// Untyped base view data class - for use in the masterpage /// </summary> public class BaseViewdata { #region Properties /// <summary> /// The Strapline for the page. /// </summary> public virtual string Strapline { get; set; } #endregion } /// <summary> /// Typed base view data class - for use in pages. /// </summary> public class BaseViewdata<T> : BaseViewdata { #region Properties /// <summary> /// The typed data contained by the view data. /// </summary> public virtual T Data { get; set; } #endregion }
The BaseViewdata class comes in two flavours: typed, and untyped. We have both because when coding the masterpage we won't know what kind of BaseViewdata we're using - whether it is BaseViewdata or BaseViewdata> etc - for this reason, and the fact that the masterpage shouldn't care about the page specific data, there is an untyped varient. The untyped BaseViewdata will contain all the masterpage specific properties - in this case the strapline text to display at the top of the page.
The typed viewdata simply acts as a wrapper around the data to be used by the page view.
BaseController.cs/// <summary> /// Base controller for the site. /// </summary> /// <remarks> /// This class acts as a base controller for all views that uses the Site.Master masterpage. /// </remarks> public abstract class BaseController : Controller { #region Fields /// <summary> /// Object used for synchronisation. /// </summary> private readonly object syncObj = new object(); /// <summary> /// The base viewdata for the site. /// </summary> private BaseViewdata baseData; #endregion #region Properties /// <summary> /// The base viewdata for the site. /// </summary> public BaseViewdata BaseData { get { if (this.baseData == null) { lock (this.syncObj) { if (this.baseData == null) { PopulateBaseData(); } } } return this.baseData; } } #endregion #region Methods /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <typeparam name="T"> /// The type of data contained in the viewdata. /// </typeparam> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected virtual ActionResult View<T>(T data) { ActionResult result; var viewData = CreateViewdata(data); result = base.View(viewData); return result; } /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <typeparam name="T"> /// The type of data contained in the viewdata. /// </typeparam> /// <param name="viewName"> /// The name of the view to render. /// </param> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected ActionResult View<T>( string viewName, T data) { ActionResult result; var viewData = CreateViewdata(data); result = base.View( viewName, viewData); return result; } /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <typeparam name="T"> /// The type of data contained in the viewData. /// </typeparam> /// <param name="viewName"> /// The name of the view to render. /// </param> /// <param name="masterName"> /// The name of the view master. /// </param> /// <param name="data"> /// The data contained in the viewData. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected ActionResult View<T>( string viewName, string masterName, T data) { ActionResult result; var viewData = CreateViewdata(data); result = base.View( viewName, masterName, viewData); return result; } /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected new ActionResult View(object data) { return this.View<object>(data); } /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <param name="viewName"> /// The name of the view to render. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected new ActionResult View( string viewName, object data) { return this.View<object>( viewName, data); } /// <summary> /// Returns a System.Web.Mvc.ViewResult that renders a view to the response. /// </summary> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <param name="viewName"> /// The name of the view to render. /// </param> /// <param name="masterName"> /// The name of the view master. /// </param> /// <returns> /// The ViewResult that is to be rendered as a view. /// </returns> protected new ActionResult View( string viewName, string masterName, object data) { return this.View<object>( viewName, masterName, data); } #endregion #region Private methods /// <summary> /// Populate the base viewdata for the site. /// </summary> private void PopulateBaseData() { this.baseData = new BaseViewData() { Strapline = "The Snapping Turtles Are Massing!" }; } /// <summary> /// Creates the viewdata that will be passed to the page. /// </summary> /// <remarks> /// Pulls in all non-generic values from the base viewdata. /// </remarks> /// <typeparam name="T"> /// The type of data contained in the viewdata. /// </typeparam> /// <param name="data"> /// The data contained in the viewdata. /// </param> /// <returns> /// The created viewdata. /// </returns> private BaseViewdata<T> CreateViewdata<T>(T data) { BaseViewdata<T> result; result = new BaseViewdata<T>() { Data = data, Strapline = this.BaseData.Strapline, }; return result; } #endregion }
The BaseController basically works by defining generic versions of the existing View methods and shadowing (we can't override them unfortunately) the existing versions. The new methods work in exactly the same way as the old methods but with one added step: they take the data provided and copy it into a BaseViewdata instance which is then passed to the original View method - ie: we're wrapping the data using our BaseViewdata class.
One thing to point out is that, in this code. the BaseViewdata is created when needed, therefore it is available to the controller at any point allowing you to override settings if and when needed.
Putting these two fellas into action we can create a page/masterpage combo that might look a bit like this:
Site.Master<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage<BaseViewdata>" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>About a year too late Andy!</title> </head> <body> <div class="strapline"> <%= this.ViewData.Model.Strapline %> </div> <asp:ContentPlaceHolder ID="MainContent" runat="server"> </asp:ContentPlaceHolder> </body> </html>Index.aspx
<%@ Page Title="" Language="C#" MasterPageFile="Site.Master" Inherits="System.Web.Mvc.ViewPage<BaseViewdata<string>>" %> <asp:Content ID="Content1" ContentPlaceHolderID="Content" runat="server"> Your Viewdata contains the string <%= this.ViewData.Model.Data %>! </asp:Content>Hooray!
And there you go, masterpages with viewdata in a scaleable solution - all you have to do is make sure your controllers inherit from the BaseController and you'll be fine. The only things about this solution that I find distastefull are:
- You have to shadow methods rather than overriding them, but this can't be helped and it only affects untyped data anyway.
- Your code-behind now contains the rather ugly System.Web.Mvc.ViewPage<BaseViewdata<string>>. If you really want to make this neater then you can always subclass the ViewPage class so that the BaseViewData part becomes implied/forced.
I realise that the code samples are quite large in this post but I like to post real code rather than psuedocode that illustrates only an idea.
ReplyDeleteDo you think in future I should omit comments/other extraneous data or continue with the full verbose code?