Chapter 18. Upgrading and Combining ASP.NET Technologies

Not all software projects start from a completely blank canvas. If your company has built previous web applications on .NET, it's very likely that you'll have existing code to upgrade or reuse.

In this final chapter, we'll consider a number of realistic project scenarios and your options for moving forward:

  • If you're working on an existing Web Forms application and want to upgrade it to support MVC code: You don't have to throw your whole Web Forms application away to migrate to MVC-style development; you can "upgrade" your existing application to support ASP.NET MVC 2 while retaining your Web Forms pages. You can then build new features using MVC techniques, perhaps migrating older features one by one.

  • If you've started a new ASP.NET MVC 2 application and need to use some Web Forms technologies in it: You may wish to reuse existing Web Forms pages, web controls, or user controls from earlier projects, or you may think that certain parts of your application are better implemented with Web Forms than with MVC. I'll explain how you can fit Web Forms code into an MVC application.

  • If you have an existing ASP.NET MVC 1 application and want to upgrade it to ASP.NET MVC 2: Since you can develop ASP.NET MVC 2 applications using your existing tools (e.g., Visual Studio 2008 SP1), and since they will run on the same server (requiring only .NET 3.5 SP1), and since the version 2 framework is almost totally backward compatible, there's little reason not to upgrade.

Using ASP.NET MVC in a Web Forms Application

Despite the enormous conceptual differences between Web Forms and MVC, the technologies' shared underlying infrastructure makes them fairly easy to integrate. There are, of course, some limitations, which you'll learn about in this chapter.

One way to use ASP.NET MVC and Web Forms together is simply to put an MVC web application project and a separate Web Forms project into the same Visual Studio solution. That's easy, but then you'd have two distinct applications. This chapter is concerned with going further: using both technologies in a single project to create a single application.

Note

Whenever this chapter talks about ASP.NET Web Forms, I'm assuming that you have a basic knowledge of that technology. If you've never used it, you can probably skip the Web Forms-related sections, because you won't have any Web Forms code to reuse.

Let's start by taking an existing Web Forms project and upgrading it to support routing, controllers, views, HTML helpers, and everything else from ASP.NET MVC. It should go without saying, but please remember to use source control or back up your project source code before beginning the upgrade process!

Upgrading an ASP.NET Web Forms Application to Support MVC

First, choose which .NET Framework version you want to target. If you'll be using Visual Studio 2008, there's no choice to make—you have to target .NET 3.5. If you'll be using Visual Studio 2010, then you'll probably want to target .NET 4 unless your web host supports only .NET 3.5.

Next, upgrade your application to target your chosen .NET Framework version:

  1. If you're switching to a newer version of Visual Studio, then the first time you open your application, the Conversion wizard will appear and prompt you through an upgrade process. This is simple—just follow the wizard's prompts. (Note that this means you'll no longer be able to open the project in an older version of Visual Studio.)

  2. Visual Studio supports two kinds of Web Forms projects: web applications, which have in directories, .designer.cs files, and a .csproj file; and web sites, which don't have any of those. If your project is a web application, that's great—move right ahead to step 3. But if your project is a web site, you'll need to convert it to a web application before you proceed. See http://msdn.microsoft.com/en-us/library/aa983476.aspx for instructions.

  3. When you have your web application open in Visual Studio, check that it targets your desired .NET Framework version. Right-click the project name in Solution Explorer and go to Properties. From the Application tab, make sure "Target framework" is set as you wish (see Figure 18-1).

Choosing which .NET Framework version to target

Figure 18-1. Choosing which .NET Framework version to target

After changing the .NET Framework version, ensure your application still compiles and runs properly. .NET's backward compatibility is pretty good, so you shouldn't have any trouble here (that's the theory, at least).

Changing the Project Type

Currently, Visual Studio won't give you any ASP.NET MVC-specific tooling support (e.g., the options to add areas, controllers, or views), nor will it offer any MVC-specific file types when you choose Add

Changing the Project Type

To change this, you need to add a project type hint for Visual Studio.

Warning

Before you proceed, back up your project file (i.e., the one with the .csproj extension), or at least be sure it's up to date in your source control system. If you edit the project file incorrectly, Visual Studio will become unable to open it.

  1. In Solution Explorer, right-click your project name and choose Unload Project.

  2. Right-click the project name again, and choose Edit MyProject.csproj (or whatever your project is called).

  3. You'll now see the .csproj XML file. Find the <ProjectTypeGuids> node, which contains a semicolon-separated series of GUIDs, and add the following value (which means "ASP.NET MVC 2 project") in front of the others:

    • {F85E285D-A4E0-4152-9332-AB1D724D3325};

    • Do not add any extra spaces or line breaks. If you don't want to type in the GUID by hand, you can copy and paste it from the corresponding section of any genuine ASP.NET MVC 2 .csproj file you might have elsewhere.

  4. Save the updated .csproj file. Then reload the project by right-clicking its name in Solution Explorer and choosing Reload Project.

    If you get the error "This project type is not supported by this installation," then either you have mistyped the GUID, or you haven't installed ASP.NET MVC 2 on your PC.

    If you get the error "Unable to read the project file," then simply click OK and choose Reload Project again. It seems to sort itself out, for whatever reason.

You should now find that MVC-specific items appear in the Add

Changing the Project Type

Adding Assembly References

Next, add the ASP.NET MVC 2 assembly and its dependencies to your project:

  1. Add a reference from your project to System.Web.Mvc version 2.0.0.0. You'll find this on the Add Reference window's .NET tab.

  2. To make deployment easier, add references from your project to System.Web.Abstractions and System.Web.Routing, again from the Add Reference window's .NET tab. If you're targeting .NET 3.5, choose version 3.5.0.0 of these, or for .NET 4 choose version 4.0.0.0.

  3. Look at Figure 18-2. In Visual Studio's Solution Explorer, expand your project's References list, highlight System.Web.Mvc, and then in the Properties pane, ensure Copy Local is set to True. This causes the assembly to be copied into your application's in folder when you compile, which is usually necessary for deployment.

Preparing System.Web.Mvc for bin deployment

Figure 18-2. Preparing System.Web.Mvc for bin deployment

Enabling and Configuring Routing

All ASP.NET MVC applications rely on the routing system, so let's set that up now.

  1. If your application doesn't already have a Global.asax file, add one now: right-click the project name in Solution Explorer, choose Add

    Enabling and Configuring Routing
  2. Go to your Global.asax file's code-behind class (e.g., right-click it and choose View Code), and add the following, making it just like the Global.asax.cs file from an ASP.NET MVC 2 application:

    using System.Web.Mvc;
    using System.Web.Routing;
    
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            AreaRegistration.RegisterAllAreas();
            RegisterRoutes(RouteTable.Routes);
        }
    
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
                "Default",                                              // Name
                "{controller}/{action}/{id}",                           // URL
                new { action = "Index", id = UrlParameter.Optional }    // Defaults
            );
        }
    
        // Leave the rest as is
    }

    Notice that this routing configuration doesn't define a default value for controller. That's helpful if you want the root URL (i.e., ~/) to keep displaying the Web Forms default page, ~/default.aspx (and not the Index action on HomeController).

  3. If you're targeting .NET 3.5, enable UrlRoutingModule by adding to your Web.config file's <httpModules> and <system.webServer> nodes:

    <configuration>
      <system.web>
        <httpModules>
          <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,
               System.Web.Routing, Version=3.5.0.0,
               Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        </httpModules>
      </system.web>
      <!-- The following section is necessary if you will deploy to IIS 7 -->
      <system.webServer>
        <validation validateIntegratedModeConfiguration="false"/>
        <modules runAllManagedModulesForAllRequests="true">
          <remove name="UrlRoutingModule"/>
          <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule,
               System.Web.Routing, Version=3.5.0.0,
               Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
        </modules>
        <handlers>
          <add name="UrlRoutingHandler" preCondition="integratedMode" verb="*"
              path="UrlRouting.axd" type="System.Web.HttpForbiddenHandler, System.Web"/>
        </handlers>
      </system.webServer>
    </configuration>

    .NET 4 users don't need to perform this step, because UrlRoutingModule is enabled for all web applications by default in .NET 4's machine-wide configuration files.

You should now have a working routing system. Don't worry, this won't interfere with requests that directly target existing *.aspx pages, since by default, routing gives priority to files that actually exist on disk.

Adding Controllers and Views

To verify that your routing system is working, you'll need to add at least one MVC controller, as follows:

  1. Create a new top-level folder, Controllers, and then right-click it and choose Add

    Adding Controllers and Views
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
  2. Now, if you recompile and visit /Home, your new controller should be invoked and will attempt to render a view. Since no such view yet exists, you'll get the error message shown in Figure 18-3.

    You know you're on the right track when you see an ASP.NET MVC error message.

    Figure 18-3. You know you're on the right track when you see an ASP.NET MVC error message.

  3. You can solve this by adding a view in the normal way. Right-click inside the Index() method and choose Add View. Make sure "Select master page" is unchecked (because you don't have an MVC View Master Page yet), and then click Add. Visual Studio will create /Views, /Views/Home, and /Views/Home/Index.aspx.

  4. The runtime ASPX compiler won't yet recognize System.Web.Mvc.ViewPage, so tell it to reference the ASP.NET MVC assemblies by adding the following to your main Web.config's <assemblies> node:

    <configuration>
      <system.web>
        <compilation debug="false">
    <assemblies>
            <!-- leave the other references in place -->
            <add assembly="System.Web.Abstractions, Version=4.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=2.0.0.0,
                           Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
          </assemblies>
        </compilation>
      </system.web>
    </configuration>

    If you're targeting .NET 3.5, then alter the version numbers for System.Web.Abstractions and System.Web.Routing from 4.0.0.0 to 3.5.0.0.

  5. To add support for strongly typed views, you need to add an extra Web.config file that references ASP.NET MVC 2's view parser. Right-click your /Views folder and then choose Add

    You know you're on the right track when you see an ASP.NET MVC error message.
  6. To add support for HTML helpers (e.g., Html.TextBox()), add the following <namespaces> node to your top-level Web.config file:

    <system.web>
        <pages>
            <namespaces>
                <add namespace="System.Web.Mvc"/>
                <add namespace="System.Web.Mvc.Ajax"/>
                <add namespace="System.Web.Mvc.Html"/>
                <add namespace="System.Web.Routing"/>
                <add namespace="System.Linq"/>
                <add namespace="System.Collections.Generic"/>
            </namespaces>
        </pages>
    </system.web>
  7. Finally, you can go back to your Index.aspx view and add any view markup, including HTML helpers.

You can now visit /Home again, and you'll see your view rendered, as in Figure 18-4.

The Web Forms project is now also an MVC project.

Figure 18-4. The Web Forms project is now also an MVC project.

That's it—you're done! Your Web Forms project should continue to work normally when you visit any of the existing .aspx pages (because files on disk take priority over routing), and you can also add controllers and views, and configure routing exactly as you would in an ASP.NET MVC application.

After upgrading a Web Forms project to support MVC, you're in the same position as if you had started with an MVC project and then added a whole set of Web Forms pages. This means, for example, that if you want to add routing support for your Web Forms pages (instead of continuing to use URLs matching their disk paths), you can follow the advice later in this chapter, in the section "Adding Routing Support for Web Forms Pages."

Interactions Between Web Forms Pages and MVC Controllers

Simply getting MVC and Web Forms code into the same project is hardly the end of the story. Your reason for doing that probably involves getting the two technologies to share data and participate in the same user workflows. Here is some guidance for making that happen.

Linking and Redirecting from Web Forms Pages to MVC Actions

If you're targeting .NET 4, then your Web Forms pages have built-in support for routing, so you can generate URLs and perform redirections to MVC actions as follows:

protected void Page_Load(object sender, EventArgs e)
{
    // You can generate a URL based on routing parameters
    string url = Page.GetRouteUrl(new { controller = "Home", action = "Index" });

    // ... or you can redirect to a location based on routing parameters
    Response.RedirectToRoute(new { controller = "Home", action = "Index" });
}

But if you're targeting .NET 3.5, those methods don't exist. Web Forms isn't aware of routing. No problem—you can implement those methods yourself as extension methods. For example, add the following class anywhere in your project:

// This class is only relevant for .NET 3.5
public static class WebFormsRoutingExtensions
{
   public static string GetRouteUrl(this Control control, object routeValues)
   {
      return GetRouteUrl(routeValues).VirtualPath;
}

   public static void RedirectToRoute(this HttpResponse resp, object routeValues)
   {
      resp.Redirect(GetRouteUrl(routeValues).VirtualPath);
   }

   private static VirtualPathData GetRouteUrl(object values)
   {
      var httpContext = new HttpContextWrapper(HttpContext.Current);
      var rc = new RequestContext(httpContext, new RouteData());
      return RouteTable.Routes.GetVirtualPath(rc, new RouteValueDictionary(values));
   }
}

Now, as long as you've referenced the namespace containing this class, you'll be able to generate URLs and redirect to MVC actions as shown in the preceding Page_Load() code sample.

You won't be able to use MVC's Html.* helper methods from your Web Forms pages, because System.Web.UI.Page doesn't have a property of type HtmlHelper (whereas Html is a property of System.Web.Mvc.ViewPage). That's fine, because you wouldn't use, for example, Html.TextBox() from a Web Forms page anyway—MVC HTML helpers don't survive postbacks. But if you can't use Html.ActionLink(), how should your Web Forms pages render link tags referencing an MVC actions?

If you're targeting .NET 4, you can use an expression builder called RouteUrl that obtains URLs from the routing system—for example:

<asp:HyperLink NavigateUrl="<%$ RouteUrl: controller=Home, action=Index %>"
               runat="server">
    Visit the Index action on HomeController
</asp:HyperLink>

But if you're targeting .NET 3.5, that expression builder isn't available. You can instead use the GetRouteUrl() extension method we defined earlier, as long as you've referenced its namespace in Web.config's <pages>/<namespaces> node—for example:

<a href="<%= Page.GetRouteUrl(new { controller = "Home", action = "Index"}) %>">
    Visit the Index action on HomeController
</a>

Tip

If you're targeting .NET 3.5, it is possible to use the same <%$ RouteUrl: ... %> expression builder syntax that generates route URLs for .NET 4, but you have to implement the RouteUrl: expression builder yourself. One way is to use Red Gate's Reflector tool (www.red-gate.com/products/reflector/) to obtain the source code to .NET 4's System.Web.Compilation.RouteUrlExpressionBuilder class, and then add that class to your own project. Then register this expression builder in your Web.config file as described at http://tinyurl.com/yavtcm6. For it to compile, you'll need to add a further overload of the GetRouteUrl() extension method that accepts a RouteValueDictionary parameter.

Transferring Data Between MVC and Web Forms

The two technologies are built on the same core ASP.NET platform, so when they're both in the same application, they share the same Session and Application collections (among others). It's also possible, though more tricky, to use TempData to share data between the two technologies. These options are explained in more detail in Table 18-1.

Table 18-1. Options for Sharing Data Between MVC Controllers and Web Forms Pages in the Same Application

Collection

Reason for Use

To Access from an MVC Controller

To Access from a Web Forms Page

Session

To retain data for the lifetime of an individual visitor's browsing session

Session

Session

Application

To retain data for the lifetime of your whole application (shared across all browsing sessions)

HttpContext.Application

Application

Temp data

To retain data across a single HTTP redirection in the current visitor's browsing session

TempData

Explained next

The notion of "temp data" is specific to ASP.NET MVC, so Web Forms doesn't come with an easy way to access it. It is possible, but you'll need to write your own code to retrieve the collection from its underlying storage. The following example shows how to create an alternative Page base class that exposes a collection called TempData, loading its contents at the beginning of a request, and saving them at the end of the request:

public class TempDataAwarePage : System.Web.UI.Page
{
    protected readonly TempDataDictionary TempData = new TempDataDictionary();

    protected override void OnInit(EventArgs e) {
        base.OnInit(e);
        TempData.Load(GetDummyContext(), new SessionStateTempDataProvider());
    }

    protected override void OnUnload(EventArgs e) {
        TempData.Save(GetDummyContext(), new SessionStateTempDataProvider());
        base.OnUnload(e);
    }

    // Provides enough context for TempData to be loaded and saved
    private static ControllerContext GetDummyContext()
    {
        return new ControllerContext(
            new HttpContextWrapper(HttpContext.Current),
            new RouteData(),
            _dummyControllerInstance
);
    }

    // Just fulfills tempData.Load()'s requirement for a controller object
    private static Controller _dummyControllerInstance = new DummyController();
    private class DummyController : Controller { }
}

Note

This example code assumes you're using the default SessionStateTempDataProvider, which keeps TempData contents in the visitor's Session collection. If you're using a different provider, you'll need to amend this example code accordingly.

Now, if you change your Web Forms pages to derive from TempDataAwarePage instead of from System.Web.UI.Page, you'll have access to a field called TempData that behaves exactly like an MVC controller's TempData collection, and in fact shares the same data. If you'd rather not change the base class of your Web Forms pages, you can use the preceding example code as a starting point for creating a utility class for manually loading and saving TempData when in a Web Forms page.

Using Web Forms Technologies in an MVC Application

Occasionally, there are valid reasons to use Web Forms technologies in an MVC application. For example, you might need to use a control that's only available as a Web Forms-style server control (e.g., a sophisticated custom control from an earlier Web Forms project). Or you might be about to create a particular UI screen for which you really think Web Forms permits an easier implementation than ASP.NET MVC does.

Using Web Forms Controls in MVC Views

In some cases, you can simply drop an existing ASP.NET server control into an MVC view and find that it just works. This is often the case for render-only controls that generate HTML but don't issue postbacks to the server. For example, you can use an <asp:SiteMapPath> or an <asp:Repeater>[122] control in an MVC view template. If you need to set control properties or invoke data binding against ViewData or Model contents, you can do so by putting a <script runat="server"> block anywhere in your view page—for example:

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        MyRepeater.DataSource = ViewData["products"];
MyRepeater.DataBind();
    }
</script>

Technically, you could even connect your <asp:Repeater> to an <asp:SqlDataSource> control, as is often done in Web Forms demonstrations, but that would totally oppose the goal of separation of concerns: it would bypass both the model and controller portions of MVC architecture, reducing the whole application to a Smart UI design. In any case, it's highly unlikely that you should ever use an <asp:Repeater> control in an MVC view: a simple <% foreach(...) %> loop gets the job done much more directly, it doesn't need a data binding event, and it can give you strongly typed access to each data item's properties. I've only shown the <asp:Repeater> example here to demonstrate that data binding is still possible.

But what about Web Forms server controls that receive input from the user and cause postbacks to the server? These are much trickier to use in an MVC project. Even if that input is merely the user clicking a "page" link, the postback mechanism will only work if that server control is placed inside a Web Forms server-side form.[123] For example, if you put an <asp:GridView> control into an MVC view, you'll get the error shown in Figure 18-5.

Many Web Forms server controls only work inside server-side forms.

Figure 18-5. Many Web Forms server controls only work inside server-side forms.

The GridView control refuses to work outside a server-side form because it depends upon Web Forms' postback and ViewState mechanisms, which are the basis of Web Forms' illusion of statefulness. These mechanisms aren't present in ASP.NET MVC, because ASP.NET MVC is designed to work in tune with (and not fight against) HTML and HTTP.

It's probably unwise for you to disregard MVC's design goals by reintroducing Web Forms' ViewState mechanism and postbacks, but technically you could do so—for example; by putting a GridView control inside a server-side form in an MVC view template, as follows:

<form runat="server">
    <asp:GridView id="myGridViewControl" runat="server" />
</form>

Now the GridView control will render itself correctly, but it won't yet respond to postback events properly. The reason is that when MVC's WebFormViewEngine renders Web Forms pages, it does the rendering as a "child request" (similar to how Html.Action() and Html.RenderAction() work), and this disables postback event processing. You can reinstate postback support, but it's messy. One way is to add the following to your view:

<script runat="server">
    protected void Page_Init(object sender, EventArgs e)
    {
        // Hack to make Web Forms postbacks work
        Context.Handler = Page;
    }
</script>

Now, assuming you bind the grid to some data, then its postback events will actually work (subject to further requirements listed shortly). When you set up the relevant GridView event handlers, the visitor can navigate through a multipage grid by clicking its "page" links, and can change sort order by clicking column headers.

Is this the best of both worlds? Unfortunately not. Trying to use postback-oriented Web Forms controls like this comes with a slew of disadvantages and problems:

  • Web Forms only lets you have one server-side form per page. (If you try to have more, it will just throw an error.) Therefore, you must either keep all your postback-oriented controls together in your page structure (limiting your layout options) or you must copy the traditional Web Forms strategy of wrapping your entire view page inside a single <form runat="server"> container, perhaps at the master page level. The main problem with this strategy is that the HTML specification, and indeed actual web browsers, don't permit nested <form> tags, so you'd become unable to use other HTML form tags that submit to any other action method.

  • A <form runat="server"> container generates a mass of sometimes nonstandard HTML, the infamous hidden __VIEWSTATE field, and JavaScript logic that runs during page loading, depending on what Web Forms controls you put inside the server-side form.

  • Postbacks erase the state of any non-Web Forms controls. For example, if your view contains an Html.TextBox(), its contents will be reset after a postback. That's because non-Web Forms controls aren't supposed to be used with postbacks.

  • The Context.Handler = Page; trick is just a hack I worked out using Reflector. There's no guarantee that it will work in all circumstances, or with future versions of ASP.NET. As far as Microsoft is concerned, postbacks simply aren't supported in MVC applications.

Using Web Forms Pages in an MVC Web Application

If you really want to use a Web Forms control with postbacks, the robust solution is to host the control in a real Web Forms page. This time there are no technical complications—an ASP.NET MVC 2 project is perfectly capable of containing Web Forms server pages alongside its controllers and views.

Simply use Visual Studio to add a Web Forms page to your MVC web application (as shown in Figure 18-6, right-click a folder in Solution Explorer, then go to Add

Using Web Forms Pages in an MVC Web Application
Just add a web form to your MVC web application—it really is that easy.

Figure 18-6. Just add a web form to your MVC web application—it really is that easy.

When you request the URL corresponding to the ASPX file (e.g., /WebForms/MyPage.aspx), it will load and execute exactly as in a regular Web Forms project (supporting postbacks). Of course, you won't get all the benefits of the MVC Framework, but you can use it to host any Web Forms server control.

Adding Routing Support for Web Forms Pages

When you request a Web Forms page using the URL corresponding to its ASPX file on disk, you'll bypass the routing system entirely (because routing gives priority to files that actually exist on disk). That's fine, but if instead of bypassing routing, you actually integrate with it, you could do the following:

  • Access Web Forms pages through "clean URLs" that match the rest of your URL schema

  • Use outbound URL-generation methods to target Web Forms pages with links and redirections that automatically update if you change your routing configuration

As you know, most of your Route entries use MvcRouteHandler to transfer control from routing into the MVC Framework. MvcRouteHandler requires a routing parameter called controller, and it invokes the matching IController class. What's needed for a Web Forms page, then, is some alternative to MvcRouteHandler that knows how to locate, compile, and instantiate Web Forms pages.

Web Forms Routing on .NET 4

If you're targeting .NET 4, it's relatively easy to fit Web Forms pages into the routing system. Just use MapPageRoute() in the RegisterRoutes() method in your Global.asax.cs file:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapPageRoute(
            "UserInfo",                 // Route name
            "users/{userName}",         // URL
            "~/WebForms/ShowUser.aspx"  // Physical ASPX file
    );

    // Also add your MVC routes here
}

Now the URL /users/anything will be handled by /WebForms/ShowUser.aspx. Internally, MapPageRoute() uses a route handler called System.Web.Routing.PageRouteHandler, which knows how to locate and compile ASPX pages. Just like MVC's MapRoute(), Web Forms' MapPageRoute() has overloads for specifying parameter defaults and constraints.

Warning

Web Forms' routing support assumes that you'll give names to all your routes, and that you'll specify an explicit route name every time you generate an outbound URL. If you don't, then outbound URL generation typically goes wrong, because it just matches the first Web Forms route that has no required parameters. For hybrid MVC/Web Forms applications, you might think the solution is to put MVC routes first, but then generic MVC routes such as {controller}/{action}/{id} will prevent visitors from reaching Web Forms pages.

This is a messy situation. For example, it breaks MVC's Html.ActionLink() helper, because that helper doesn't ask for any route name. If you're really building a hybrid application, you might prefer not to register Web Forms route entries with MapPageRoute(), but instead register them using the alternative .NET 3.5-compatible Web Forms routing code I'll supply later in this chapter—it doesn't suffer this problem.

To generate outbound URLs to Web Forms pages, you'll usually need to specify the route name to be matched. In the preceding example, the route entry was called UserInfo, so you can create a Web Forms hyperlink with a clean URL as follows:

<asp:HyperLink runat="server"
               NavigateUrl="<%$ RouteUrl:RouteName=UserInfo, UserName:Bob %>">
    Go to the user list
</asp:HyperLink>

or you can perform a redirection to it from a Web Forms code-behind handler, as follows:

Response.RedirectToRoute("UserInfo", new { userName = "Bob" });

or you can generate a link to it from an MVC view, as follows:

<%: Html.RouteLink("Go to the user list", "UserList", new { userName = "Bob" }) %>

or you can perform a redirection to it from an MVC action method, as follows:

return RedirectToRoute("UserList", new { userName = "Bob" });

If your URL pattern includes a curly brace parameter, then the matching Web Forms page can read its value. In code-behind methods, use the RouteData property (e.g., RouteData.Values["userName"]), or to set properties on server controls, use the RouteValue expression builder—for example:

<asp:TextBox runat="server" Text="<%$ RouteValue: userName %>" />

Note

By default, PageRouteHandler checks both the incoming clean URL and the URL of the physical ASPX file to verify that the user is not denied access by URL-based authorization rules in your Web.config file. If you want to disable this so that the user needs only to be granted access to the incoming clean URL, then pass false for MapPageRoute()'s checkPhysicalUrlAccess parameter.

Web Forms Routing on .NET 3.5

If you're targeting .NET 3.5, you'll need to do a bit more work, because there's no built-in MapPageRoute() method, and Web Forms generally isn't aware of routing.

To fit Web Forms into routing, you first need to define a custom route entry type suitable for use with Web Forms pages. The following route entry type, WebFormsRoute, knows how to use BuildManager.CreateInstanceFromVirtualPath() to locate, compile, and instantiate a Web Forms page. Unlike .NET 4's native Web Forms routing code, this route entry type is careful not to interfere with outbound URL generation except when you specifically supply a virtualPath parameter that corresponds to its own.

using System.Web.Compilation;
public class WebFormsRoute : Route
{
    // Constructor is hard-coded to use the special WebFormsRouteHandler
    public WebFormsRoute(string url, string virtualPath)
        : base(url, new WebFormsRouteHandler { VirtualPath = virtualPath }) { }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext,
                                                   RouteValueDictionary values)
    {
        // Only generate outbound URL when "virtualPath" matches this entry
        string path = ((WebFormsRouteHandler)this.RouteHandler).VirtualPath;
        if ((string)values["virtualPath"] != path)
            return null;
        else
        {
            // Exclude "virtualPath" from the generated URL, otherwise you'd
            // get URLs such as /some/url?virtualPath=~/Path/Page.aspx
            var valuesExceptVirtualPath = new RouteValueDictionary(values);
            valuesExceptVirtualPath.Remove("virtualPath");
            return base.GetVirtualPath(requestContext, valuesExceptVirtualPath);
}
    }

    private class WebFormsRouteHandler : IRouteHandler
    {
       public string VirtualPath { get; set; }
       public IHttpHandler GetHttpHandler(RequestContext requestContext)
       {
          // Store RouteData so it can be read back later
          requestContext.HttpContext.Items["routeData"] = requestContext.RouteData;

          // Compiles the ASPX file (if needed) and instantiates the web form
          return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath
                                              (VirtualPath, typeof(IHttpHandler));
       }
    }
}

Once you've defined this class (anywhere in your MVC web application project), you can use it to set up Route entries to Web Forms pages. For example, add the following route entry to RegisterRoutes() in Global.asax.cs:

routes.Add(new WebFormsRoute("users/{userName}", "~/WebForms/ShowUser.aspx"));

Note

Be sure to put this entry (and other WebFormsRoute entries) at the top of your routing configuration, above your normal MVC route entries. Otherwise, you'll find, for example, that the default route ({controller}/{action}/{id}) will override your WebFormsRoute both for inbound URL matching and outbound URL generation.

As you'd expect, this will expose ~/WebForms/ShowUser.aspx on the URL /users/anything. Now you can also generate links or redirections to that route entry—for example, from an MVC view:

<%= Html.RouteLink("Go to the user list",
            new { virtualPath="~/WebForms/ShowUser.aspx", userName = "bob" }) %>

or from an MVC action:

return RedirectToRoute(new {
    virtualPath = "~/WebForms/ShowUser.aspx",
    userName = "bob"
});

or from a Web Forms page code-behind event handler, if you reference the namespace of the WebFormsRoutingExtensions extension method class from earlier in this chapter:

void myButton_Click(object sender, EventArgs e)
{
    Response.RedirectToRoute(new {
        virtualPath = "~/WebForms/ShowUser.aspx",
        userName = "bob"
});
}

All of these will return the clean URL to the browser (in this example, that's /users/bob). You can't easily use named routes with this code—instead, it uses the ASPX page's virtual path as both a unique route name and as a required routing parameter to avoid interfering with MVC routes' outbound URL generation.

To read routing parameters in Web Forms pages, add the following extension method:

public static class WebFormsRoutingExtensions
{
    public static RouteData GetRouteData(this Control control)
    {
        return (RouteData)HttpContext.Current.Items["routeData"];
    }

    // If you already have this class, leave the other methods in place
}

Now, assuming you've referenced this class's namespace, then you can access routing parameters in Web Forms code-behind classes—for example:

protected void Page_Load(object sender, EventArgs e)
{
    myLabel.Text = this.GetRouteData().Values["userName"];
}

Also, if you've added WebFormsRoutingExtensions' namespace to the <pages>/<namespaces> node in your Web.config file, you can also access routing parameter values in ASPX inline code—for example:

<%= Page.GetRouteData().Values["userName"] %>

If you're targeting .NET 4, then this way of registering route entries is also compatible with the built-in expression builders, so you can also read routing parameter values or generate outbound URLs as follows:

<asp:Literal runat="server" Text="<%$ RouteValue:userName %>" />
<asp:HyperLink NavigateUrl="<%$ RouteUrl: virtualPath=~/WebForms/ShowUser.aspx,
                                          userName=bob %>" runat="server">
    Click me
</asp:HyperLink>

A Note About URL-Based Authorization

I mentioned earlier that .NET 4's native routing helper knows about UrlAuthorizationModule, so if you're using URL-based authorization, it checks that the user is allowed to visit both the clean URL and the URL of the ASPX file to which it maps.

However, the .NET 3.5-compatible code I've just shown doesn't do that. So, if you're using URL-based authorization (which very few MVC developers do), then you need to add authorization rules for your clean routing URLs, not just for the paths to the ASPX files that handle those URLs.

In other words, if you want to protect a Web Forms page exposed by the following route entry:

routes.Add(new WebFormsRoute("some/url/{PersonName}", "~/Path/MyPage.aspx"));

then don't configure URL-based authorization as follows:

<configuration>
  <location path="Page/MyPage.aspx">
    <system.web>
      <authorization>
        <allow roles="administrator"/>
        <deny users="*"/>
      </authorization>
    </system.web>
  </location>
</configuration>

Instead, configure it as follows:

<configuration>
  <location path="some/url">
    <system.web>
      <authorization>
        <allow roles="administrator"/>
        <deny users="*"/>
      </authorization>
    </system.web>
  </location>
  <location path="Page/MyPage.aspx"> <!-- Prevent direct access -->
    <system.web>
      <authorization>
        <deny users="*"/>
      </authorization>
    </system.web>
  </location>
</configuration>

This is because UrlAuthorizationModule is only concerned about the URLs that visitors request, and doesn't know or care what ASPX file, if any, will ultimately handle the request.

Upgrading from ASP.NET MVC 1

If you're continuing development on a project originally built with ASP.NET MVC 1, it's easy to make the case for upgrading to ASP.NET MVC 2. Here are the arguments:

  • There are benefits: After upgrading, you can use newer features such as strongly typed input helpers, areas, client-side validation, and so on.

  • There are usually no significant drawbacks: You don't have to purchase new development tools (if you're already using Visual Studio 2008 SP1, that's enough) and you don't usually have to change your deployment plans. As long as your server runs .NET 3.5 SP1, you're all set.[124]

If you also upgrade to .NET 4 (which requires Visual Studio 2010), there are further benefits, such as the <%: ... %> autoencoding syntax and easier deployment, but this isn't strictly required.

In the last part of this chapter, you'll learn about a few different ways to upgrade an application, and consider what else you might need to do to keep things running smoothly.

Using Visual Studio 2010's Built-In Upgrade Wizard

Visual Studio 2010 has ASP.NET MVC 2 built in, and it knows how to upgrade ASP.NET MVC 1 projects. This is very convenient—it deals with most of the upgrade steps automatically.

The first time you open your ASP.NET MVC 1/Visual Studio 2008 project in Visual Studio 2010, the Conversion wizard will appear. Follow its prompts and it will update your source code as follows:

  • It changes a version number in your main solution file (.sln) to say it's now a Visual Studio 2010 solution. This means it can no longer be opened by earlier versions of Visual Studio.[125]

  • It changes a version number in your project files (.csproj) to say you're now using the Visual Studio 2010 toolset. However, this alone doesn't prevent Visual Studio 2008 from opening and working with those .csproj files.

  • It changes your ASP.NET MVC .csproj file's internal <ProjectTypeGuids> value to say it's now an ASP.NET MVC 2 project. This means it can't be opened by developers who haven't installed ASP.NET MVC 2 on their workstations.

  • It changes the version of System.Web.Mvc that you're referencing from 1.0.0.0 to 2.0.0.0.

  • It adds a reference to System.ComponentModel.DataAnnotations so that you can use Data Annotations attributes to express model metadata.

  • It adds new JavaScript files to your /Scripts folder: jquery-1.4.1.js and jquery.validate.js (plus .vsdoc and .min variants of each), and MicrosoftMvcValidation.js to support client-side validation.

  • It replaces your copies of MicrosoftAjax.js and MicrosoftMvcAjax.js (and the .debug versions of each) with updated versions as used by ASP.NET MVC 2.

  • It updates your /Web.config and /Views/Web.config files so that they reference System.Web.Mvc version 2.0.0.0 rather than version 1.0.0.0.

Even if your application compiles and runs successfully at this point, you probably haven't finished upgrading. See the "A Post-Upgrade Checklist" section later in this chapter for details of what else you might need to do.

Upgrading to .NET 4

During the upgrade process, Visual Studio 2010's Conversion Wizard will bring up the prompt shown in Figure 18-7, asking if you'd like to switch to targeting .NET 4.

Visual Studio 2010 will ask permission to target .NET 4

Figure 18-7. Visual Studio 2010 will ask permission to target .NET 4

Of course, you should only say yes if you'll later be deploying to a server with .NET 4 installed. If you agree to target .NET 4, then the Conversion wizard will carry out the following additional upgrade steps:

  • It updates your .csproj files' <TargetFrameworkVersion> node to indicate that you're targeting .NET 4. This tells Visual Studio to let you use C# 4 code and .NET 4 libraries. Once you start doing so, you definitely can't open these projects in Visual Studio 2008 any more.

  • It adds references to some .NET 4 web-related assemblies: System.Web.Entity, System.Web.DynamicData, and System.Web.ApplicationServices.

  • It removes any explicit reference to System.Core, because the .NET 4 build system references this implicitly.

  • It simplifies your main Web.config file by removing sections that aren't required in .NET 4, or are implicit because they're now part of .NET 4's machine-wide configuration files. Specifically, it removes

    • The <configSection> entry relating to System.Web.Extensions

    • The <httpHandlers>, <httpModules>, <handlers>, and <modules> entries relating to standard framework components such as UrlRoutingHandler and ScriptModule.

    • The <system.codedom> node

    • The <assemblies> references to assemblies that are implicit in .NET 4, such as System.Core and System.Xml.Linq.

  • It updates your main Web.config file to influence the ASP.NET runtime and page compiler as follows:

    • It sets targetFramework ="4.0" on the <compilation> node so that you can use C#4 syntax and <%: ... %> syntax in your views.

    • It sets controlRenderingCompatibility="3.5" and clientIDMode="AutoID" on the <pages> node so that any Web Forms server controls you're using don't change the HTML that they generate. You need to remove the controlRenderingCompatibility attribute if you're using Web Forms server controls and want to take advantage of ASP.NET 4's cleaner HTML markup (and then you might need to change your CSS rules to match).

Other Ways to Upgrade

Visual Studio 2008 doesn't have built-in support for upgrading ASP.NET MVC 1 projects to ASP.NET MVC 2. This leaves you with two possible ways to upgrade:

  • Using an external tool: Eilon Lipton, ASP.NET MVC team member, has created an unofficial stand-alone upgrade tool that performs many of the same steps as Visual Studio 2010's built-in Conversion wizard. For more details and to download the tool, see Eilon's blog post at http://tinyurl.com/yf5zyhq.

  • Manually: Anything Visual Studio can do, you can do (more slowly). There's a reasonably short list of the minimal manual upgrade steps on Microsoft's ASP.NET site at http://tinyurl.com/yybufsp.

A Post-Upgrade Checklist

After using Visual Studio 2010's Conversion wizard, or one of the other upgrade techniques, there are still a few more steps you might need to take. Here's a checklist:

  • Do you have a custom controller factory that inherits from DefaultControllerFactory? If so, and if it overrides the GetControllerInstance() method, you'll have to update it as follows because the method signature has changed. Change this:

    protected override IController GetControllerInstance(Type controllerType)
    {
        return base.GetControllerInstance(controllerType);
    }

    to this:

    protected override IController GetControllerInstance(RequestContext requestContext,
                                                         Type controllerType)
    {
        return base.GetControllerInstance(requestContext, controllerType);
    }

    Until you make this change, your project won't compile.

  • Are you bin-deploying System.Web.Mvc? If you had previously set this reference's Copy Local property to True, you may need to apply that setting again. Visual Studio 2010's Conversion wizard loses that setting when it updates the referenced assembly version.

  • Are you bin-deploying System.Web.Abstractions and System.Web.Routing? This was necessary for deploying ASP.NET MVC 1 applications to servers without .NET 3.5 SP1, but since ASP.NET MVC 2 requires .NET 3.5 SP1, there's no reason to bin-deploy those assemblies any longer. They will be in the server's GAC.

  • Are you upgrading to .NET 4? If so, you'll probably want to use the new <%: ... %> autoencoding syntax, which means doing a replace-in-files to change <%= to <%: everywhere, and also removing manual invocations to Html.Encode() wherever that's now dealt with by the autoencoding syntax. To make any custom HTML helpers compatible with this new syntax, make sure they return MvcHtmlString rather than string (you can construct an MvcHtmlString using MvcHtmlString.Create(someString)).

  • Are you using jQuery? Visual Studio 2010's Conversion wizard adds jquery-1.4.1.js to your /Scripts folder, but it's up to you to update any <script src="..."> references in your views or master pages. You can then delete any older version of jQuery from your project.

  • Will you want to use ASP.NET MVC 2's client-side validation feature? If so, note that it uses the following new CSS class names by default: field-validation-valid and validation-summary-valid. You might want to copy into your CSS file the equivalent rules from /Content/Site.css in any other ASP.NET MVC 2 project.

  • Will you want to use ASP.NET MVC 2's areas feature? If so, update your Global.asax.cs file's Application_Start() method as follows, so that it calls AreaRegistration.RegisterAllAreas() before registering other routes, just like any other ASP.NET MVC 2 application does:

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterRoutes(RouteTable.Routes);
    
        // Leave any other code you already have here
    }

    Unless you do this, areas will not work normally in your upgraded application.

  • Are you using the MVC Futures assembly, Microsoft.Web.Mvc.dll? If so, be sure to upgrade to the ASP.NET MVC 2 version, which you can download from CodePlex at http://aspnet.codeplex.com/releases/view/41742. Otherwise, any calls to Html.RenderAction() will result in a compiler error. Also, there are ASP.NET MVC 2-specific versions of other libraries, such as MVCContrib.

  • Are you using JsonResult to return JSON data? If so, note that its behavior is different in ASP.NET MVC 2. For security, it no longer accepts GET requests by default. See the section "A Note About JsonResult and GET Requests" in Chapter 14 for the reason behind this. The quickest workaround is to use the JsonRequestBehavior.AllowGet option to revert to ASP.NET MVC 1-style behavior, but the most secure long-term solution is to change your JavaScript code so that it calls your action using a POST request instead. See Chapter 14 for more details about these options.

  • Are you using anti-forgery tokens? If so, see the following section for details of a possible problem and a workaround.

Apart from the DefaultControllerFactory method signature change, ASP.NET MVC 2 has very good backward compatibility with ASP.NET MVC 1. Hopefully, at this point you're done and your application compiles and runs successfully.

However, there are still other (less important) breaking changes, so you might also need to adapt your code in other ways. For details, see the list of breaking changes at www.asp.net/learn/whitepapers/what-is-new-in-aspnet-mvc/.

Avoiding Anti-Forgery Token Problems Next Time You Deploy

After upgrading from ASP.NET MVC 1 to ASP.NET MVC 2, you might have a little surprise when you first deploy to your production servers.

The anti-forgery tokens that you can generate using Html.AntiForgeryToken() store an encrypted, serialized data structure. ASP.NET MVC 2 uses a newer data format and can't read the tokens generated by ASP.NET MVC 1. So, if you deploy your upgraded application while visitors are actively using your site, their __RequestValidationToken cookies will suddenly become invalid, leading to HttpAntiForgeryException errors. Visitors won't be able to continue using your site until they clear their session cookies by closing and reopening their browsers.

For a small intranet application, this might be OK—you can edit your global error handler page to display a prominent message saying, "If you're having problems, please try closing and reopening your browser." But for larger, public Internet applications, inconveniencing visitors in this way may not be acceptable.

Detecting and Fixing the Problem Automatically

Another option is to create an error handler that detects HttpAntiForgeryException errors and responds by removing potentially obsolete __RequestValidationToken cookies. You can implement this as an ASP.NET MVC exception filter if you like, but in case you don't have a single controller base class where you can apply the filter globally, I'll show how you can do it as an ASP.NET global error handler.

Create an Application_Error() method in Global.asax.cs as follows:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Error(object sender, EventArgs e)
    {
        var application = (HttpApplication) sender;
        if (IsHttpAntiForgeryTokenException(application.Server.GetLastError()))
            HandleAntiForgeryCookieFormatError(application);
    }
}

Next, implement methods to detect and handle HttpAntiForgeryException errors as follows:

private static bool IsHttpAntiForgeryTokenException(Exception exception)
{
    // Scan up the chain of InnerExceptions
    while (exception != null) {
        if (exception is HttpAntiForgeryException)
            return true;
        exception = exception.InnerException;
    }
    return false;
}
private static void HandleAntiForgeryCookieFormatError(HttpApplication application)
{
    var antiForgeryCookieNames = application.Request.Cookies.Cast<string>()
        .Where(x => x.StartsWith("__RequestVerificationToken"))
        .ToList();

    // To delete a cookie, send a new pre-expired cookie with the same name
    foreach (var cookieName in antiForgeryCookieNames)
        application.Response.Cookies.Add(new HttpCookie(cookieName) {
            Expires = new DateTime(2000, 1, 1)
        });

    application.Response.Redirect("~/Home/UpgradeNotice");
}

Once this code detects any HttpAntiForgeryException, it deletes all of the user's __RequestVerificationToken cookies and then redirects them to the URL /Home/UpgradeNotice. You'll need to handle this URL and display a page saying, "We've just upgraded our site—please go back, reload, and try your operation again." There's no simple, safe way to let the operation continue directly, because then an attacker could bypass your anti-forgery checks by deliberately sending an invalid token.

After a few days, once you're sure that visitors won't still expect to resume browsing sessions that started before your upgrade, you can safely remove all of this workaround code.

Warning

If you're using [HandleError], beware that it will intercept the HttpAntiForgeryException and display its own error view, preventing this solution from working. Either stop using [HandleError], or create another exception filter that runs first and deals with HttpAntiForgeryException errors before [HandleError] does.

Summary

In this chapter, you've seen how even though ASP.NET MVC and Web Forms feel very different to a developer, the underlying technologies overlap so much that they can easily cohabit the same .NET project. You can start from an MVC project and add Web Forms pages, optionally integrating them into your routing system. Or you can start from a Web Forms project and add support for MVC features—that's a viable strategy for migrating an existing Web Forms application to ASP.NET MVC.

You've also learned about the process of upgrading from ASP.NET MVC 1 to ASP.NET MVC 2: how tools can automate much of the process, what common manual steps remain, and how to work around issues you might experience.

That brings us to the end of the book. I hope you enjoyed reading it, and I wish you success with your ASP.NET MVC projects!



[122] For details of these and other Web Forms controls, see a dedicated Web Forms resource, such as Pro ASP.NET 4 in C# 2010, by Matthew MacDonald (Apress, 2010).

[123] That is, a <form> tag with runat="server". This is Web Forms' container for postback logic and ViewState data.

[124] Admittedly, ASP.NET MVC 1 required only .NET 3.5 on the server, but most server administrators will have installed the service pack anyway.

[125] That might seem inconvenient (what if other developers are still using Visual Studio 2008?), but it is necessary: Visual Studio 2010's built-in code generation tools (e.g., for resource files, or for LINQ to SQL) aren't necessarily compatible with Visual Studio 2008's equivalents, so if you use both versions together, you could lose work. If you don't think this is a problem for your application, you can have two different .sln files (one for Visual Studio 2008 and one for 2010) and carry on, because both Visual Studio versions can read the same .csproj files.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.138.138