This site uses cookies for analytics, personalized content and ads. By continuing to browse this site, you agree to this use. Learn more
Microsoft Logo
Gray Pipe
  • Developer Network
    • Downloads
      • Visual Studio
      • SDKs
      • Trial software
    • Programs
      • Subscriptions
      • Students
      • ISV
      • Startups
      • Events
    • Community
      • Magazine
      • Forums
      • Blogs
      • Channel 9
    • Documentation
      • APIs and reference
      • Dev centers
      • Samples
      • Retired content
Developer Network Developer

Subscriber portal

Get tools
magazine
  • Issues and downloads
    • All issues
    • 2018
      • April 2018
      • March 2018
      • February 2018
      • January 2018
    • 2017
      • Connect(); 2017
      • December 2017
      • November 2017
      • October 2017
      • September 2017
      • August 2017
      • July 2017
      • June 2017
      • May 2017
      • April 2017
      • March 2017
      • February 2017
      • January 2017
    • 2016
      • December 2016
      • Connect(); 2016
      • November 2016
      • October 2016
      • September 2016
      • August 2016
      • July 2016
      • June 2016
      • May 2016
      • April 2016
      • March 2016
      • February 2016
      • January 2016
    • 2015
      • December 2015
      • November 2015
      • Windows 10 issue
      • October 2015
      • September 2015
      • August 2015
      • July 2015
      • June 2015
      • May 2015
      • April 2015
      • March 2015
      • February 2015
      • January 2015
    • 2014
      • Special 2014
      • December 2014
      • November 2014
      • October 2014
      • September 2014
      • August 2014
      • July 2014
      • June 2014
      • May 2014
      • April 2014
      • March 2014
      • February 2014
      • January 2014
    • 2013
      • Government 2013
      • December 2013
      • November 2013
      • October 2013
      • September 2013
      • August 2013
      • July 2013
      • June 2013
      • May 2013
      • April 2013
      • March 2013
      • February 2013
      • January 2013
    • 2012
      • December 2012
      • November 2012
      • Windows 8
      • October 2012
      • September 2012
      • August 2012
      • July 2012
      • June 2012
      • May 2012
      • April 2012
      • March 2012
      • February 2012
      • January 2012
    • 2011
      • December 2011
      • November 2011
      • October 2011
      • September 2011
      • August 2011
      • July 2011
      • June 2011
      • May 2011
      • April 2011
      • March 2011
      • February 2011
      • January 2011
    • 2010
      • December 2010
      • November 2010
      • October 2010
      • September 2010
      • August 2010
      • July 2010
      • June 2010
      • May 2010
      • April 2010
      • March 2010
      • February 2010
      • January 2010
    • 2009
      • December 2009
      • November 2009
      • October 2009
      • September 2009
      • August 2009
      • July 2009
      • June 2009
      • May 2009
      • April 2009
      • March 2009
      • February 2009
      • January 2009
  • Subscribe
  • Submit article
search clear
We’re sorry. The content you requested has been removed. You’ll be auto redirected in 1 second.
Issues and downloads 2013 March 2013 ASP.NET - Migrating ASP.NET Web Forms to the MVC Pattern with the ASP.NET Web API

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:

XML
Copy
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:

XML
Copy
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:

XML
Copy
[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
XML
Copy
<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):

XML
Copy
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):

XML
Copy
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):

XML
Copy
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:

XML
Copy
<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:

XML
Copy
[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:

XML
Copy
[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:

XML
Copy
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:

XML
Copy
<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:

XML
Copy
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:

XML
Copy
...
  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:

XMLScript
Copy
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):

XML
Copy
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:

XMLScript
Copy
.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:

XML
Copy
<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
XMLScript
Copy
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):

 

XML
Copy
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:

XML
Copy
[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):

XML
Copy
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:

XML
Copy
[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:

XML
Copy
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 http://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.

 

MSDN Magazine Blog

 

More MSDN Magazine Blog entries >


Current Issue


April 2018 issue

Browse All MSDN Magazines


Subscribe to MSDN Flash newsletter


Receive the MSDN Flash e-mail newsletter every other week, with news and information personalized to your interests and areas of focus.

Follow us
  • https://www.facebook.com/microsoftdeveloper
  • https://twitter.com/msdev
  • https://plus.google.com/111221966647232053570/
Sign up for the MSDN Newsletter
Is this page helpful?
Your feedback about this content is important.
Let us know what you think.
Additional feedback?
1500 characters remaining
Thank you!
We appreciate your feedback.

Dev centers

  • Windows
  • Office
  • Visual Studio
  • Microsoft Azure
  • More...

Learning resources

  • Microsoft Virtual Academy
  • Channel 9
  • MSDN Magazine

Community

  • Forums
  • Blogs
  • Codeplex

Support

  • Self support

Programs

  • BizSpark (for startups)
  • Microsoft Imagine (for students)
United States (English)
  • Newsletter
  • Privacy & cookies
  • Terms of use
  • Trademarks
logo © 2018 Microsoft