ASP.NET - Leverage Multiple Code Frameworks with One ASP.NET
By Jeff Fritz | May 2013
In 2001, when Microsoft introduced the Microsoft .NET Framework and, with it, a new technology called ASP.NET, Web developers embraced it for building sites using a forms-based framework. This framework, known as Web Forms, stood the test of time for eight years, with enhancements and changes to support an evolving Web environment. Creating a Web application during that time was a simple choice, with a New Project dialog that presented four ASP.NET options, as shown in Figure 1. Most of us ignored the ASP.NET Mobile Web Site and Web Control Library projects and built only ASP.NET Web Application projects. If you needed Web services, you’d add a SOAP-based service to an existing Web site with an .asmx file.
Figure 1 The Original New Project ASP.NET Choices in Visual C#
In early 2009, the ASP.NET landscape changed dramatically with the introduction of Model-View-Controller (MVC). With the promise of no more viewstate, page event lifecycle or postback events to handle, developers flocked to the new framework. I was one of them, intrigued by the potential of this more-testable Web technology. We had to find ways to justify to our managers and cost centers the budget for switching applications to MVC, and many developers worked through the steps to get MVC content presented in the same application as an existing Web Forms app. Things worked very well with MVC for several years, and then the Web grew up a little. ASP.NET needed to evolve again.
In 2012, Microsoft delivered two new frameworks to add to the ASP.NET toolkit: Web API and SignalR. Both of these frameworks bring something special to the environment, and each is unique in its own way:
- Web API provides an MVC-like experience for developers to deliver content intended for machine interpretation. There’s no UI, and transactions occur in a RESTful manner. Content types are negotiated, and Web API can automatically format content as JSON or XML, based on the HTTP headers submitted to a Web API endpoint.
- SignalR is the new “real-time Web” delivery model from Microsoft. This technology opens up the client-server communications channel to allow for immediate, rich communications from the server to the client. The content delivery model in SignalR reverses our normal expectations, as the server calls the client to interact with content.
Consider the trade-offs already seen between Web Forms and MVC with those of Web API and MVC, as shown in Figure 2.
Figure 2 Benefits of Each ASP.NET Component Framework
Productivity involves features that let you develop and deliver a solution quickly. Control is the extent to which you can affect the bits being transmitted over the network to your connected users. UI indicates whether you can use the framework to deliver a complete UI. Finally, Real Time suggests how well the framework presents content in a timely fashion that could be perceived as an immediate update.
Now, in 2013, when I open my copy of Visual Studio and attempt to start an ASP.NET project, I’m faced with the dialogs shown in Figure 3 and Figure 4.
Figure 3 New Web Project in Visual Studio 2012
Figure 4 New Project Template Dialog in Visual Studio 2012
There are some tricky questions in those windows. What type of project should I start with? What template is going to get me closest to my solution, fastest? And what if I want to include some components of each template? Can I build a mobile application with some server controls and a Web API?
Do I Have to Choose Just One Approach?
Do I have to choose just one approach? The short answer is no, you don’t have to select only one of these frameworks to build a Web application. There are available techniques that allow you to use Web Forms and MVC together, and contrary to the dialog windows presented, Web API and SignalR can easily be added as features to a Web application. Remember, all ASP.NET content is rendered through a series of HttpHandlers and HttpModules. As long as the correct handlers and modules are referenced, you can build a solution with any of these frameworks.
This is the heart of the “One ASP.NET” concept: Don’t choose just one of these frameworks—build your solution with the parts of each that best suit your needs. You’ve got a number of choices; don’t limit yourself to just one.
So you can see this in action, I’m going to put together a small Web application that will have a unified layout, a search screen and a create screen for a list of products. The search screen will be powered by Web Forms and Web API, and show live updates from SignalR. The create screen will be generated automatically by MVC templates. I’m also going to make the Web Forms look great by using a third-party control library, the Telerik RadControls for ASP.NET AJAX. A trial version of these controls is available at bit.ly/15o2Oab.
Setting up the Sample Project and Shared Layout
To get started, I need to create a project using the dialog shown in Figure 3. While I could choose an empty or Web Forms application, the most encompassing solution to choose is the MVC application. Starting with an MVC project is a great choice because you get all of the tooling from Visual Studio to help you configure your models, views and controllers, as well as the ability to add Web Forms objects anywhere in the project file structure. It’s possible to add the MVC tooling back into an existing Web application by changing some of the XML content in the .csproj file. This process can be automated by installing the NuGet package called AddMvc3ToWebForms.
To configure the Telerik controls for use in this project, I need to do some hacking in Web.config to add the HttpHandlers and HttpModules that would normally be configured in a standard Telerik RadControls project. First, I’ll add a couple of lines to define the Telerik AJAX controls UI skin:
Next, I’ll add the Telerik tag prefix:
I’ll make the minimal additions needed to the Web.config HttpHandlers for the Telerik controls:
And, finally, I’ll make the additions to the Web.config Handlers for the Telerik controls:
Now I want to create a layout page for this project, so I’ll create a Web Forms site.master page in the Views | Shared folder. For this site layout, I want to add a standard logo and menu to all pages. I’ll add a logo image by simply dragging the image onto my layout. Next, to add a great cascading menu to the layout, I’ll drag a RadMenu from my controls toolbox onto the designer, just below the image. From the designer surface, I can quickly build out my menu by right-clicking on the menu control and selecting Edit Items to get the window shown in Figure 5.
Figure 5 Telerik RadMenu Configuration Window
The two menu items I want to focus on are under Products—Search and New. For each of these items, I’ve set the NavigateUrl property and text as follows:
With the menu configured, I now have a problem where I’ve defined my layout using Web Forms, and need to host MVC content. This is not a trivial problem, but it is a problem that can be solved.
Bridging the Divide—Configuring MVC to Use a Web Forms Master Page
Like most of you, I prefer to keep things simple. I want to share my defined layout for this project between Web Forms and MVC. There’s a well-documented technique devised by Matt Hawley that demonstrates how to use a Web Forms master page with MVC Razor-based views (bit.ly/ehVY3H). I’m going to use that technique in this project. To create this bridge, I’ll configure a simple Web Forms view called RazorView.aspx that references the master page:
<%@ Page <% Html.RenderPartial((string)ViewBag._ViewName); %> </asp:Content>
In order for my MVC controllers to use this view and allow their Razor-based views to be executed, I need to extend each controller to route the view content appropriately. This is accomplished through an extension method that reroutes the model, ViewData and TempData appropriately through RazorView.aspx, as shown in Figure 6.
public static ViewResult RazorView(this Controller controller, string viewName = null, object model = null) { if (model != null) controller.ViewData.Model = model; controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName) ? viewName : controller.RouteData.GetRequiredString("action"); return new ViewResult { ViewName = "RazorView", ViewData = controller.ViewData, TempData = controller.TempData }; }
With this method constructed, I can easily route all of the MVC actions through the master page. The next step is to set up the ProductsController so that products can be created.
MVC and the Create Product Screen
The MVC part of this solution is a fairly standard MVC approach. I defined a simple model object called BoardGame in the Models folder of my project, as shown in Figure 7.
Next, using the standard MVC tooling in Visual Studio, I create an empty ProductController. I’ll add a Views | Product folder, then right-click on the Product folder and choose View from the Add menu. This view will support the creation of new board games, so I’ll create it with the options shown in Figure 8.
Figure 8 Creating the “New” View
Thanks to the MVC tooling and templates, I don’t need to change a thing. The view created has labels and validation, and can use my master page. Figure 9 shows how to define the New action in the ProductController.
This syntax should be familiar to MVC developers, as the only change is to return a RazorView instead of a View. The _Products object is a static, read-only collection of dummy products that are defined in this controller (instead of using a database in this sample):
public static readonly List<BoardGame> _Products = new List<BoardGame>() { new BoardGame() {Id=1, Name="Chess", Price=9.99M}, new BoardGame() {Id=2, Name="Checkers", Price=7.99M}, new BoardGame() {Id=3, Name="Battleship", Price=8.99M}, new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M} };
Configuring the Web Forms-Based Search Page
I want my users to be able to access the product search page with a URL that doesn’t look like it’s a Web Forms URL, and is friendly to search. With the release of ASP.NET 2012.2, this can now be configured easily. Simply open the App_Start/RouteConfig.cs file and call EnableFriendlyUrls to switch on this capability:
public static void RegisterRoutes( RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.EnableFriendlyUrls(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
With this line added, ASP.NET will route requests for /Product/Search to the physical file residing at /Product/Search.aspx
Next, I want to configure a search page that shows a grid of the current products and their stock levels. I’ll create a Product folder in my project and add a new Web Form to it named Search.aspx. In this file, I’ll remove all markup except the @Page directive and set the MasterPageFile to the Site.Master file defined previously. To display my results, I’ll choose the Telerik RadGrid so I can quickly configure and display the result data:
<%@ Page <telerik:RadGrid
The grid will automatically render columns bound to it on the server side and provide sorting and filtering capabilities. However, I’d like to make this more dynamic. I want to see data delivered and managed on the client side. In this model, data could be transmitted and bound with no server-side code in the Web Forms. To accomplish this, I’m going to add a Web API that will deliver and perform the data operations.
Adding Web API to the Mix
I’ll use the standard Project | Add New menu to add a Web API controller named ProductController to a folder named “api” in my project. This helps me keep clear the differences between the MVC controllers and the API controllers. This API is going to do one thing—deliver data for my grid in JSON format and support OData queries. To accomplish this in Web API, I’m going to write a single Get method and decorate it with the Queryable attribute:
This code returns my collection of BoardGame objects in the static list with a small bit of formatting. By decorating the method with [Queryable] and returning a queryable collection, the Web API framework will automatically handle and process OData filter and sort commands. The method also needs to be configured with the input parameter ODataQueryOptions in order to handle the filter data submitted by the grid.
To configure the grid on Search.aspx to consume this new API, I need to add some client settings to the page markup. In this grid control, I define the client databinding with a ClientSettings element and a DataBinding setting. The DataBinding setting lists the location of the API, the response format type and the name of the controller to query, as well as the OData query format. With these settings, and a definition of the columns to present in the grid, I can run the project and see the grid bound to the data in the _Products dummy list of data, as Figure 10 shows.
<telerik:RadGrid <ClientSettings AllowColumnsReorder="True" ReorderColumnsOnClient="True" ClientEvents- <Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling> <DataBinding Location="/api" ResponseType="JSON"> <DataService TableName="Product" Type="OData" /> </DataBinding> </ClientSettings> <MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id"> <Columns> >
Activating the Grid with Real-Time Data
The last piece of the puzzle is the ability to show real-time changes of the stock levels as products are shipped and received. I’m going to add a SignalR hub to transmit updates and present those new values on the search grid. To add SignalR to my project, I need to issue the following two NuGet commands:
These commands will install the ASP.NET server components for hosting within the IIS Web server and make the JavaScript client libraries available to the Web Forms.
The SignalR server-side component is called a Hub, and I’ll define mine by adding a class called StockHub to a folder called Hubs in my Web project. The StockHub is required to descend from the Microsoft.AspNet.SignalR.Hub class. I defined a static System.Timers.Timer to allow the application to simulate the changing of stock levels. For this simulation, every 2 seconds (when the timer Elapsed event handler triggers), I’ll randomly set the stock level of a randomly chosen product. Once the product stock level is set, I’ll notify all attached clients by executing a method on the client called setNewStockLevel, which is shown in Figure 11.
public class StockHub : Hub { public static readonly Timer _Timer = new Timer(); private static readonly Random _Rdm = new Random(); static StockHub() { _Timer.Interval = 2000; _Timer.Elapsed += _Timer_Elapsed; _Timer.Start(); } static void _Timer_Elapsed(object sender, ElapsedEventArgs e) { var products = ProductController._Products; var p = products.Skip(_Rdm.Next(0, products.Count())).First(); var newStockLevel = p.NumInStock + _Rdm.Next(-1 * p.NumInStock, 100); p.NumInStock = newStockLevel; var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>(); hub.Clients.All.setNewStockLevel(p.Id, newStockLevel); } }
For this hub’s data to be accessible from the server, I need to add a line to RouteConfig indicating the presence of the hub. By calling routes.MapHubs in the RegisterRoutes method of RouteConfig, I complete the server-side configuration of SignalR.
Next, the grid needs to listen for these events from the server. To accomplish this, I need to add some JavaScript references to the SignalR client library installed from NuGet and the code generated from the MapHubs command. The SignalR service connects and exposes the setNewStockLevel method on the client using the code shown in Figure 12.
<script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script> <script src="/signalr/hubs"></script> <script type="text/javascript"> var grid; $().ready(function() { var stockWatcher = $.connection.stockHub; stockWatcher.client.setNewStockLevel = function(id, newValue) { var row = GetRow(id); var orgColor = row.css("background-color"); row.find(".numInStock").animate({ backgroundColor: "#FFEFD5" }, 1000, "swing", function () { row.find(".numInStock").html(newValue).animate({ backgroundColor: orgColor }, 1000) }); }; $.connection.hub.start(); }) </script>
In the jQuery ready event handler, I establish a reference called stockWatcher to the StockHub using the $.connection.stockHub syntax. I then define the setNewStockLevel method on the client property of the stockWatcher. This method uses some other JavaScript helper methods to traverse the grid, find the row with the appropriate product and change the stock level with a fancy color animation provided by the jQuery UI, as shown in Figure 13.
Figure 13 The Search Interface with Grid Generated by Web API and Maintained by SignalR
Wrapping Up
I’ve demonstrated how to build an ASP.NET MVC project and add a Web Forms layout, third-party AJAX controls and Web Forms routing to it. I generated the UI with MVC tooling and activated the content with Web API and SignalR. This project used features from all four of the ASP.NET frameworks to present a cohesive interface, taking advantage of the best features of each component. You can do the same. Don’t choose just one ASP.NET framework for your next project. Instead, choose to use them all.
Jeffrey T. Fritz is a developer evangelist for Telerik with more than 15 years of experience building large-scale multi-tenant Web applications in the Software as a Service model. He’s an INETA speaker and maintains a blog at csharpfritz.com. You can find him on Twitter at twitter.com/csharpfritz and can reach him at jeff.fritz@telerik.com.
THANKS to the following technical experts for reviewing this article: Scott Hanselman (Microsoft) and Scott Hunter (Microsoft)
MSDN Magazine Blog
More MSDN Magazine Blog entries >
Receive the MSDN Flash e-mail newsletter every other week, with news and information personalized to your interests and areas of focus.