Add post-backs to MVC –or- add front controller to Web Forms

You can download the code for this post at my CodePlex repository: http://palermo.codeplex.com/

Lots of programmers I talk to are managing software assets written in ASP.NET.  These applications have been in production for years, and they work.  The problem that is pervasive among all of them is that the Page_Load method is much too large.  Model-View-Presenter is fatally flawed as a pattern because the view obtains control before the presenter does.  This wrong ordering is an unrecoverable error in the workability of the pattern.  I have spent a number of years attempting to implement the MVP pattern well before I came to this conclusion.

This post contains a short example that inserts a controller in the request pipeline before the Web Form.  This allows large and bloated Page_Load methods to offload a bit of logic to a controller that executes in front of the Web Form.  Interestingly enough, this same technique allows an ASP.NET MVC page to leverage post-backs and server side controls.

Here is the solution view:

image

Default.aspx.controller.cs is the controller that executes ahead of Default.aspx.  The Visual Studio tooling doesn’t know to nest this file like I would like it.

Here is the page run the first time:

image

And here is the page after 3 clicks on the button that is an <asp:Button/>

image

Our page looks like the following:

<%@ Page Language="C#" AutoEventWireup="true" 
CodeBehind="Default.aspx.cs" 
Inherits="MvcApplication1.Controllers.Default" %>
<%@ Import Namespace="MvcApplication1"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <%var cust = (Customer) ViewData["model"]; %>
        
        <h1>Web Form with front controller</h1>
        Customer #: <%=cust.Id %><br />
        Name: <%=cust.Name %><br />
        Is Preferred: <%=cust.IsPreferred %>
        
        <asp:Button Text="Do a postback" runat="server" ID="btn" 
        onclick="btn_Click" />
    </div>
    </form>
</body>
</html>

Here is the code-behind:

using System;
 
namespace MvcApplication1.Controllers
{
    public partial class Default : ViewDataPage
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
 
        protected void btn_Click(object sender, EventArgs e)
        {
            btn.Text += " - " + DateTime.Now.ToString("hh:mm:ss");
        }
    }
}

Now, we can see that we are retrieving the Customer from ViewData.  But wait, Web Forms don’t have view data.  That’s correct.  That’s why I’ve made ViewDataPage an IViewDataContainer:

using System.Web.Mvc;
using System.Web.UI;
 
namespace MvcApplication1
{
    public class ViewDataPage : Page, IViewDataContainer
    {
        public ViewDataDictionary ViewData { get; set; }
    }
}

Now, here is the route that we are using:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcApplication1
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801
 
    public class MvcApplication : HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                "Default", // Route name
                "{controller}", // URL with parameters
                new {controller = "Default"}
                // Parameter defaults
                );
        }
 
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
 
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Now, what does the front controller look like?

using System.Web.Compilation;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcApplication1.Controllers
{
    public class DefaultController : Controller
    {
        protected override void Execute(RequestContext requestContext)
        {
            var customer = new Customer
                               {
                                   Id = 2,
                                   Name = "Jeffrey Palermo",
                                   IsPreferred = true
                               };
 
            ViewData.Add("model", customer);
            string controllerName =
                requestContext.RouteData.GetRequiredString("controller");
            string pagePath = string.Format("~/Controllers/{0}.aspx", 
                controllerName);
            var page = (ViewDataPage) BuildManager
                  .CreateInstanceFromVirtualPath(
                  pagePath , typeof (ViewDataPage));
            page.ViewData = ViewData;
            page.ProcessRequest(System.Web.HttpContext.Current);
        }
    }
}

Notice that I overrode the Execute method.  In this case, there is no need for the concept of an action.  We merely have an object that executes before the Web Form.  Then the controller builds the Web Form and asks it to process the request.

Half of this method could easily be factored into another class, but it works very simply.   I did not have to jump through any hoops.  I hammered out this code in about 10 minutes.

If your Page_Load methods are getting too long, consider putting a front controller in front of the page.


Trackbacks

ASP.NET MVC Archived Buzz, Page 1 Posted on 2.18.2010 at 2:53 PM

Pingback from ASP.NET MVC Archived Buzz, Page 1

ASP.NET MVC Archived Blog Posts, Page 1 Posted on 2.25.2010 at 10:00 PM

Pingback from ASP.NET MVC Archived Blog Posts, Page 1

Comments

Chris Benard said on 2.18.2010 at 2:26 PM

Very interesting. This may help with the nesting of the controller class:

aspnetresources.com/.../partial_class_f

joshka said on 2.18.2010 at 4:46 PM

Ricardo said on 2.18.2010 at 7:11 PM

"I hammered out this code in about 10 minutes." Yes, after spending more than a few years looking at a possible solution to the problem and actually writing a book about MVC... correct?

Thanks for sharing but this is in no way as simple as you suggest it is... I guess it would help applications that have a lot of code under Page_Load.

Abhilash said on 2.18.2010 at 8:33 PM

All I could say is amazing!

Thanks for sharing

Fernando Zamora said on 2.18.2010 at 9:50 PM

Nice! Thanks for sharing.

Hugo Bonacci said on 2.19.2010 at 11:02 AM

Cool stuff - I've messed around with the idea of getting WebControls to work inside of MVC. I've always felt that you can still use WebControls as an elegant way to write templates for HTML that avoid the whole server side tag mess by binding a WebControl to a Model.

Just in case you're curious - somewebguy.wordpress.com/.../webcontrols-in-

joe said on 2.22.2010 at 4:54 PM

Jeffrey -- I'm curious about this:

"I have spent a number of years attempting to implement the MVP pattern well before I came to this conclusion"

What's wrong with the (fairly) popular MyPage : MyPresenter<IMyView> approach where the init event hands off control to the presenter?

Paulo Morgado said on 2.22.2010 at 9:22 PM

The way WebForms work is by interpreting the request and firing a set of events based on the request. You just have to know what events to handle.

The view doesn’t necessarily have knowledge by itself what presenter should use (although it’s usually always the same).

When the view is loaded it can instantiate the presenter (based on whatever logic there is in place for that), the presenter does its “magic” and the page is rendered.

What is your controller doing that couldn’t be done by a presenter instantiated by a page?

With Web Forms, the HTML is totally decoupled from the presenter which doesn’t happen with MVC where the HTML is coupled to the Controller. That makes WebForms far better for interactions that MVC – and the right pattern for that is MVP.

Jim said on 3.16.2010 at 11:47 AM

How would you implement a unit test against this code. It seems like there is a lot of coupling in the Execute Method. For instance: System.Web.HttpContext.Current