Share via


ASP.NET MVC

Building Web Apps without Web Forms

Chris Tavares

This article is based on a prerelease version of the ASP.NET MVC Framework. Details herein are subject to change.

This article discusses:

  • Model View Controller pattern
  • Creating controllers and views
  • Building forms and posting back
  • Controller factories and other extensibility points
This article uses the following technologies:
ASP.NET

Code download available at:MVCFramework2008_03.exe(189 KB)

Contents

Model View Controller Pattern
Creating a Controller
Creating a View
A Bigger Example
Creating Forms and Posting Back
Controller Creation
Other Extensibility Points
Farewell Web Forms?

I have been a professional developer for about 15 years now and a hobbyist for at least 10 years before that. Like most of my generation, I started out on 8-bit machines and then moved on to the PC platform. Working my way up through machines of increasing complexity, I wrote apps that did everything from small games to personal data management to controlling external hardware.

For the first half of my career, however, all the software I wrote had one thing in common: it was always local applications running on a user's desktop. In the early '90s I started hearing about this new thing called the World Wide Web. I saw an opportunity here to build a Web application that let me enter my timecard information without actually having to schlep back to my office from the work site.

The experience was, in a word, baffling. Coming to grips with the stateless Web just didn't click with my desktop-oriented mindset. Add lousy debugging, a UNIX server I didn't have root access to, and that weird angle bracket stuff, and my younger self shamefully dove back into desktop development for several more years.

I stayed away from Web development; it was obviously important, but I didn't really understand the programming model. Then the Microsoft® .NET Framework and ASP.NET were released. Finally, here was a framework that let me work on Web applications, yet it was almost like programming desktop applications. I could build windows (pages), hook up controls to events, and the designer kept me from having to deal with those darn angle brackets. And best of all, ASP.NET automatically handled the stateless nature of the Web for me with view state! I was a happy programmer again ... at least for a while.

As my experience grew, so did my choices in design. I'd learned several best practices that I applied when working on my desktop apps. Two of them were:

  • Separation of concerns: don't mix UI logic with underlying behavior.
  • Automated unit testing: write automated tests that verify whether your code does what you think it does.

The underlying principles here apply regardless of technology. Separation of concerns is a fundamental principle that exists to help you deal with complexity. Mixing Different responsibilities within the same object—like calculating remaining work hours, formatting data, and drawing a graph—is just asking for maintenance problems. And automated testing is crucial to getting production-quality code while still maintaining your sanity, particularly when you're updating an existing project.

ASP.NET Web Forms made it very easy to get started, but, in other ways, trying to apply my design principles to Web apps was a struggle. Web Forms are relentlessly UI focused; the fundamental atom is the page. You start by designing your UI and dragging controls. It's very seductive to just start slapping your application logic into the page's event handlers (much like Visual Basic® enabled for Windows® apps).

On top of that, unit testing of pages is often difficult. You can't run a Page object through its lifecycle without spinning up all of ASP.NET. While it is possible to test Web apps by sending HTTP requests to a server or automating a browser, that kind of testing is fragile (change one control ID and the test breaks), hard to set up (you have to set up the server on every developer's machine exactly the same way), and slow to run.

As I started building more sophisticated Web apps, the abstractions that Web Forms provides, like controls, view state, and the page lifecycle, started to chafe rather than help. I was spending more and more time configuring data binding (and writing tons of event handlers to get it configured correctly). I had to figure out how to reduce the view state size to get my pages to load faster. Web Forms require that a physical file exist at every URL, something that dynamic sites (like a wiki, for example) make difficult. And successfully writing a custom WebControl is a remarkably complex process that requires a comprehensive understanding of both page lifecycle and the Visual Studio® designer.

Since coming to work at Microsoft, I've had the opportunity to share my learning about various .NET pain points and to hopefully alleviate some of that pain. Most recently, such an opportunity arose through my participation as a developer on the patterns & practices Web Client Software Factory project (codeplex.com/websf). In particular, one of the things that patterns & practices builds into its deliverables is automated unit testing. In the Web Client Software Factory, we proposed the use of the Model View Presenter (MVP) pattern for building testable Web Forms.

In a nutshell, instead of putting your logic in the page, MVP has you build your pages so that the page (View) simply makes calls into a separate object, the Presenter. The Presenter object then performs any logic necessary to respond to the activity on the view, typically by using other objects (the Model) to access databases, perform business logic, and so on. Once those steps are complete, the Presenter updates the view. This approach gives you testability because the presenter is isolated from the ASP.NET pipeline; it communicates with the view through an interface and can be tested in isolation from the page.

MVP works, but the implementation can be a bit awkward; you need a separate view interface, and you have to write lots of event forwarding functions in your codebehind files. But if you want a testable UI in your Web Forms applications, it's about the best you're going to get. Any improvements would require a change in the underlying platform.

Model View Controller Pattern

Luckily, the ASP.NET team has been listening to developers like me and has started development of a new Web application framework that sits side-by-side with the Web Forms you know and love but has a distinctly different set of design goals:

  • Embrace HTTP and HTML—don't hide it.
  • Testability is built-in from the ground up.
  • Extensible at almost every point.
  • Total control over your output.

This new framework is based around the Model View Controller (MVC) pattern, thus the name, ASP.NET MVC. The MVC pattern was originally invented back in the '70s as part of Smalltalk. As I'll show in this article, it actually fits into the nature of the Web quite well. MVC divides your UI into three distinct objects: the controller, which receives and handles input; the model, which contains your domain logic; and the view, which generates your output. In the context of the Web, the input is an HTTP request, and the request flow looks like Figure 1.

Figure 1 MVC Pattern Request Flow

Figure 1** MVC Pattern Request Flow **(Click the image for a larger view)

This is actually quite different from the process in Web Forms. In the Web Forms model, the input goes into the page (the View), and the view is responsible for both handling the input and generating the output. When it comes to MVC, on the other hand, the responsibilities are separated.

So, right now there's probably one of two things running through your head. Either, "Hey, this is great. How do I use it?" or "Why would I write three objects when I only had to write one before?" Both are excellent questions and best explained by looking at an example. So I'm going to write a small Web app using the MVC Framework to demonstrate its advantages.

Creating a Controller

To follow along, you'll need to install Visual Studio 2008 and get a copy of the MVC Framework. At the time of this writing, it's available as part of the December 2007 Community Technology Preview (CTP) of the ASP.NET Extensions (asp.net/downloads/3.5-extensions). You'll want to grab both the extensions CTP and the MVC Toolkit, which includes some very useful helper objects. Once you download and install the CTP, you'll get a new project type in your New Project dialog called ASP.NET MVC Web Application.

Selecting the MVC Web Application project gives you a solution that looks a little bit different from the usual Web site or application. The solution template creates a Web application with some new directories (as shown in Figure 2). In particular, the Controllers directory contains the controller classes, and the Views directory (and all of its subdirectories) contains the views.

Figure 2 The MVC Project Structure

Figure 2** The MVC Project Structure **

I'm going to write a very simple controller that returns a name passed in on the URL. Right-clicking on the Controllers folder and choosing Add Item displays the usual Add New Item dialog, with some new additions, including an MVC Controller Class and several MVC View components. In this case, I'm adding an ever-so-imaginatively named HelloController class:

using System; using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Controllers { public class HelloController : Controller { [ControllerAction] public void Index() { ... } } }

A controller class is a lot lighter weight than a page. As a matter of fact, the only things that are truly necessary are to derive from System.Web.Mvc.Controller and to put the [ControllerAction] attribute on your action methods. An action is a method that's called in response to a request to a particular URL. Actions are responsible for doing whatever processing is required and then rendering a view. I'll start by writing a simple action that passes the name along to the view, as you can see here:

[ControllerAction] public void HiThere(string id) { ViewData["Name"] = id; RenderView("HiThere"); }

The action method receives the name from the URL via the id parameter (more about how in a moment), stores it in the ViewData collection, and then renders a view named HiThere.

Before discussing how this method gets called, or what the view looks like, I'd like to talk about testability. Remember my previous comments about how hard it is to test Web Forms page classes? Well, controllers are a lot easier to test. In fact, a controller can be instantiated directly, and action methods called, without any additional infrastructure. You don't need an HTTP context, and you don't need a server, just a test harness. As an example, I've included a Visual Studio Team System (VSTS) unit test for this class in Figure 3.

Figure 3 Controller Unit Test

namespace HelloFromMVC.Tests { [TestClass] public class HelloControllerFixture { [TestMethod] public void HiThereShouldRenderCorrectView() { TestableHelloController controller = new TestableHelloController(); controller.HiThere("Chris"); Assert.AreEqual("Chris", controller.Name); Assert.AreEqual("HiThere", controller.ViewName); } } class TestableHelloController : HelloController { public string Name; public string ViewName; protected override void RenderView( string viewName, string master, object data) { this.ViewName = viewName; this.Name = (string)ViewData["Name"]; } } }

There are several things going on here. The actual test is very straightforward: instantiate the controller, call the method with the expected data, then verify that the correct view was rendered. I do the check by creating a Test-specific subclass that overrides the RenderView method. This allows me to short-circuit the actual creation of HTML. I just care that the right data was sent to the view and that the correct view was rendered. I don't care about the underlying details of the view itself for this test.

Creating a View

Of course, ultimately it is necessary for me to generate some HTML, so let's create that HiThere view. To do this, first I create a new folder in the solution named Hello under the Views folder. By default, the controller will look for a view in the Views\<ControllerPrefix> folder (the controller prefix is the name of the controller class minus the word "Controller"). So for views rendered by the HelloController, it looks in Views\Hello. The solution ends up looking like Figure 4.

Figure 4 Adding a View to the Project

Figure 4** Adding a View to the Project **(Click the image for a larger view)

The HTML for the view looks like this:

<html xmlns="https://www.w3.org/1999/xhtml" > <head runat="server"> <title>Hi There!</title> </head> <body> <div> <h1>Hello, <%= ViewData["Name"] %></h1> </div> </body> </html>

Several things should leap out at you. There are no runat="server" tags. There's no form tag. There are no control declarations. In fact, this looks a lot more like classic ASP than ASP.NET. Note that MVC views are only responsible for generating output, so they don't need any of the event handling or complex controls that Web Forms pages do.

The MVC Framework does borrow the .aspx file format as a useful text templating language. You can even use codebehind if you want, but by default the codebehind file looks like this:

using System; using System.Web; using System.Web.Mvc; namespace HelloFromMVC.Views.Hello { public partial class HiThere : ViewPage { } }

No page Init or load methods, no event handlers, nothing except the declaration of the base class, which is not Page but instead ViewPage. This is all you need to be an MVC view. Run the app, navigate to https://localhost:<port>/Hello/HiThere/Chris, and you'll see something like Figure 5.

Figure 5 Successful MVC View

Figure 5** Successful MVC View **(Click the image for a larger view)

If, instead of Figure 5, you see a nasty looking exception, don't panic. If you have the HiThere.aspx file set as the active document in Visual Studio when you press F5, Visual Studio will attempt to access the .aspx file directly. Since MVC views require the controller to have run before displaying, trying to navigate directly to the page will not work. Just edit the URL to match what you see in Figure 5, and it should work fine.

How'd the MVC Framework know to call my action method? There wasn't even a file extension for that URL. The answer is URL routing. If you look inside the global.asax.cs file, you'll see the chunk of code in Figure 6. The global RouteTable stores a collection of Route objects. Each Route describes a URL form and what to do with it. By default, two routes get added to the table. The first is what does the magic. It says that for each URL that consists of three parts after the server name, the first part should be taken as a controller name, the second as an action name, and the third as the ID parameter:

Figure 6 Route Table

public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Change Url= to Url="[controller].mvc/[action]/[id]" // to enable automatic support on IIS6 RouteTable.Routes.Add(new Route { Url = "[controller]/[action]/[id]", Defaults = new { action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); RouteTable.Routes.Add(new Route { Url = "Default.aspx", Defaults = new { controller = "Home", action = "Index", id = (string)null }, RouteHandler = typeof(MvcRouteHandler) }); } }

Url = "[controller]/[action]/[id]"

This default route is what enabled my HiThere method to get invoked. Remember this URL: https://localhost/Hello/HiThere/Chris? This route mapped Hello to the controller, HiThere to the action, and Chris to the ID. The MVC Framework then created a HelloController instance, called the HiThere method, and passed Chris as the value of the ID parameter.

This default route gives you a lot, but you can add your own routes as well. For example, I want a really friendly site where folks should only have to enter their name for a personalized greeting. If I add this route to the top of the routing table

RouteTable.Routes.Add(new Route { Url = "[id]", Defaults = new { controller = "Hello", action = "HiThere" }, RouteHandler = typeof(MvcRouteHandler) });

I can then simply go to https://localhost/Chris and my action is still invoked, and I see my familiar friendly greeting.

How did the system know which controller and action to invoke? The answer is in the Defaults parameter. This makes use of the new C# 3.0 anonymous type syntax to create a pseudo dictionary. The Defaults object on the Route can contain arbitrary extra information, but for MVC it can also contain some well-known entries: controller and action. If there's no controller or action specified in the URL, then it'll use the name in the Defaults. That's why I can leave them out of the URL and still have my request mapped to the correct controller and action.

One more thing to note: remember that I said "add to the top of the table"? If you put it at the bottom, you'll instead get an error. Routing works on a first come, first served basis. When processing URLs, the routing system walks the table from top to bottom, and the first route to match wins. In this case, the default route "[controller]/[action]/[id]" matches because there are default values for the action and ID. Thus, it goes looking for ChrisController, and since I don't have a controller I get an error.

A Bigger Example

Now that I've demonstrated the basics of the MVC Framework, I'd like to show you a larger example that does more than just display a string. A wiki is a Web site that can be edited in the browser. Pages can be added or edited easily. I've written a small sample wiki using the MVC Framework. The "Edit this page" screen is shown in Figure 7.

Figure 7 Editing the Home Page

Figure 7** Editing the Home Page **(Click the image for a larger view)

You can check out the code download for this article to see how the underlying wiki logic is implemented. Right now I want to concentrate on how the MVC Framework made it easy to get the wiki onto the Web. I started out by designing my URL structure. I wanted the following:

  • /[pagename] displays the page with that name.
  • /[pagename]?version=n shows the requested version of the page, where 0 = the current version, 1 = the previous one, and so on.
  • /Edit/[pagename] opens the editing screen for that page.
  • /CreateNewVersion/[pagename] is the URL that is posted to in order to submit an edit.

Let's start off with the basic display of a wiki page. To do this, I created a new class named WikiPageController. I then added an action named ShowPage. The WikiPageController started out looking like Figure 8. The ShowPage method is pretty straightforward. The WikiSpace and WikiPage classes represent a set of wiki pages and a specific page (and its revisions), respectively. This action just loads up the model and calls RenderView. But what's that "new WikiPageViewData" line in there?

Figure 8 WikiPageController Implementation of ShowPage

public class WikiPageController : Controller { ISpaceRepository repository; public ISpaceRepository Repository { get { if (repository == null) { repository = new FileBasedSpaceRepository( Request.MapPath("~/WikiPages")); } return repository; } set { repository = value; } } [ControllerAction] public void ShowPage(string pageName, int? version) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); RenderView("showpage", new WikiPageViewData { Name = pageName, Page = page, Version = version ?? 0 }); } }

My previous example demonstrated one way to pass data from the controller to the view: the ViewData dictionary. Dictionaries are convenient, but they're also dangerous. They can contain absolutely anything, you don't get any IntelliSense® for the contents, and because the ViewData dictionary is of type Dictionary<string, object> to consume its contents you have to cast everything.

When you know what data you will need in the view, you can instead pass strongly typed ViewData object. In my case, I created a simple object, WikiPageViewData, as shown in Figure 9. This object carries the wiki page information to the view along with a couple of utility methods to do things like get the HTML version of the wiki markup.

Figure 9 WikiPageViewData Object

public class WikiPageViewData { public string Name { get; set; } public WikiPage Page { get; set; } public int Version { get; set; } public WikiPageViewData() { Version = 0; } public string NewVersionUrl { get { return string.Format("/CreateNewVersion/{0}", Name); } } public string Body { get { return Page.Versions[Version].Body; } } public string HtmlBody { get { return Page.Versions[Version].BodyAsHtml(); } } public string Creator { get { return Page.Versions[Version].Creator; } } public string Tags { get { return string.Join(",", Page.Versions[Version].Tags); } } }

Now that I've got the view data defined, how do I use it? In ShowPage.aspx.cs you'll see this:

namespace MiniWiki.Views.WikiPage { public partial class ShowPage : ViewPage<WikiPageViewData> { } }

Notice that I have defined the base class to be of type ViewPage<WikiPageViewData>. This means that the page's ViewData property is of type WikiPageViewData and not a Dictionary like in the previous example.

The actual markup in the .aspx file is pretty straightforward:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.ShowPage" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <h1><%= ViewData.Name %></h1> <div id="content" class="wikiContent"> <%= ViewData.HtmlBody %> </div> </asp:Content>

Notice that I'm not using the indexing operator [] when referencing ViewData. Instead, since I now have a strongly typed ViewData, I can just access the property directly. No casts are required, and Visual Studio gives you IntelliSense.

The astute observer will have noticed the <asp:Content> tag in this file. Yes, Master Pages do work with MVC views. And Master Pages can also be views. Let's look at the Master Page codebehind:

namespace MiniWiki.Views.Layouts { public partial class Site : System.Web.Mvc.ViewMasterPage<WikiPageViewData> { } }

The associated markup is shown in Figure 10. Right now, the Master Page gets the exact same ViewData object as the view does. I've declared my Master Page's base class to be ViewMasterPage<WikiPageViewData> so that I have the proper type of ViewData. From there, I set up the various DIV tags to lay out my page, fill out the version list, and finish up with the usual content placeholder.

Figure 10 Site.Master

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="MiniWiki.Views.Layouts.Site" %> <%@ Import Namespace="MiniWiki.Controllers" %> <%@ Import Namespace="MiniWiki.DomainModel" %> <%@ Import Namespace="System.Web.Mvc" %> <html xmlns="https://www.w3.org/1999/xhtml"> <head runat="server"> <title><%= ViewData.Name %></title> <link href="https://../../Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="inner"> <div id="top"> <div id="header"> <h1><%= ViewData.Name %></h1> </div> <div id="menu"> <ul> <li><a href="https://Home">Home</a></li> <li> <%= Html.ActionLink("Edit this page", new { controller = "WikiPage", action = "EditPage", pageName = ViewData.Name })%> </ul> </div> </div> <div id="main"> <div id="revisions"> Revision history: <ul> <% int i = 0; foreach (WikiPageVersion version in ViewData.Page.Versions) { %> <li> <a href="https://<%= ViewData.Name %>?version=<%= i %>"> <%= version.CreatedOn %> by <%= version.Creator %> </a> </li> <% ++i; } %> </ul> </div> <div id="maincontent"> <asp:ContentPlaceHolder ID="MainContentPlaceHolder" runat="server"> </asp:ContentPlaceHolder> </div> </div> </div> </body> </html>

One other thing to notice is the call to Html.ActionLink. This is an example of a rendering helper. The various view classes have two properties, Html and Url. Each has useful methods to output chunks of HTML. In this case, Html.ActionLink takes an object (here, of an anonymous type) and runs it back through the routing system. This produces a URL that will route to the controller and action I've specified. This way, no matter how I change my routes, the "Edit this page" link will always point to the right place.

You may also notice that I have also had to resort to manually building a link (the links to previous page versions). Unfortunately, the current routing system doesn't work that well for generating URLs where query strings are involved. This should be fixed in later versions of the framework.

Creating Forms and Posting Back

Now, let's look at the EditPage action on the controller:

[ControllerAction] public void EditPage(string pageName) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); RenderView("editpage", new WikiPageViewData { Name = pageName, Page = page }); }

Again, the action doesn't do much—it just renders the view with the given page. Things get more interesting in the view, shown in Figure 11. This file is building an HTML form, but there's no Runat="server" to be seen. The Url.Action helper is used to generate the URL that the form posts back to. There are also several uses of various HTML helpers like TextBox, TextArea, and SubmitButton. They do pretty much what you'd expect: generate HTML for various input fields.

Figure 11 EditPage.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="EditPage.aspx.cs" Inherits="MiniWiki.Views.WikiPage.EditPage" %> <%@ Import Namespace="System.Web.Mvc" %> <%@ Import Namespace="MiniWiki.Controllers" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <form action="<%= Url.Action( new { controller = "WikiPage", action = "NewVersion", pageName = ViewData.Name })%>" method=post> <% if (ViewContext.TempData.ContainsKey("errors")) { %> <div id="errorlist"> <ul> <% foreach (string error in (string[])ViewContext.TempData["errors"]) { %> <li><%= error%></li> <% } %> </ul> </div> <% } %> Your name: <%= Html.TextBox("Creator", ViewContext.TempData.ContainsKey("creator") ? (string)ViewContext.TempData["creator"] : ViewData.Creator)%> <br /> Please enter your updates here:<br /> <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? (string)ViewContext.TempData["body"] : ViewData.Body, 30, 65)%> <br /> Tags: <%= Html.TextBox( "Tags", ViewContext.TempData.ContainsKey("tags") ? (string)ViewContext.TempData["tags"] : ViewData.Tags)%> <br /> <%= Html.SubmitButton("SubmitAction", "OK")%> <%= Html.SubmitButton("SubmitAction", "Cancel")%> </form> </asp:Content>

One of the more annoying things to deal with in Web programming is errors on a form. More specifically, you want to display error messages, but you want to keep the previously entered data. We've all had the experience of making a mistake on a form that has 35 fields, only to be presented with a bunch of error messages and a new, blank form. The MVC Framework offers TempData as a place to store the previously entered information so that the form can be repopulated. This is something that ViewState actually made very easy in Web Forms, since saving the contents of controls was pretty much automatic.

I'd like to do this in MVC as well, and that's where TempData comes in. TempData is a dictionary, much like the untyped ViewData. However, the contents of TempData only live for a single request and then they're deleted. To see how this is used, look at Figure 12, the NewVersion action.

Figure 12 NewVersion Action

[ControllerAction] public void NewVersion(string pageName) { NewVersionPostData postData = new NewVersionPostData(); postData.UpdateFrom(Request.Form); if (postData.SubmitAction == "OK") { if (postData.Errors.Length == 0) { WikiSpace space = new WikiSpace(Repository); WikiPage page = space.GetPage(pageName); WikiPageVersion newVersion = new WikiPageVersion( postData.Body, postData.Creator, postData.TagList); page.Add(newVersion); } else { TempData["creator"] = postData.Creator; TempData["body"] = postData.Body; TempData["tags"] = postData.Tags; TempData["errors"] = postData.Errors; RedirectToAction(new { controller = "WikiPage", action = "EditPage", pageName = pageName }); return; } } RedirectToAction(new { controller = "WikiPage", action = "ShowPage", pageName = pageName }); }

First, it creates a NewVersionPostData object. This is another helper object that has properties and methods that store the contents of the post and some validation. To load up the postData object, I'm using a helper function from the MVC Toolkit. UpdateFrom is actually an extension method provided by the toolkit, and it uses reflection to match up the names of form fields to names of properties on my object. The net result is that all the field values are loaded into my postData object. Use of UpdateFrom does have the disadvantage, however, of getting form data directly from HttpRequest, making unit testing more difficult.

The first thing the NewVersion checks is SubmitAction. This will be OK if the user clicked the OK button and actually wants to post the edited page. If any other value is here, the action ends up redirecting back to ShowPage, which just redisplays the original page.

If the user did click OK, then I check the postData.Errors property. This runs some simple validations on the post contents. If there aren't any errors, I do the processing to write the new version of the page back into the wiki. However, if there are errors, things get interesting.

If there are errors, I set the various fields of the TempData dictionary so that it contains the contents of the PostData. Then I redirect back to the Edit page. Now, since the TempData is set, the page will redisplay with the form initialized with the values the user posted the last time.

This process of dealing with posts, validation, and TempData is a little aggravating right now and requires a bit more manual work than is really necessary. Future releases should include helper methods that automate at least some of the checking of TempData. One last note on TempData: the contents of TempData are stored in the user's server-side session. If you turn session off, TempData won't work.

Controller Creation

The basics of the wiki are now working, but there are a few rough spots in the implementation I'd like to clean up before going further. For example, the Repository property is used to decouple the logic of the wiki with the physical storage. You can provide repositories that store content on the file system (just as I've done here), a database, or wherever else you want. Unfortunately, I've got two problems to resolve.

First, my controller class is tightly coupled to the concrete FileBasedSpaceRepository class. I need to have a default, so that if the property's not set, I have something reasonable to use. Even worse, the path to the files on disk is hardcoded here as well. At the very least this stuff should come from configuration.

Second, the Repository is really a required dependency; my object won't run without it. Good design indicates that the repository should really be a constructor parameter, not a property. But I can't add it to the constructor because the MVC Framework requires a no-argument constructor on controllers.

Luckily, there's an extensibility hook that can get me out of this bind: the controller factory. A controller factory does much like the name says: it creates Controller instances. You just need to create a class that implements the IControllerFactory interface and register it with the MVC system. You can register controller factories for all controllers or just for specific types. Figure 13 shows a controller factory for WikiPageController, passing the repository now as a constructor parameter.

Figure 13 Controller Factory

public class WikiPageControllerFactory : IControllerFactory { public IController CreateController(RequestContext context, Type controllerType) { return new WikiPageController( GetConfiguredRepository(context.HttpContext.Request)); } private ISpaceRepository GetConfiguredRepository(IHttpRequest request) { return new FileBasedSpaceRepository(request.MapPath("~/WikiPages")); } }

In this case, the implementation is pretty trivial, but this can enable the creation controllers that use much more powerful tools (in particular dependency injection containers). In any event, now I have all the details of getting the dependencies for the controller separated into an object that's easier to manage and maintain.

The last step in making this work is to register the factory with the framework. I do this via the ControllerBuilder class by adding the following line to Global.asax.cs in the Application_Start method (either before or after the routes):

ControllerBuilder.Current.SetControllerFactory( typeof(WikiPageController), typeof(WiliPageControllerFactory));

This will register a factory for the WikiPageController. If I had other controllers in this project, they wouldn't use this factory, since it's registered only for the WikiPageController type. You can also call SetDefaultControllerFactory if you want to set a factory to be used for every controller.

Other Extensibility Points

The controller factory is just the start of the framework extensibility. I don't have space in this article to get into details about all of them, so I'll just hit the highlights. First, if you want to output something other than HTML or if you want to use a templating engine other than Web Forms, you can set the controller's ViewFactory to something else. You can implement the IViewFactory interface, and then you've got complete control over how the output is generated. This is useful for generating RSS, XML, or even graphics.

The routing system is quite flexible, as you've already seen. But there's nothing in the routing system that is specific to MVC. Every route has a RouteHandler property; so far, I've always set this to MvcRouteHandler. But it is possible to implement the IRouteHandler interface and hook the routing system up to other Web technologies. A future drop of the framework will be shipping with a WebFormsRouteHandler, and other technologies will be taking advantage of the generic routing system in the future.

Controllers don't have to derive from System.Web.Mvc.Controller. All a controller need do is implement the IController interface, which has only a single method called Execute. From there you can do whatever you like. If you only want to tweak a few behaviors of the base Controller class, on the other hand, Controller has many virtual functions you can override:

  • OnPreAction, OnPostAction, and OnError let you hook up generic pre- and post-processing on every action that gets executed. OnError gives you a controller-wide error handling mechanism.
  • HandleUnknownAction is called when a URL gets routed to the controller but that controller doesn't implement the action requested in the route. By default, this method throws an exception, but you can override it to do anything you'd like.
  • InvokeAction is the method that figures out what action method to call and calls it. If you'd like to customize the process (for example, to get rid of the requirement for [ControllerAction] attributes), this is the place to do it.

There are several more virtual methods on Controller, but they're primarily there as testing hooks rather than as extension points. For example, RedirectToAction is virtual so that you can create a derived class that doesn't actually redirect. This lets you test actions that redirect without needing a full Web server running.

Farewell Web Forms?

At this point you may be wondering, "What's happening to Web Forms? Is MVC replacing it?" The answer is no! Web Forms is a well-understood technology, and Microsoft will continue to support and enhance it. There are many applications where Web Forms work very well; the typical intranet database reporting app, for instance, can be created using Web Forms in a fraction of the time it would take to write it in MVC. Plus Web Forms supports a vast marketplace of controls, many of which are extremely sophisticated and save massive amounts of work.

So, when should you choose MVC over Web Forms? In many ways, it comes down to your requirements and your preferences. Are you struggling to get your URLs formed the way you want? Do you want to unit test your UI? Either of those scenarios would lean toward MVC. On the other hand, are you doing a lot of data display, with editable grids and fancy tree view controls? Then you're probably better off with Web Forms for the moment.

Over time, the MVC Framework will probably catch up in the UI control department, but most likely it'll never be as easy to get started with as with Web Forms, where massive amounts of functionality are just a drag and drop away. But in the meantime, the ASP.NET MVC Framework offers Web developers a new way to build Web applications in the Microsoft .NET Framework. The Framework is designed for testability, embraces HTTP instead of trying to abstract it away, and is extensible at just about every point. It is a compelling complement to Web Forms for those developers who want complete control over their Web applications.

Chris Tavares is a Developer on the Microsoft patterns & practices team, where he works to help the development community understand the best practices for building systems on Microsoft platforms. He is also a virtual member of the ASP.NET MVC team, helping to design the new framework. Chris can be contacted at cct@tavaresstudios.com.