There's been some recent chatter about mixing ASP.NET MVC with WebForms, which is something I'm all too familiar with. This setup works just fine, but what if you wanted to use the new Razor view engine within ASP.NET MVC? A bit of backstory.
On a secondary project that I work on within Microsoft, we were given the ability to rewrite the entire UI of the application in ASP.NET MVC. At that time, the first beta of MVC3 was rolling out and we decided to take our first plunge and use the Razor view engine. Unfortunately, the common UI layout that is shared across multiple properties that we had to use was WebForms based. Our attempts to convince the developers of the common UI components to support more of a modularized infrastructure failed for any foreseeable future, so we were left with the conundrum of figuring out an intermediary solution.
After discussions with the ASP.NET team to see if using a Razor view where it's layout was a WebForms master page was or could be a supported scenario - the general consensus being "No". The primary reason is that WebForms and Razor pages have a completely different architecture and runtime execution model (renders from a control tree vs. mostly-single pass template, respectively). However, what ASP.NET MVC does allow is context switching between view engines based on partials that are rendered. With that in mind, I decided to see if a partial based implementation could be achieved.
The solution is fairly simple, and provides an easy upgrade path if and when you could ditch the WebForms master page. We'll start by creating a few extensions to the controller for rendering Razor based views. The reason we're doing this, is so that a WebForms based view can be rendered, while you think you're rendering a Razor based view.
1: public static ViewResult RazorView(this Controller controller)
2: {
3: return RazorView(controller, null, null);
4: }
5:
6: public static ViewResult RazorView(this Controller controller, object model)
7: {
8: return RazorView(controller, null, model);
9: }
10:
11: public static ViewResult RazorView(this Controller controller, string viewName)
12: {
13: return RazorView(controller, viewName, null);
14: }
15:
16: public static ViewResult RazorView(this Controller controller, string viewName, object model)
17: {
18: if (model != null)
19: controller.ViewData.Model = model;
20:
21: controller.ViewBag._ViewName = GetViewName(controller, viewName);
22:
23: return new ViewResult
24: {
25: ViewName = "RazorView",
26: ViewData = controller.ViewData,
27: TempData = controller.TempData
28: };
29: }
30:
31: static string GetViewName(Controller controller, string viewName)
32: {
33: return !string.IsNullOrEmpty(viewName)
34: ? viewName
35: : controller.RouteData.GetRequiredString("action");
36: }
The thing to note from above, is that we're actually rendering a view called "RazorView" specifying the actual view to be rendered on ViewBag._ViewName. This value will later be used to render the appropriate RazorView.
Side note: This is a naive implementation of GetViewName assuming that if you do not specify the view name, you're rendering a view with the same action name from the route data. This may not be correct in all places, but seems to work reasonably well.
You'll also need to create a "RazorView" WebForms view in the Shared directory which inherits from your master page. This view will ultimately render the Razor view as a partial defined by your action result. I've also created a Razor layout page that all Razor views will use to alleviate the issue of having to touch each view if and when a conversion to only the Razor view engine takes place. You'll find each of the view files below.
Shared/Site.Master:
1: <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
2: <!DOCTYPE html>
3: <html>
4: <head runat="server">
5: <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
6: </head>
7: <body>
8: <div>
9: <asp:ContentPlaceHolder ID="MainContent" runat="server" />
10: </div>
11: </body>
12: </html>
Shared/_SiteLayout.cshtml:
Shared/RazorView.aspx:
1: <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
2: Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
3:
4: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
5: <% Html.RenderPartial((string) ViewBag._ViewName); %>
6: </asp:Content>
Home/Index.cshtml:
1: @{
2: Layout = "~/Views/Shared/_SiteLayout.cshtml";
3: }
4:
5: <h2>This is the index page</h2>
What you'll notice, is that it's not possible to utilize Razor content sections to render different parts from the WebForms master page. However, you are able to use Razor content sections from the Razor layout file since you're in that model. If you do have to render multiple WebForm content sections, you may need to make things a bit more elaborate by specifying specific content areas manually. Ultimately, I wouldn't recommend that, and just try to limit your output to a main content panel.
Within your controller, your action result is simply:
1: public ActionResult Index()
2: {
3: return this.RazorView();
4: }
While this solution isn't optimal, it does allow you to utilize Razor views while still adhering to a common WebForms master page. If possible, I recommend not doing this, but sometimes (like in our solution) you have to do what is necessary. If your master pages are simple enough (and primarily just layout), I fully recommend having a WebForms master page and a Razor layout page. Regardless, the way this has been setup, migration to a Razor only architecture is fairly trivial - just simply change all this.RazorView() calls to View() calls and copy over your layout to _SiteLayout.cshtml.
Click here to download the source for this solution.
e4722db9-b711-46b0-8c10-69b84dcdbd46|19|4.2