March 2013

Volume 28 Number 03

ASP.NET - Migrating ASP.NET Web Forms to the MVC Pattern with the ASP.NET Web API

By Peter Vogel | March 2013

While ASP.NET MVC tends to get most of the attention these days, ASP.NET Web Forms and its related controls allow developers to generate powerful, interactive UIs in a short period of time—which is why there are so many ASP.NET Web Forms applications around. What ASP.NET Web Forms doesn’t support is implementing the Model-View-Controller (MVC) and Model-View-ViewModel (MVVM) patterns, which can enable test-driven development (TDD).

The ASP.NET Web API (“Web API” hereafter) provides a way to build or refactor ASP.NET Web Forms applications to the MVC pattern by moving code from the codebehind file to a Web API controller. This process also enables ASP.NET applications to leverage Asynchronous JavaScript and XML (AJAX), which can be used to create a more responsive UI and improve an application’s scalability by moving logic into the client and reducing communication with the server. This is possible because the Web API leverages the HTTP protocol and (through coding by convention) automatically takes care of several low-level tasks. The Web API paradigm for ASP.NET that this article proposes is to let ASP.NET generate the initial set of markup sent to the browser but handle all of the user’s interactions through AJAX calls to a standalone, testable controller.

Setting up the infrastructure to have a Web Forms application interact with the server through a set of AJAX calls isn’t difficult. But I won’t mislead you: Refactoring the code in the Web Forms application code file to work in a Web API controller might not be a trivial task. You have to give up the various events fired by the controls, auto-generated server-side validation and the ViewState. However, as you’ll see, there are some workarounds for living without these features that can reduce the pain.

Adding Web API Infrastructure

To use the Web API in an ASP.NET project, all you need to do (after adding the NuGet Microsoft ASP.NET Web API package) is right-click and select Add | New Item | Web API Controller Class. If you cannot see the Web API Controller Class in the dialog, ensure that you have the NuGet Microsoft ASP.NET Web API package installed, and that you select Web from the items under the desired programming language. However, adding the controller this way creates a class with a lot of default code that you’ll just have to delete later. You might prefer to simply add an ordinary class file and have it inherit from the System.Web.Http.ApiController class. To work with the ASP.NET routing infrastructure, your class name must end with the string “Controller.”

This example creates a Web API controller called Customer:

public class CustomerController : ApiController
{

A Web API controller class supports a great deal of coding by convention. For example, to have a method called whenever a form is posted back to the server, you need only have a method named “Post” or with a name that begins with “Post” (under the hood, a page that’s posted back to the server is sent to the server with the HTTP POST verb; the Web API picks methods based on the request’s HTTP verb). If that method name violates your organization’s coding convention, you can use the HttpPost attribute to flag the method to use when data is posted to the server. The following code creates a method called UpdateCustomer in the Customer controller to handle HTTP posts:

public class CustomerController : ApiController
{
  [HttpPost]
  public void UpdateCustomer()
  {

Post methods accept, at most, a single parameter (a post method with multiple parameters is ignored). The simplest data that can be sent to a post method is a single value in the body of the post, prefixed with an equal sign (for example, “=ALFKI”). The Web API will automatically map that data to the post method’s single parameter, provided the parameter is decorated with the FromBody attribute, as in this example:

[HttpPost]
public HttpResponseMessage UpdateCustomer([FromBody] string CustID)
{

This is, of course, almost useless. If you want to post back more than a single value—the data from a Web Form, for example—you’ll need to define a class to hold the values from the Web Form: a Data Transfer Object (DTO). The Web API coding convention standards help out here. You need only define a class with property names that match the names associated with the controls in the Web Form to have your DTO properties automatically populated with data from the Web Form by the Web API.

As an example of data that can be posted back to a Web API controller, the (admittedly simple) example Web Form shown in Figure 1 has only three TextBoxes, a RequiredFieldValidator and a Button.

Figure 1 A Basic Sample Web Form

<form id="form1" runat="server">
<p>
  Company Id: <asp:TextBox ID="CustomerID"
    ClientIDMode="Static" runat="server">
    </asp:TextBox> <br/>
  Company Name: <asp:TextBox ID="CompanyName"
    ClientIDMode="Static" runat="server">
    </asp:TextBox>
  <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
    runat="server" ControlToValidate="CompanyName"
    Display="Dynamic"
    ErrorMessage="Company Name must be provided">
  </asp:RequiredFieldValidator><br/>
  City: <asp:TextBox ID="City"
    ClientIDMode="Static" runat="server"> 
    </asp:TextBox><br/>
</p>
<p>
  <asp:Button ID="PostButton" runat="server" Text="Update" />
</p>
</form>

To have the post method accept the data from the TextBoxes in this Web Form, you’d create a class with properties with names that match the ID properties of the TextBoxes, as this class does (any controls in the Web Form that don’t have a matching property are ignored by the Web API):

public class CustomerDTO
{
  public string CustomerID { get; set; }
  public string CompanyName { get; set; }
  public string City { get; set; }
}

A more complex Web Form might require a DTO that you can’t live with (or is beyond the abilities of the Web API to bind to). If so, you can create your own Model Binder to map data from the Web Form controls to the DTO properties. In a refactoring scenario, the code in your Web Form will already be working with the names of the ASP.NET controls—having identically named properties on the DTO reduces the work required when you move that code into the Web API controller.

Routing the Web Form

The next step in integrating the Web API into an ASPX Web Form processing cycle is to provide a routing rule in the Application_Start event of the application’s Global.asax file that will direct the form’s postback to your controller. A routing rule consists of a template that specifies URLs to which the rule applies and which controller is to handle the request. The template also specifies where in the URL to find values that are to be used by the Web API (including values to be passed to methods in the controller).

There are some standard practices here that can be ignored. The standard routing rule can match almost any URL, which can lead to unexpected results when the rule is applied to URLs that you didn’t intend the rule to be used with. To avoid that, a Microsoft best practice is to have URLs associated with the Web API begin with the string “api” to prevent collisions with URLs used elsewhere in the application. That “api” performs no other useful function and just pads out all of your URLs.

Putting that together, you end up with a generalized routing rule in the Application_Start event that looks like this (you need to add using statements for both System.Web.Routing and System.Web.Http to the Global.asax to support this code):

RouteTable.Routes.MapHttpRoute(
  "API Default",
  "api/{controller}/{id}",
  new { id = RouteParameter.Optional })
);

This routing extracts the controller name from the second parameter in the template, so URLs become tightly coupled to controllers. If you rename the controller, any clients using the URL stop working. (I also prefer that any parameters mapped in the URL by the template have more meaningful names than “id.”) I’ve come to prefer more-specific routing rules that don’t require the controller name in the template but, instead, specify the controller name in the defaults passed in the third parameter to the MapHttpRoute method. By making the templates in my routing rules more specific, I also bypass the need for a special prefix for URLs used with Web API controllers, and I’m less frequently surprised by the results of my routing rules.

My routing rules look like the following code, which creates a route called CustomerManagementPost that applies only to URLs beginning with “CustomerManagement” (following the server and site name):

RouteTable.Routes.MapHttpRoute(
  "CustomerManagementPost",
  "CustomerManagement",
  new { Controller = "Customer" },
  new { httpMethod = new HttpMethodConstraint("Post") }
);

This rule would, for example, apply only to a URL like www.phivs.com/CustomerManagement. In the defaults, I tie this URL to the Customer controller. Just to make sure the route is only used when I intend it, I use the fourth parameter to specify that this route is to be used only when data is being sent back as an HTTP POST.

Refactoring to the Controller

If you’re refactoring an existing Web Form, the next step is to get the Web Form to post its data to this newly defined route rather than back to itself. This is the first change to existing code—everything else done so far has been added code, leaving existing processing in place. The revised form tag should look something like this:

<form id="form1" runat="server" action="CustomerManagement"
   method="post" enctype="application/x-www-form-urlencoded">

The key change here is setting the form tag’s action attribute to use the URL specified in the route (“CustomerManagement”). The method and enctype attributes help ensure cross-browser compatibility. When the page posts back to the controller, the Web API will automatically call the post method, instantiate the class being passed to the method and map data from the Web Form to the properties on the DTO—and then pass the DTO to the post method.

With all the pieces in place, you can now write code in your controller’s post method to work with the data in the DTO. The following code updates a matching Entity Framework entity object for a model based on the Northwind database using the data passed from the Web Form:

[HttpPost]
public void UpdateCustomer(CustomerDTO custDTO)
{
  Northwind ne = new Northwind();
  Customer cust = (from c in ne.Customers
                   where c.CustomerID == custDTO.CustomerID
                   select c).SingleOrDefault();
  if (cust != null)
  {
    cust.CompanyName = custDTO.CompanyName;
  }
  ne.SaveChanges();

When processing is complete, something should be sent back to the client. Initially, I’ll just return an HttpResponseMessage object configured to redirect the user to another ASPX page in the site (a later refactoring will enhance this). First, I need to modify the post method to return an HttpResponseMessage:

[HttpPost]
public HttpResponseMessage UpdateCustomer(CustomerDTO custDTO)

Then I need to add the code to the end of the method that returns the redirect response to the client:

HttpResponseMessage rsp = new HttpResponseMessage();
  rsp.StatusCode = HttpStatusCode.Redirect;
  rsp.Headers.Location = new Uri("RecordSaved.aspx", UriKind.Relative);
  return rsp;
}

The real work now begins, including:

  • Moving whatever code was in the ASPX code file into the new controller method
  • Adding in any server-side validation performed by the Validation controls
  • Detaching the code from the events fired by the page

These aren’t trivial tasks. However, as you’ll see, you have some options that might simplify this process by continuing to AJAX-­enable the page. One of those options, in fact, allows you to leave code in the Web Form if it can’t be moved to the controller (or if it’s to be moved later).

At this point in refactoring an existing Web Form, you’ve moved to the MVC pattern but you haven’t moved to the AJAX paradigm. The page is still using the classic request/response cycle rather than eliminating the page’s postback. The next step is to create a genuinely AJAX-enabled page.

Moving to AJAX

The first step in eliminating the request/response cycle is to insert some JavaScript into the process by setting the button’s OnClientClick property to call a client-side function. This example has the button call a JavaScript function named UpdateCustomer:

<asp:Button ID="PostButton" runat="server" Text="Update"
  OnClientClick="return UpdateCustomer();" />

In this function, you’ll intercept the postback triggered by the user clicking the button and replace it with an AJAX call to your service’s method. Using the return keyword in OnClientClick and having UpdateCustomer return false will suppress the postback triggered by the button. Your intercept function should also invoke any client-side validation code generated by the ASP.NET Validation controls by calling the ASP.NET-provided Page_ClientValidate function (in a refactoring process, calling the validators’ client-side code might let you avoid having to recreate the validators’ server-side validation).

If you’re refactoring an existing Web Form, you can now remove the action attribute on the form tag that uses your route. Removing the action attribute allows you to implement a hybrid/staged approach to moving your Web Form’s code to your Web API controller. For code that you don’t want to move to your controller (yet), you can continue to let the Web Form post back to itself. For example, in your intercept function, you can check to see which changes have taken place in the Web Form and return true from the intercept function to let the postback continue. If there are multiple controls on the page that trigger postbacks, you can choose which controls you want to process in your Web API controller and write intercept functions just for those. This lets you implement a hybrid approach when refactoring (leaving some code in the Web Form) or a staged approach (migrating code over time).

The UpdateMethod now needs to call the Web API service to which the page was formerly posting back. Adding jQuery to the project and to the page (I’ve used jQuery 1.8.3) lets you use its post function to call your Web API service. The jQuery serialize function will convert the form into a set of name/value pairs that the Web API will map to the property names on the CustomerDTO object. Integrating this call into the UpdateCustomer function—so that the post only happens if no client-side errors are found—gives this code:

function UpdateCustomer() {
  if (Page_ClientValidate()){
    $.post('CustomerManagement', $('#form1').serialize())
    .success(function () {
      // Do something to tell the user that all went well.
    })
    .error(function (data, msg, detail) {
      alert(data + '\n' + msg + '\n' + detail)
    });
  }
  return false;
}

Serializing the form sends a lot of data to the controller, not all of which might be necessary (for example, the ViewState). I’ll walk through sending just the necessary data later in this article.

The final step (at least for this simple example) is to rewrite the end of the post method in the controller so the user stays on the current page. This version of the post method just returns an HTTP OK status using the HttpResponseMessage class:

...
  ne.SaveChanges();
  HttpResponseMessage rsp = new HttpResponseMessage();
  rsp.StatusCode = HttpStatusCode.OK;
  return rsp;
}

Workflow Processing

You must now decide where the responsibility for any further processing should lie. As shown earlier, if the user is to be sent to a different page, you can handle that in your controller. However, if the controller is now just returning an OK message to the client, you might want to perform some additional processing in the client. For example, adding a label to the Web Form to display the result of the server-side processing would be a good start:

Update Status: <asp:Label ID="Messages" runat="server" Text=""></asp:Label>

In the success method for your AJAX call, you’d update the label with the status of your AJAX call:

success: function (data, status) {
  $("#Messages").text(status); 
},

It’s not unusual, as part of processing a posted page, for the Web Form’s server-side code to update the controls on the page before returning the page to the user. To handle that, you’ll need to return data from the service’s post method and update the page from your JavaScript function.

The first step in that process is to set the Content property of the HttpResponseMessage object to hold the data that you’re returning. Because the DTO created to pass data to the post method from the form is already available, using it to send data back to the client makes sense. However, there’s no need to mark your DTO class with the Serializable attribute to use it with the Web API. (In fact, if you do mark the DTO with the Serializable attribute, the backing fields for the DTO properties will be serialized and sent to the client, giving you odd names to work with in your client-side version of the DTO.)

This code updates the DTO City property and moves it to the HttpResponseMessage Content property, formatted as a JSON object (you’ll need to add a using statement for System.Net.Http.Headers to your controller to make this code work):

HttpResponseMessage rsp = new HttpResponseMessage();
rsp.StatusCode = HttpStatusCode.OK;
custDTO.City = cust.City;
rsp.Content = new ObjectContent<CustomerDTO>(custDTO,
              new JsonMediaTypeFormatter(),
              new MediaTypeWithQualityHeaderValue("application/json"));

The final step is to enhance the intercept function to have its success method move the data into the form:

.success(function (data, status) {
  $("#Messages").text(status);
  if (status == "success") {
    $("#City").val(data.City);
  }
})

This code doesn’t, of course, update the ASP.NET ViewState. If the page does later post back in the normal ASP.NET fashion, then the City TextBox will fire a TextChanged event. If there’s code in the Web Form’s server-side code tied to that event, you might end up with unintended consequences. If you’re either doing a staged migration or using a hybrid approach, you’ll need to test for this. In a fully implemented version of the paradigm, where the Web Form isn’t posted back to the server after the initial display, this isn’t a problem.

Replacing Events

As I noted earlier, you’re going to have to live without the ASP.NET server-side events. However, you can instead capture the equivalent JavaScript event that triggers the postback to the server and invoke a method on your service that does what the code in the server-side event would’ve done. A staged refactoring process leverages this, letting you migrate these events when you have time (or feel the need).

For example, if the page has a delete button for deleting the currently displayed Customer, you can leave the functionality in the page’s code file as part of your initial migration—just let the delete button post the page back to the server. When you’re ready to migrate the delete function, begin by adding a function to intercept the delete button’s client-side onclick event. In this example, I’ve chosen to wire up the event in JavaScript—a tactic that will work with any client-side event:

<asp:Button ID="DeleteButton" runat="server" Text="Delete"  />
<script type="text/javascript">
  $(function () {
    $("#DeleteButton").click(function () { return DeleteCustomer() });
  })

In the DeleteCustomer function, rather than serialize the whole page, I’ll send only the data required by the server-side delete method: the CustomerID. Because I can embed that single parameter in the URL used to request the service, this lets me use another one of the standard HTTP verbs to select the correct controller method: DELETE (for more on HTTP verbs, see bit.ly/92iEnV).

Using the jQuery ajax function, I can issue a request to my controller, building the URL with data from the page and specifying that the HTTP delete verb is to be used as the type of request (see Figure 2).

Figure 2 Using the jQuery AJAX Functionality to Issue a Controller Request

function DeleteCustomer() {               
  $.ajax({
    url: 'CustomerManagement/' + $("#CustomerID").val(),
    type: 'delete',
    success: function (data, status) {
      $("#Messages").text(status);                       
  },
  error: function (data, msg, detail) {
    alert(data + '\n' + msg + '\n' + detail)
    }
  });
  return false;
}

The next step is to create a routing rule that will identify which part of the URL contains the CustomerID and assign that value to a parameter (in this case, a parameter named CustID):

 

RouteTable.Routes.MapHttpRoute(
  "CustomerManagementDelete",
  "CustomerManagement/{CustID}",
  new { Controller = "Customer" },
  new { httpMethod = new HttpMethodConstraint("Delete") }
);

As with the post, the Web API will automatically route an HTTP DELETE request to a method in the controller named or beginning with “Delete”—or to a method flagged with the HttpDelete attribute. And, as before, the Web API will automatically map any data extracted from the URL to parameters on the method that match the name in the template:

[HttpDelete]
public HttpResponseMessage FlagCustomerAsDeleted(string CustID)
{
  //... Code to update the Customer object ...
  HttpResponseMessage rsp = new HttpResponseMessage();
  rsp.StatusCode = HttpStatusCode.OK;
  return rsp;
}

Beyond the HTTP Verbs

Most ASP.NET pages weren’t designed with the HTTP verbs in mind; instead, a “transactional” approach was often used in defining the original version of the code. This can make it difficult to tie the page’s functionality into one of the HTTP verbs (or it can force you to create a complex post method that handles several different kinds of processing).

To handle any transaction-oriented functionality, you can add a route that specifies a method (called an “action” in routing-speak) on the controller by name rather than by HTTP type. The following example defines a URL that routes a request to a method called Assign­CustomerToOrder and extracts a CustID and OrderID from the URL (unlike post methods, methods associated with other HTTP verbs can accept multiple parameters):

RouteTable.Routes.MapHttpRoute(
  "CustomerManagementAssign",
  "CustomerManagement/Assign/{CustID}/{OrderID}",
  new { Controller = "Customer", Action="AssignCustomerToOrder" },
  new { httpMethod = new HttpMethodConstraint("Get") }
  );

This declaration for the method picks up the parameters extracted from the URL:

[HttpGet]
public HttpResponseMessage AssignCustomerToOrder(
  string CustID, string OrderID)
{

The intercept function wired to the appropriate client-side event uses the jQuery get function to pass a URL with the correct components:

function AssignOrder() {
  $.get('CustomerManagement/Assign/' + 
    $("#CustomerID").val() + "/" + "A123",
    function (data, status) {
      $("#Messages").text(status);
      });
  return false;
}

To recap, refactoring the code file for a traditional ASPX page into a Web API controller isn’t a trivial task. However, the flexibility of the ASP.NET Web API, the power it provides for binding HTTP data to .NET objects, and the ability to leverage HTTP standards provide a potential way to move existing applications to an MVC/TDD model—and improve scalability by AJAX-enabling the page along the way. It also provides a paradigm for creating new ASP.NET applications that exploit both the productivity of Web Forms and the functionality of the ASP.NET Web API.


Peter Vogel is a principal at PH&V Information Services, specializing in ASP.NET development with expertise in service-oriented architecture, XML, database and UI design.

Thanks to the following technical experts for reviewing this article: Christopher Bennage and Daniel Roth
Christopher Bennage is a developer at Microsoft on the Patterns & Practices team. His job is to discover, collect and encourage practices that bring developers joy. Amongst his recent technical interests are JavaScript and (casual) game development. He blogs at https://dev.bennage.com.

Daniel Roth is a senior program manager on the Azure Application Platform team currently working on the ASP.NET Web API. Prior to working on ASP.NET he worked on WCF, starting when it first shipped in .NET Framework 3.0. His passions include delighting customers by making frameworks simple and easy to use.