Feb 15 2012

ASP.NET MVC 3, Razor-C#, and VB.NET WebForms - A Tale of Black Magic Voodoo

Category: ASP.NET | MVCMatt @ 15:45

What do you get when you mix a legacy VB.NET WebForms application, ASP.NET MVC, and Razor views that are written in C#?  If you said “Pain!” you are quite right.  But what you also get is the ability to leverage your existing investment in VB.NET WebForms while crafting new code in ASP.NET MVC Razory-C# goodness.  In this series of posts, I’m going to tell you how you, too, can concoct a wicked brew that will enable you to do crazy things, such as creating Razor views in C# that utilize VB.NET WebForms master pages, or how you can render MVC action methods from within WebForms markup.

Uhhh… why?

I can already here screams of “Matt, you must really hate yourself!  Why in the world would you do this?!?”  The answer is that sometimes you cannot afford to do a massive rewrite all at once.  Sometimes you need to do things incrementally.  My team has an extensive amount of functionality that’s built on VB.NET WebForms.  We recognize that’s not where we want to be, but there’s not exactly a quick fix.  We’re talking years worth of code and (quite literally) hundreds of pages.  We simply cannot afford to go dark for 6 months and rewrite, nor would I want to even if we could.  Why bother converting pages that no developer is ever going to touch again? Instead, I’d rather create an avenue where we can gradually migrate pieces as we come in contact with them, thereby reducing maintenance costs over time.  Building (or perhaps rediscovering?) this avenue of progressive migration while conquering new barriers that have emerged since last time I tried VB WebForms+MVC is what this series of posts is all about.  

Wait, you’ve done this before??

I’m a bit sad to say that this isn’t the first time I’ve mixed-and-matched technologies.  Back in July of 2010, I was confronted with the challenge of maintaining an immense VB.NET WebForms app.  I was already heavily drinking the MVC Kool-Aid, so I naturally looked for a way to start moving pieces to MVC.  WebForms and MVC will play very nicely together.  VB.NET and C#, however, not so much.  VB.NET WebForms and C# ASP.NET MVC?  Not so well at all.  The solution I came up with back in 2010 was to use MVCContrib’s Portable Areas feature to bridge the gap between the two languages and platforms.  I’m going to do something similar here, but there’s an added twist: in 2010, I was using the WebForms view engine.  Nowadays, if I’m doing MVC, I’m doing Razor, and WebForms and Razor don’t exactly mix.  There are solutions, as Matt Hawley (and later Scott Hansleman) have shown, but they only manage to address some of the pain points.

My solution builds on Matt and Scott’s work, solves a few problems they overlooked, and also bridges the gap between C# and VB.NET.  It does this by keeping the C# MVC code in a completely separate project from the VB WebForms code.  The gap is bridged by way of a virtual path provider that reads the C# MVC content files as embedded resources.  The end result is a wicked mix of platforms without any of the trickery bleeding in to the MVC or WebForms code directly. 

Diving Into the Deep End

Let’s start by reviewing exactly what our solution needs to enable:

  • ASP.NET MVC controllers, views, etc. can be created in C# and intermingled seamlessly in a VB.NET WebForms application.
  • Razor can be used as the view engine.
  • Razor views can be rendered within the existing VB.NET master pages.
  • Our controller actions and views are ignorant of VB.NET and WebForms (ie: no visible hacks to bring Razor, C#, and VB.NET WebForms together).
  • Page titles can be set from Razor views.

Note that we won’t solve all of these problems in this first post, but we’ll lay the groundwork for a solution that will address all of these challenges and then some. 

We’ll need an application to actually test our solution with.  I created a VB.NET WebForms project using the VS 2010 project template, which gave me a Default page, a master page, etc:

image

Next, I added a standard C# ASP.NET MVC project.  The goal here isn’t to mesh two applications together, it’s to enable an existing application to be slowly migrated, so I chose to create an empty application for simplicities sake. 

The “trick” to this solution is embedded resources: all the content files needed by the MVC project need to be converted to embedded resources (meaning they’ll be embedded in the output assembly), where they’ll be referenced as needed by a virtual path provider. 

Remembering to mark every JavaScript, CSS, and view file that you add as an embedded resource is both tedious and error-prone, so let’s fix that.  Edit your MVC project file, and add the following BeforeBuild task to it:

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

Next, add a regular-ole reference from the VB.NET project to the C# MVC project.  While you’re at it, go ahead and add a reference via NuGet to the EmbeddedResourceVirtualPathProvider.  This package is the key to it all: it will allow your C#-based Razor views and other MVC resources, which have been embedded in your MVC project, to be served-up from within the VB.NET WebForms application. 

You will need to add some of the boiler-plate MVC stuff to your VB.NET application, such as the additional references for MVC and Razor.  Add references to System.Web.Razor, System.Web.WebPages.Razor, and System.Web.Mvc. 

You will also need to add a few additional references to the ‘assemblies’ element in the VB.NET project.  You can copy-and-paste these from the web.config file in your MVC project.  You should end up with something like this:

<compilation debug="true" strict="false" explicit="true" targetFramework="4.0">
    <assemblies>
        <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    </assemblies>
</compilation>

Next, add a ‘Views’ folder to the VB.NET project, and copy-and-paste the ‘Views/web.config’ and ‘Views/_ViewStart.cshtml’ file from your MVC project into the corresponding location in your VB.NET project.  Even though your controllers, their actions, and the views are going to be served up from within the MVC projects assembly, they’re going to appear as if they’re coming from the VB.NET project.  At runtime your views will inherit the settings specified by the VB.NET project’s web.config and ViewStart files and not the ones in your C# MVC project.

The final bit of plumbing we need to do to get the ball rolling is register our routes.  I prefer to define my routes in C# rather than in VB.NET.  In my sample project, I’ve created a simple static class that’s invoked by the VB.NET project’s Global.asax file.  Here’s the C# code:

public static class BlackMagicMvcBootstrapper
{
    public static void Bootstrap()
    {
        AreaRegistration.RegisterAllAreas();

        var routes = RouteTable.Routes;

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute("HomeRoute",
                         "{controller}/{action}",
                         new { controller = "Home", action = "Index" });
    }
}

The bootstrapper is called during startup of the VB.NET WebForms application:

Public Class Global_asax
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Fires when the application is started

        BlackMagicMvcBootstrapper.Bootstrap()

    End Sub

End Class

In a production application, I’d recommend implementing a cleaner solution, one that enables your MVC project to hook in to IoC and what-not, but those things are beyond the scope of this article.

Hello, World!

Alright, all of the plumbing we need for basic integration is now in place, all that remains is to create our first controller.  Go ahead and make a simple Home controller:

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

And create the Index view:

@model dynamic

@{
    ViewBag.Title = "Hello, World!";
}

<h2>Hello, World!</h2>

Build and launch the VB.NET app.  It should take you to the standard VB.NET Default.aspx page as shown above.  Navigate to http://localhost:Your_Port_here/Home/Index though, and you should be greeted by your Razor view:

image

Uhhhh… so?

Ok, so that’s not terribly impressive, but this is just the start.  In the next post, we’ll look at how to leverage VB.NET master pages from our MVC views without changing how we write our actions. 

Tags:

blog comments powered by Disqus