Multiple Views with MVC
In my previous two posts (one and two) discussing the use of AJAX within an ASP.NET MVC Framework application, I’ve tried to demonstrate some ways that the framework can be extended or modified. In keeping with this approach, I thought I’d show how it is possible to change the way that Views are resolved using a “View Factory”. Again, this is much more about the demonstration than the subject matter: i.e. AJAX is incidental to seeing how to use the MVC framework. Please read the previous posts if you want to follow the specific example I’m using here.
Usual disclaimers apply – and this is based on pre-release software that will no doubt change, and therefore this post will likely become outdated.
The key to this post, then, is that instead of using a single View that behaves differently according to whether or not AJAX is supported, I want to have two separate Views – one for AJAX, one for without.
To achieve this is pretty simple.
1. We need to create our own implementation of IViewFactory. This is responsible for locating and creating an instance of an IView (which both ViewPage and ViewUserControl implement).
2. To “inject” (all you DI fans excuse me borrowing the term without using a DI framework) our new View Factory into every Controller we are going to create our own IControllerFactory implementation.
3. We need to configure the framework to use our new Controller Factory.
4. Finally we can create two Views – an AJAX version and a pure HTML version.
Easy huh? Lets fly through the implementation of each of these...
View Factory
To minimise my effort I’ve sub-classed the WebFormViewFactory;
public class AjaxViewFactory : WebFormViewFactory
{
protected override IView CreateView(
ControllerContext controllerContext,
string viewName,
string masterName, object viewData)
{
string mode = controllerContext.RouteData.Values["mode"] as string;
IView view = null;
try
{
view = base.CreateView(controllerContext, viewName + "-" + mode,
masterName, viewData);
}
catch (InvalidOperationException)
{
if (view == null)
view = base.CreateView(controllerContext, viewName,
masterName, viewData);
else
throw;
}
return view;
}
}
Now I should imagine you’re all screaming now (and no, not because I don’t check “mode” isn’t null – this is demo code J). Truth is, I’d like the implementation of this to be different – perhaps by overriding WebFormViewFactory.GetTypeFromName, but some of the framework’s members are private and not virtual... hence my hack.
Basically all we do here is append the value of “mode” (which is “ajax” or “html”) to the end of the view name. So a request for a “List” view will return “List-ajax” or “List-html”. If we can’t find a View by this name, we fall back to the version without a suffix. [Important: The catching of exceptions in this way as effectively anticipated business logic is strongly not recommended – this is the ugly code I was referring to. This is hard to maintain and could easily become a performance hit if you have lots of Views without a suffix, so in production code I would put the effort in to handle this properly – I haven’t here simply to save space and complexity] . I then reuse the standard functionality of the WebFormViewFactory to do the hard work of creating an instance.
This should make it pretty obvious how you could change this behaviour to your heart’s content. There is no reason why you couldn’t completely remove any remnant of the WebFormViewFactory – as long as you return an instance of an IView you’re away!
Controller Factory
The Controller Factory is almost embarrassingly simple;
public class AjaxControllerFactory : IControllerFactory
{
public IController CreateController(RequestContext context,
Type controllerType)
{
Controller controller =
(Controller)Activator.CreateInstance(controllerType);
controller.ViewFactory = new AjaxViewFactory();
return controller;
}
}
All we do here is create a new instance of our Controller, and make sure we set the View Factory to be an instance of our new class. The key to getting our new Controller Factory to be used instead of the default factory is a single call in the Application_Start event of Global.asax;
ControllerBuilder.Current.SetDefaultControllerFactory(
typeof(AjaxControllerFactory));
And that’s it.
Views
To get my Views working I took the existing “People.aspx” View, and copied it twice to “People-ajax.aspx” and “People-html.aspx”. I then removed the if statement below;
<% if (ViewData.EnableAjax)
{ %>
<%= Ajax.UpdateRegionLink<AjaxSampleController>(d =>
d.UpdatePerson(emp.Id), "Individual", emp.Name) %>
<%}
else
{ %>
<%= Html.ActionLink(emp.Name, new { action = "ViewPeople",
id = emp.Id })%>
<% } %>
... and instead left the relevant section in each View – i.e. the Ajax call in the “–ajax” view, and Html call in “-html” view.
Round-up
Well folks, that’s all. I hope that’s a pretty clear demonstration of how easy it is to plug into the MVC pipeline. If you’re interested in IViewFactory specifically, head on over to MVCContrib – looks like some View Factories are in the code base.
Comments
Anonymous
February 05, 2008
Cool! I also wrote about something just like this, except I used built-in serialization to return data as JSON for AJAX calls. http://www.aaronlerch.com/blog/2008/01/01/unifying-web-sites-and-web-services-with-the-aspnet-mvc-framework/Anonymous
February 05, 2008
Aaron - nice post! SimonAnonymous
October 25, 2010
Well, after so many years using ASP.Net, I’m still trying to figure out how to achieve the same results using MVC. For example, starting from the Basic Default MVC app that is provided with VS 2010, let’s imagine I want to change the “LogonUserControl.ascx” to tell me who is the current logged user (as it works currently) OR allow me to login from there, showing me the text boxes for username and password (whithout having to navigate to another LogonView). Pretty much, I want to be able to login from the home page, so I take the control and STRONGLY TYPE it as: ... <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Gioby.Models.LogOnModel>" %> Then change the code as: ... <% if (Request.IsAuthenticated) { %> Welcome <b><%: Page.User.Identity.Name %></b> [ <%: Html.ActionLink("Log Off", "LogOff", "Account")%> ] <% } else { %> <% using (Html.BeginForm()) { %> <div id="logon"> <div class="editor-label"> <%: Html.LabelFor(m => m.UserName)%> <%: Html.TextBoxFor(m => m.UserName)%> <%: Html.ValidationMessageFor(m => m.UserName, "") %> <%: Html.LabelFor(m => m.Password)%> <%: Html.PasswordFor(m => m.Password)%> <%: Html.ValidationMessageFor(m => m.Password, "") %> <input type="submit" value="Log On" /> </div> <div class="editor-label"> <%: Html.ActionLink("Register here", "Register", "Account")%> <%: Html.CheckBoxFor(m => m.RememberMe, new { @class = "pad-left" })%> <%: Html.LabelFor(m => m.RememberMe) %> </div> </div> <% } %> <% } %> On the HomeController, I add a procedure as [HttpPost] public ActionResult Index(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { Check Login against your DB if (!String.IsNullOrEmpty(returnUrl)) return Redirect(returnUrl); return RedirectToAction("Index", "Home"); } // If we got this far, something failed, redisplay form return View(model); } I test it and from the home page … it works !!! BUT ...when I navigate to let's say the “Register” view, since the “LogonUserControl.ascx” is located inside the “MasterPage” when I click on the Register button, I get this error: "The model item passed into the dictionary is of type Site.Models.RegisterModel', but this dictionary requires a model item of type Site.Models.LogOnModel'." ???? Does that mean that I will never be able to put different pieces together into one view? Let’s say I want to write an eCommerce site and on the home page I want to see “Most used Tags”, “Most bought products”, “Product of the Month”, “List of Categories” each one with their own POST BACK button …all within the same View …is this possible using MVC?Anonymous
October 26, 2010
@ Filu, You should use the arguments to Html.BeginForm that specify the precise Controller & Action that you want to post the logon details to. Remember if you want to encapsulate logic as well as markup using Html.RenderAction is arguably better than RenderPartial... so then you'd have the "if user is logged in" in your controller, and two simpler views (a logged in view, and a "please log in" view). HTH Simon