Fun with IView and IViewEngine
I’m currently working on a little side project that I’m kind of excited about. I’m building an MVC based forum that can be plugged into any MVC application with very little effort. In fact, it looks like most scenarios will only require adding a reference to a DLL and one line of code in Application_Start (and maybe an html link in a menu somewhere). It’s not quite ready for the public yet, but there are few things I’m really proud of that I’d like to share.
In order to keep setup/configuration as simple as possible I wanted to find a way to provide a default UI out of the box. The question was “how do I render views when I don’t actually have an .aspx file on disk?” Even more important, most sites use master pages. How do I get my html to render in someone else’s master page? After some googling, reflector-ing, and cursing, I finally came up with this little class
public class DynamicView : IView
{
public string PrimaryContentPlaceHolderId { get; set; }
public string TitleContentPlaceHolderId { get; set; }
public string MasterLocation { get; set; }
public string DefaultTitle { get; set; }
public ViewUserControl Control { get; set; }
public void Render(ViewContext viewContext, TextWriter writer)
{
DynamicViewPage viewPage = new DynamicViewPage();
viewPage.AppRelativeVirtualPath = "/";
viewPage.MasterLocation = MasterLocation;
viewPage.ViewData = viewContext.ViewData;
if (TitleContentPlaceHolderId != null)
{
viewPage.AddContentControl(TitleContentPlaceHolderId,
(w, p) => w.Write(DefaultTitle));
}
viewPage.AddContentControl(PrimaryContentPlaceHolderId,
(w, p) => RenderControl(viewPage.Html, Control));
viewPage.RenderView(viewContext);
}
public void RenderControl(HtmlHelper html,
ViewUserControl control)
{
control.ViewData = html.ViewData;
control.RenderView(html.ViewContext);
}
private class DynamicViewPage : ViewPage
{
public void AddContentControl(string contentPlaceHolderId,
RenderMethod renderMethod)
{
CompiledTemplateBuilder compiledTemplateBuilder =
new CompiledTemplateBuilder(
x => x.SetRenderMethodDelegate(renderMethod));
AddContentTemplate(contentPlaceHolderId,
compiledTemplateBuilder);
}
}
}
(NOTE: For the sake of simplicity I have removed some code from the original file that didn’t apply to this blog post. I have not tried compiling the code as posted here).
This class looks a little crazy, but it’s actually pretty straight forward. You provide it with the path to a master page, the name of the “title” and “main” content areas on that master page, a title for the page, and a ViewUserControl that should be rendered in the main content area. The DynamicView class implements IView which allows the MVC framework to do its magic. And that’s it! Well, almost… there is one other little detail.
For my forum project, I’ve created a controller, actions, and associated routing rules. When a request is made for, say “http://yoursite/forum”, the request will be handed to my controller as expected. My controller action tells the MVC framework it should render a view by the name of “Index”, but how do I get the MVC framework to find the correct dynamic view? Thankfully, the MVC framework has an IViewEngine interface that can be used to solve the problem.
The MVC framework ships with a default IViewEngine that maps views to .aspx files in the “Views” directory. However, you can actually create and register additional view engines. When multiple view engines are registered, if the first engine is unable to find a valid view for the current request it hands off processing to the next view engine. Armed with that knowledge I created the following DynamicViewEngine class (NOTE: some of this code is specific to my “forum” project, but I think you’ll get the idea).
public class DynamicViewEngine : IViewEngine
{
private string _masterPageLocation;
private string _primaryContentPlaceHolderId;
private string _titleContentPlaceHolderId;
public DynamicViewEngine(string masterPageLocation,
string primaryContentPlaceHolderId,
string titleContentPlaceHolderId)
{
_masterPageLocation = masterPageLocation;
_primaryContentPlaceHolderId = primaryContentPlaceHolderId;
_titleContentPlaceHolderId = titleContentPlaceHolderId;
}
public ViewEngineResult FindPartialView(
ControllerContext controllerContext,
string partialViewName,
bool useCache)
{
return new ViewEngineResult(
new string[] { "Dynamic Forum Views" });
}
public ViewEngineResult FindView(
ControllerContext controllerContext,
string viewName,
string masterName,
bool useCache)
{
object controller = controllerContext.RouteData.Values["controller"];
if (controller.ToString().ToLower() != "forum")
{
return new ViewEngineResult(
new string[] { "Dynamic Forum Views" });
}
DynamicView view = new DynamicView();
view.MasterLocation = _masterPageLocation;
view.PrimaryContentPlaceHolderId = _primaryContentPlaceHolderId;
view.TitleContentPlaceHolderId = _titleContentPlaceHolderId;
switch (viewName.ToLower())
{
case "index":
view.Control = new IndexControl();
view.DefaultTitle = "Forum";
break;
case "view":
view.Control = new ViewControl();
view.DefaultTitle = "Forum Post";
break;
case "post":
view.Control = new PostControl();
view.DefaultTitle = "New Post";
break;
case "reply":
view.Control = new ReplyControl();
view.DefaultTitle = "Forum Reply";
break;
default:
return new ViewEngineResult(
new string[] { "Dynamic Forum Views" });
}
return new ViewEngineResult(view, this);
}
public void ReleaseView(
ControllerContext controllerContext,
IView view)
{
IDisposable disposable = view as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
The interesting method on this class is “FindView”. This is where I determine if the requested view is one of my forum views. If it is, I return an instance of my DynamicView with an appropriate user control being rendered in the master page’s “main” content area. (Not shown here are the various user controls. The user controls are simply sub classes of ViewUserControl with html manually written out by overriding the “Render” method).
The DynamicViewEngine can now be registered with one simple line of code, like this…
ViewEngines.Engines.Add(new DynamicViewEngine("~/Views/Shared/Site.Master", "MainContent", "TitleContent"));
So now with two fairly compact classes I’ve accomplished my goal of being able to distribute views in a DLL that can integrate into any existing application. There’s no requirement for developers to create multiple pages in order to host the forum. There aren’t a ton of files that need to be copied to specific directories. It really is as simple as adding a reference to one DLL and calling an initialization method.
But wait, there’s more! There’s one other really nice (and unintended) side effect of this architecture. Let’s say that a developer decides to use the forum, but wants to change the html of one of the pages. It turns out this is extremely easy. Simply create a “Forum” directory under the “Views” directory in the web project and then create an .aspx page for the view you wish to override (the same way you would create a view for any standard MVC application). Now the default view engine will render the custom view instead of handing control over to the DynamicViewEngine. The best part is that the default controller still handles all the “logic” of the request. All the developer needs to do is provide the html template. This highly pluggable application that is both easy to get up and running as well as simple to extended and customize.
I’m hoping to have my forum ready for people to check out soon, but until then I hope this little snippet gives you some new ideas of how you can use the MVC framework to create easily deployable components/modules.
August 11th, 2009 at 10:43 am
Hello, nice work. What I am trying to do is the following:
- Pages are dynamic and created programmatically
PageID MasterPageLocation
—————————–
1 “~/app_master/default.master”
next I have list of partial views like this
ViewID ContentPlaceHolderID PageID Path
———————————————
1 “LeftBar” 1 “~/some.ascx”
2 “MainContent 1 “~/other.ascx”
Maybe good thing to think about since most of CMS systems requires such approach.
August 11th, 2009 at 12:12 pm
@Andrej - I don’t think it would be too hard to make something like that work. Try adding another ViewUserControl property to DynamicView (named something like “LeftBar”) and use viewPage.AddContentControl to render it in the desired content area.
August 12th, 2009 at 1:32 pm
hey,
I love this concept & I was hoping to use it in one of my projects. But seems like the Render method of the ViewUserControl is not getting called, is something missing in your code sample?
thanks
August 12th, 2009 at 1:39 pm
In continuation - I am missing the method:
AddContentTemplate(contentPlaceHolderId,
compiledTemplateBuilder);
seems like some thing is missing
thanks
August 12th, 2009 at 1:56 pm
@Sunny - AddContentTemplate is a function on the private class “DynamicViewPage”. Are you using DynamicViewPage (instead of ViewPage) in DynamicView.Render?
August 12th, 2009 at 2:55 pm
well, I have the code exactly as shown here.
The Render method of my view class does not get called, so even though I can see the master page & layout applied to the rendered output, I am not able to see my view.
I dont know what code needs to go into the AddContentTemplate method.
Thanks for your reply!
August 12th, 2009 at 3:34 pm
If DynamicView.Render is not being called, it sounds like the request is not being handled by the DynamicViewEngine. Is DynamicViewEngine.FindView being called? If so, is it returning a ViewEngineResult with an instance of DynamicView?
AddContentTemplate is a function on DynamicViewPage and it handles rendering content to the correct content area on the master page. The source code for AddContentTemplate is included in the original post. You shouldn’t need to deal with it.
Let me know if you’re still having trouble.
August 13th, 2009 at 8:09 am
Alrighty - something weired with my VS2008 intellisense due to which I could not get to the method… but typing it in solved the problem & the code works, odd but true!
Thanks for this concept & all the help!
August 13th, 2009 at 8:34 am
Great! I’m glad it’s working for you now.