Mar 1 2012

ASP.NET MVC 3, Razor-C#, and VB.NET WebForms - Using Razor Views With WebForms Master Pages

Category: ASP.NET | MVCMatt @ 15:42

When we left off last time, I showed you how to write ASP.NET MVC code in C#, then consume and expose that functionality within a VB.NET WebForms project.  Why?  Because I’m maintaining a project with a huge investment in VB.NET WebForms, and a wholesale migration isn’t feasible.  This approach allows for a gradual migration from VB.NET WebForms to C# ASP.NET (with Razor!)  So far, all I’ve shown you how to do is render Razor views.  But what about all those existing VB.NET master pages?  Today I’ll show you how you can use these master pages as “layouts” for your Razor views.

Today’s Goals

Our app doesn’t really do all that much right now.  Sure, we’ve managed to get C#-controllers with Razor views to render within a VB.NET application, but that’s not really all that useful.  As it stands, we’d have to duplicate our master pages and their functionality as Razor layouts in order to achieve a consistent look-and-feel.  That would mean double-maintenance for any changes to the master pages, which is going to be tedious and easy to overlook.  That’s not acceptable.  Our goals for this post are:

  • To render Razor views using VB.NET WebForms master pages as “layouts.”
  • To be able to set the page title from our Razor views (as you’ll see, this is no small feat).
  • To write our controllers and actions as if there’s nothing fishy going on; they should look like normal, everyday action methods that return normal view results.

While you can easily render WebForms MVC views with WebForms master pages, Razor views require some trickery.  This is because of the drastically different ways the two view technologies work.  WebForms builds up a control tree (in the form of an object graph) in one pass, then actually generates HTML in a second pass. Razor, on the other hand, pretty much writes directly and immediately to the response.  There is a well-documented solution though, as Matt Hawley and Scott Hansleman demonstrated back in 2011.

The RazorView Solution – Close, But No Cigar

Matt Hawley from the CodePlex team blogged about a technique for “wrapping” Razor views with WebForms views in order to leverage WebForms master pages.  This post was later expounded upon by Scott Hansleman.  The “trick” is to wrap the Razor views with a WebForms view first:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
 
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>I'm a secret WebForms View that lies to everyone and renders Razor stuff. Ssssh! Delete this line!</h2>
    <% Html.RenderPartial((string) ViewBag._ViewName); %>
</asp:Content>

Instead of directly rendering the Razor view, you first render a WebForms view (shown above), which then renders the real Razor view as a partial.  This delays the Razor template from executing until the WebForms control tree has been built up.   The downside to the approach demonstrated by Matt and Scott is that you cannot return a normal ViewResult anymore.  Instead, you must return a special ActionResult, like so:

public class HomeController : Controller
{
    // GET: /Home/
    public ActionResult Index()
    {
        return this.RazorView();
    }
}

That may not seem like a big deal at first, but it ties your day-to-day MVC code to this hackery, meaning you’ll have even more work ahead of you once you are ready to cut ties to WebForms.  An ideal solution would enable you to write MVC code as if you weren’t using this WebForms wrapper, making it far easier to convert to real Razor layouts at some point down the road.

Another problem with this approach is that it does not handle page titles.  In Razor, the common approach is for views to store the title in ViewBag, and for the view’s layout to retrieve the title from there.  This approach does not work for WebForms master pages since they do not have access to the ViewBag, nor can the wrapper easily pass that information over since the page’s title has already been rendered by the time the Razor view is actually executed. 

Let’s look at how to overcome these limitations.

Clean Action Methods – The RazorBridgeActionInvoker Solution

One of the extensibility points exposed by the ASP.NET MVC infrastructure is the IActionInvoker interface.  The implementer of this interface is responsible for, as you might have guessed, actually invoking an action method on a controller and executing its result.  For whatever reason, the MVC team decided that the best way to expose this hook was through overriding a method in your Controller class instead of the more IoC-friendly approach they used elsewhere

Still, if you’re using layer supertypes (which you should be for your controllers), it’s quite easy to plug in your own action invoker, and that’s exactly what we’re going to do.  We’ll take the same approach that Matt and Scott took with wrapping the Razor views in a WebForms view, but we’ll perform the wrap in the action invoker instead of within the action method itself.  This pushes the bridge code out of our action methods and frees us from thinking about the hack in our day-to-day coding.  It will also make it trivially simple to drop the hack when we’re ready.  All we’ll have to do is take out the custom action invoker.  Our other MVC code will remain unchanged. 

Let’s dive in!  First, we’ll need a layer super type that all of our controllers will derive from:

public abstract class BlackMagicController : Controller
{
    protected override IActionInvoker CreateActionInvoker()
    {
        return new RazorBridgeActionInvoker();
    }
}

Next, we’ll need the actual action invoker, RazorBridgeActionInvoker.  When an action method returns a ViewResult, the invoker needs to wrap it with the special “bridge” action result that links it to the WebForms master page.  We can derive from the default invoker to layer on our new behavior: 

public class RazorBridgeActionInvoker : ControllerActionInvoker
{
    protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
    {
        var result = base.CreateActionResult(controllerContext, actionDescriptor, actionReturnValue);

        if (result is ViewResult)
        {
            result = new RazorBridgeViewResult((ViewResult) result);
        }

        return result;
    }
}

Now we need our actual wrapper action result.  This is quite similar to the original action result that Matt and Scott referenced in their posts:

public class RazorBridgeViewResult : ViewResult
{
    private readonly ViewResult _razorViewResult;

    public RazorBridgeViewResult(ViewResult razorViewResult)
    {
        _razorViewResult = razorViewResult;
        ViewData = razorViewResult.ViewData;
        ViewName = razorViewResult.ViewName;
    }

    private static string GetViewName(RouteData routeData, string viewName)
    {
        return !string.IsNullOrEmpty(viewName)
                   ? viewName
                   : routeData.GetRequiredString("action");
    }

    public override void ExecuteResult(ControllerContext context)
    {
        _razorViewResult.ViewData["MvcViewName"] = GetViewName(context.RouteData, ViewName);
        _razorViewResult.ViewName = "RazorBridgeView";

        _razorViewResult.ExecuteResult(context);
    }
}

And of course we’ll need our actual WebForms bridge view:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    
    <% Html.RenderPartial(ViewData["MvcViewName"].ToString());%>

</asp:Content>

Notice how the view references a master page?  This master page will reside in our C# MVC application as an embedded resource, and it will be resolved at runtime by the virtual path provider:

<%@ Master Language="C#" MasterPageFile="~/Site.master" %>

<asp:Content runat="server" ID="Master" ContentPlaceHolderID="MainContent">
    <asp:ContentPlaceHolder ID="MainContent" runat="server" />
</asp:Content>

Notice how the WebForms master here is referencing another WebForms master that doesn’t actually exist?  That master actually exists in our VB.NET application and will be resolved at runtime, so ignore the warning that Visual Studio and/or Resharper will most likely give you.

Critical sidenote: In my last post, I modified the MVC project file to include a slew of files as embedded resources so that the embedded virtual path provider could find them, but I missed one very important file type: WebForms master pages!  Edit your MVC project file, and change your BeforeBuild target to the following:

<Target Name="BeforeBuild">
  <ItemGroup>
    <EmbeddedResource Include="**\*.aspx;**\*.ascx;**\*.master;**\*.cshtml;**\*.gif;**\*.css;**\*.js;**\*.png;**\*.jpg" />
  </ItemGroup>
</Target>

You will also need to add the MVC namespaces to your real Web.config file in the VB.NET project.  You should have something that looks like this:

<configuration>
  ...
  <system.web>
        <pages>
            <namespaces>
                <add namespace="System.Web.Mvc" />
                <add namespace="System.Web.Mvc.Ajax" />
                <add namespace="System.Web.Mvc.Html" />
            </namespaces>
        </pages>
  ...
  </system.web>
  ...
</configuration>

All that’s left now is to modify our HomeController to use our layer super type.  As you can see, only the base type changes.  The action method itself remains exactly as it was before:

public class HomeController : BlackMagicController
{
    public ActionResult Index()
    {
        return View();
    }
}

Load up the app and navigate to the Hello/World action, and you should see your view nested within the VB.NET master page:

image

We’re almost finished, now we just need to provide some way to change the title.

It Gets Hacky – Supporting Razor-Style Page Titles

It wouldn’t be a hack without a little JavaScript.  To be fair, this is my least-favorite part of the solution, and I would love to find something else.  Unfortunately, everything else I tried failed.  The fact is that the Razor view is executed too late in the WebForms rendering process to actually change the page title directly.  So what can we do?  Why, change the title with JavaScript, of course!  We can use the HttpContext’s Items collection to pass a title from Razor to WebForms, where with just a little bit of JavaScript trickery, our RazorBridgeView can now set the page title:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    
    <% Html.RenderPartial(ViewData["MvcViewName"].ToString());%>
    
    <script type="text/javascript">
        var title = '<%=Context.Items["RazorBridgeTitle"]%>';

        if (title != "") {
            document.title = "Your App Name - SET FROM MVC! - " + title;
        }
    </script>

</asp:Content>

Setting the title in Razor via HttpContext is ugly.  This can be cleaned up somewhat by introducing a custom base class for the Razor views:

//Both pages are required.
public abstract class RazorBridgeViewPage : RazorBridgeViewPage<dynamic>
{

}

public abstract class RazorBridgeViewPage<T> : WebViewPage<T>
{
    public string Title
    {
        get { return (string)(Context.Items["RazorBridgeTitle"] ?? string.Empty); }
        set { Context.Items["RazorBridgeTitle"] = value; }
    }
}

You will need to register the custom base view in both your MVC project’s ~/Views/Web.config (for Visual Studio to provide Intellisense) as well as your WebForms project’s ~/Views/Web.config (which will actually be used to select the base page at runtime):

...
<system.web.webPages.razor>
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      <pages pageBaseType="BlackMagic.Mvc.WebFormsBridge.RazorBridgeViewPage">
      ...
  </pages>
</system.web.webPages.razor>
...

With the custom base ViewPage in place, your views can remain free of references to the HttpContext. 

I admit this is hacky, but it has the advantage of actually working, whereas every other solution I investigated failed. If anyone knows of something better, please let me know!

image

Wait, There’s More!

Well, there is, but it will have to wait until next post.  All this MVC functionality is great, but how do you start tying in to it from your existing WebForms?  How do you generate strongly-typed links?  And how can you start migrating parts of large, complex pages to MVC without rewriting the entire page at once?  We’ll explore solutions to those challenges in the next post.  In the mean time, feel free to hop over to Github to check out my BlackMagicMvc sample application.

Tags:

blog comments powered by Disqus