Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Model-View-Controller using ASP.NET WebForms View Engine

0.00/5 (No votes)
1 Jun 2009 1  
The purpose of this article is to demonstrate a sample framework that uses the MVC pattern successfully with the traditional ASP.NET WebForms engine.

Introduction

With the release of the ASP.NET MVC (Model-View-Controller) view engine, many developers are rejoicing that the old pattern has finally made it to the .NET world. In reality, there have been MVC view engines in existence for ASP.NET for quite some time. Contrary to popular belief, the common WebForms view engine, which is the default ASP.NET rendering engine so many developers have been familiar with, readily supports the Model-View-Controller paradigm. The purpose of this article is to demonstrate a sample framework that uses the MVC pattern successfully with the traditional ASP.NET WebForms engine.

Background

Model-View-Controller has been around for quite some time, and is attributed to the SmallTalk language/platform. You'll find, like many design patterns, that actual explanations and implementations of the MVC pattern may vary. It is not my intent to provide a "pure" solution adhering to some golden standard, but to present a variation of MVC that works well with ASP.NET WebForms and explain what and why it is being used.

There are three intrinsic parts to MVC, and these are described as follows:

The Model

The model is often referred to as the "domain entity" and is essentially data. It is a container for information that is moving through your system. Typically, a model should contain attributes and properties but little to no function nor operation, short of constructors and possibly validation. An example of a model would be contact information or a set of credentials.

The View

The view takes the data from the model and presents it to the end user as meaningful information. A view might render a list as a bulleted list, a drop down, a multi-select box, or even a paged grid. There might be a view for a console that emits text, and another view for a rich web interface that contains 3D graphics controls.

I consider the following rules important to follow for a view:

  • A view only understands the model and its own "rendering space" (web, console, WinForm, etc.).
  • The view is primarily concerned with mapping the model to the render space, and possibly receiving changes from that render space (i.e., user input).
  • Views should only manipulate data to the extent that it is necessary to pull from the model or place back within the model. Business logic belongs elsewhere (we'll discuss this in a minute with the controller).
  • Views do not understand how to write data to databases (or retrieve it), and are unaware of complex logic such as security concerns or calculations. Instead, views expose properties that allow their conditions to be set by another mechanism that understands these.
  • Views receive information via exposed properties and methods.
  • Views are like most people in the movie "The Matrix" ... they are completely unaware that they are being controlled, and have absolutely zero affinity to a controller.
  • Because views are ignorant of being controlled, if a view requires additional information or is responding to a command from the UI such as "delete" or "update," the view is only able to marshal these values into a container and raise an event. The view raises the event and forgets about it.

The separation of the view from the business logic is important. In a rapid prototyping environment, when we need a UI for proof of concept, we don't have time to wire in the backend services. A view that is ignorant of control can be easily set up with a "mock" controller that simply pushes static data for the prototype. On the flipside, in a production environment, there may be a controller that manages lists of entities but must present these differently based on the user. In this case, multiple views can be invoked with the same controller. On a large development team, a view that is cleanly separated from its control can be developed independently of the controller itself, allowing, for example, the mobile device team to build the UI on the device while the Silverlight experts build their own rich control.

The Controller

Finally, we have the controller. The controller is responsible for handling all of the business logic, fetching data, processing changes, responding to events, and making it all work. The controller is aware of both the view and the model. However, I believe a controller should also follow a set of its own rules to fit solidly within an enterprise software environment:

  • A controller should never understand which concrete instance of a view it is working with but only interact with a view interface.
  • The controller should never, ever concern itself with view logic. This means controllers do not emit HTML fragments, do not understand drop downs, and don't have to worry about JavaScript. The controller simply deals with lists of models and events that contain models and nothing else. A true test of a controller is that it functions perfectly whether the view is a console output, a WinForm, or a web interface.
  • Controllers should never try to interact with another controller's views. Instead, controllers can raise their own events for consumption by other controllers.
  • Controllers communicate to views by setting attributes or calling methods on the view, and by handling events raised by the view.
  • Controllers should never talk directly to another controller, only to a controller interface (therefore, a "super" controller might have sub-controllers, but again, it is interacting with interfaces, not concrete instances).

By following these rules, you can have a flexible yet powerful controller architecture. One controller can generate the static models needed for a prototype. Another controller might set up some mock objects to run Unit Tests, while the production controller interacts with services to pull lists of models and manipulate the models by responding to events.

mvcwebforms_1.png

Traditional, out-of-the-box ASP.NET seems to violate these principles. While the code-behind separates the code from the display elements, there is a strong affinity between the two. In fact, most pages must embed a specific, static reference to user controls in order for them to function. This creates a dependency that doesn't allow the flexibility we discussed. Furthermore, doing things like taking a text field value and moving it into the model and sending to the service violates the rules of the controller not understanding the nuances of the view or the view not knowing its controller.

Fortunately, through the use of a framework to define the views as controls and controllers, along with dynamic user controls, we can build a platform that adheres to the tenants of MVC. Dynamic user controls are very important so that the controller can deal with the view interface and a factory or other mechanism can invoke the concrete instance of the view being used at runtime. The included application presents this framework, and provides a proof-of-concept for showing a controller and two views that are controller-ignorant yet can still interact through the use of events.

Using the Code

The included code is a proof-of-concept implementation of the MVC pattern using WebForms. The source application includes a full-blown framework to support the application. The "model" exists in our domain projects with the purpose to describe the business data. In our example, this is a set of reserved keywords for the C# language. The KeywordModel contains a unique identifier for the keyword and the value which is the keyword itself.

The code is straightforward, and should compile "out of the box". You can right-click on the Default.aspx page to view. You'll see a very plain page with two columns: one with a "currently selected" keyword and a dropdown, another with a list of all of the keywords, and finally a Submit button. When you select a keyword, it should dynamically show next to the "Selected" label, and the corresponding keyword in the right column will become bold. When you click Submit, the last selected keyword will render red in the right column to demonstrate the server-side actions that "remember" the recent selection (there is no query string nor forms parsing in the application).

I created an Interface.Data to describe interactions with persisted data. There is a Load and a List function. To save time, instead of wiring into a full-blown database, I simply embedded an XML resource and used a concrete class called KeywordDataAccess to pull values from the XML. In my applications, I try to keep the data layer as focused as possible (reads, writes, updates). Any business logic such as calculations and sorts are in the service layer that sits on top of the data layer. You'll find that everything above the data layer references an IDataAccess<KeywordModel>, not a concrete instance.

The service layer simply invokes the data layer to grab the data. Note that we use the Factory pattern to grab our concrete instance. This will allow us to stub out "mock" data layers for Unit Tests or even swap the XML data layer with SQL, Access, or other data layers without having to change anything else but the concrete class for the data layer and the instance that the factory returns. For a larger application, of course, a Dependency Injection framework would help wire these in — hence the constructor that takes the data access reference for the constructor injection. In this case, the only "extra" handling of the domain model that the service is required to do is to perform a sort of the data prior to presenting it (see the sort in the List method).

Finally, we get to the presentation layer, serviced as a web application. We've already discussed the model that contains the business data. Now, we have two more pieces: the view, and the controller. Let's talk about the controller first.

For the application to truly scale, the controller should function independently of the specific view it needs to control. A controller is simply of type T (where T is a model), and can operate on any view of type T. The following diagram illustrates the controller interface and implementation.

mvcwebforms_2.png

Note that the controller itself simply has a constructor that takes in the view it will be managing. Everything else is handled by the base class, DynamicController<T>, that implements IController<T>. There are a few things we have wired into our base controller:

  • Context — a GUID is generated to manage state through callbacks and postbacks.
  • The controller knows the type (T) it is managing, so our model allows us to simply go to the ServiceFactory and get the default service for T. Note that a SetService method is also exposed for injection of the service for things like Unit Tests using mocked services.
  • The controller keeps track of its view (_control) and exposes events for other controllers. Remember our rule: controllers talk to controllers and their own control, controls only raise events and are ignorant of being controlled.
  • There is a list of models along with a "current" model.
  • The controller responds to a "select" event. If we were performing CRUD or deletes, etc., we would process this in the controller and then pass it through. In our sample, we just pass it through so any higher-level controllers can respond to the event as well.
  • Finally, we have an Initialize method that is meant to be invoked the first time the controller is created. Subsequent postbacks and callbacks do not call this method, and internal lists are managed via state. More on that when we discuss the controls.

The interface is simple:

using System;
using Interface.Domain;

namespace Interface.Controller
{
    /// <summary>
    ///     Interface for a controller 
    /// </summary>    
    public interface IController<T> where T : IKey, new() 
    {
        /// <summary>
        ///     Called the first time to initialize the controller
        /// </summary>
        void Initialize();

        /// <summary>
        ///     Raised when a model is selected
        /// </summary>
        event EventHandler<EventArgs> OnSelect;

        /// <summary>
        ///     Current active or selected model
        /// </summary>
        T CurrentModel { get; set; }

        /// <summary>
        ///     Context (state management)
        /// </summary>
        Guid CurrentContext { get; set; }
    }
}

...and the base controller class:

/// <summary>
///     Controller base class
/// </summary>
/// <typeparam name="T">The type to control</typeparam>
public class DynamicController<T> : IController<T> where T : IKey, new()
{
    /// <summary>
    ///     Manages the state of the controller and control
    /// </summary>
    public Guid CurrentContext
    {
        get { return _control.CurrentContext; }
        set { _control.CurrentContext = value; }
    }

    /// <summary>
    ///     Inject the control and grab the default service
    /// </summary>
    /// <param name="control"></param>
    public DynamicController(IControl<T> control)
    {
        _control = control;
        _service = ServiceFactory.GetDefaultService<T>();
        _control.NeedData += _ControlNeedData;
        _control.OnSelect += _ControlOnSelect;
    }

    /// <summary>
    ///     Fired when the child control raises the select event
    /// </summary>
    /// <param name="sender">Sender</param>
    /// <param name="e">Args</param>
    private void _ControlOnSelect(object sender, EventArgs e)
    {
        if (OnSelect != null)
        {
            OnSelect(this, e);
        }
    }

    /// <summary>
    ///     Needs the list again
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void _ControlNeedData(object sender, EventArgs e)
    {
        _control.ControlList = _service.List();
    }

    /// <summary>
    ///     Fired when a model is selected from the list
    /// </summary>
    public event EventHandler<EventArgs> OnSelect;

    /// <summary>
    ///     The control this controller will work with
    /// </summary>
    protected IControl<T> _control;

    /// <summary>
    ///     Service related to the entity
    /// </summary>
    protected IService<T> _service;

    /// <summary>
    ///     Current active or selected model
    /// </summary>
    public virtual T CurrentModel
    {
        get { return _control.CurrentModel; }
        set { _control.CurrentModel = value; }
    }

    /// <summary>
    ///     Allows injection of the service
    /// </summary>
    /// <param name="service">The service</param>
    public virtual void SetService(IService<T> service)
    {
        _service = service;
    }

    /// <summary>
    ///     Called the first time to initialize the controller
    /// </summary>
    public virtual void Initialize()
    {
        _control.Initialize(_service.List());
    }
}

Now that we have a decent understanding of the controller, let's move on to the controls. The project defines two controls for the same model: KeywordDropdownView and KeywordListView.

mvcwebforms_3.png

Both of these views implement IControl<T>, the view for the KeywordModel entity. The controls are a bit more in-depth. You'll note the controls contain an IControlCache which allows the controls to persist internal state. For example, instead of going out to the service (and hence, data) layers to request a list each time, the control can store these lists in the cache. When the cache expires, the control simply raises the NeedData event and the controller will supply a new list.

Two examples of the IControlCache implementation are included to give you some ideas of how to code this. One, SimpleCache, simply shoves the objects in the Session object. The other takes advantage of the ASP.NET Cache object and stores the items in cache for 5 minutes.

Each control is wired to receive a context: the unique GUID that identifies the instance. This solves a common problem. Many developers are happy to store objects in session with generic keys, like this:

...
Session["ControlList"] = ControlList;
...

The problem is that if you open multiple tabs in the same browser, each tab now fights for the same session variable and you can collide across pages. Generating a GUID in the page and using the GUID for caching and session storage ensures that each instance in the browser, even when they share the same session, is managed appropriately.

The view contract:

/// <summary>
///     Interface for a generic control
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IControl<T> where T : IKey, new()
{
    /// <summary>
    ///     Initializes the control with the list of items to manage
    /// </summary>
    /// <param name="list"></param>
    void Initialize(List<T> list);

    /// <summary>
    ///     Raised when something is selected from the list
    /// </summary>
    event EventHandler<EventArgs> OnSelect;

    /// <summary>
    ///     Raised when the control needs data again
    /// </summary>
    event EventHandler<EventArgs> NeedData;

    /// <summary>
    ///     The list for the control
    /// </summary>
    List<T> ControlList { get; set; }

    /// <summary>
    ///     The current active model
    /// </summary>
    T CurrentModel { get; set; }

    /// <summary>
    ///     Caching mechanism for the control to save/load state
    /// </summary>
    IControlCache ControlCache { get; set; }

    /// <summary>
    ///     Context (state management) for the control
    /// </summary>
    Guid CurrentContext { get; set; }
}

And the view base class:

/// <summary>
///     A dynamic control
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class DynamicControl<T> : UserControl, 
                IControl<T> where T : IKey, new()
{
    /// <summary>
    ///     Context for this control
    /// </summary>
    private Guid _context = Guid.Empty;

    /// <summary>
    ///     Cache for the control
    /// </summary>
    public IControlCache ControlCache { get; set; }

    /// <summary>
    ///     Unique context for storing state
    /// </summary>
    public Guid CurrentContext
    {
        get
        {
            if (_context.Equals(Guid.Empty))
            {
                _context = Guid.NewGuid();
            }

            return _context;
        }

        set
        {
            _context = value;
            LoadState();
        }
    }

    /// <summary>
    ///     List that the control works with
    /// </summary>
    public virtual List<T> ControlList { get; set; }

    /// <summary>
    ///     The current selected model
    /// </summary>
    public virtual T CurrentModel { get; set; }

    /// <summary>
    ///     Initializes the control with the list of items to manage
    /// </summary>
    /// <param name="list"></param>
    public virtual void Initialize(List<T> list)
    {
        ControlList = list;
    }

    /// <summary>
    ///     Allow override of tis event
    /// </summary>
    public virtual event EventHandler<EventArgs> OnSelect;

    /// <summary>
    ///     Need data?
    /// </summary>
    public virtual event EventHandler<EventArgs> NeedData;

    /// <summary>
    ///     Load event - allow things to wire
    ///          and settle before trying to bring in state
    /// </summary>
    /// <param name="e"></param>
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        LoadState();
    }

    /// <summary>
    ///     Last thing to do is save state
    /// </summary>
    /// <param name="e"></param>
    protected override void OnUnload(EventArgs e)
    {
        SaveState();
        base.OnUnload(e);
    }

    /// <summary>
    ///     Save the control state
    /// </summary>
    public virtual void SaveState()
    {
        if (ControlCache != null)
        {
            object[] savedState = new object[] {ControlList ?? new List<T>(), 
                                                CurrentModel};
            ControlCache.Save(CurrentContext, savedState);
        }
    }

    /// <summary>
    ///     Load the control state
    /// </summary>
    public virtual void LoadState()
    {
        if (ControlCache != null)
        {
            object[] loadedState = ControlCache.Load(CurrentContext) as object[];
            if (loadedState != null && loadedState.Length == 2)
            {
                ControlList = loadedState[0] as List<T> ?? ControlList;
                CurrentModel = (T) loadedState[1];
            }
        }
    }
}

The KeywordDropdownView view simply takes the list of models from the controllers and renders them into a drop down. It contains some JavaScript code (embedded as a resource, this is covered in more detail in JavaScript and User Controls 101) that responds to a selection. The example demonstrates how a control can be responsive on both the client and server sides. When a new keyword is selected in the dropdown, it raises an event on the client that other controls can subscribe to the called keywordDropdownChanged. It then does a callback to the control on the server side, which raises the OnSelect event for server-side management. We'll examine these events in more detail.

Instead of my favorite JQuery library add-on, I decided to use traditional JavaScript that is cross-browser compatible (I tested on IE 6, IE 7, IE 8, and FireFox). This will give you some examples of DOM manipulation on the fly as well as one way to wire in a callback.

The KeywordListView uses a simple Repeater to list the keywords as labels, which render as DIV tags on the client. Again, this is a proof of concept to show two interactions. First, on the client, the control registers for the keywordDropdownChanged event. When the event is raised, it searches through its own rendered list of keywords and changes the target to bold. You can see this happen on the client as an example of two controls talking to each other without any knowledge of their existence or implementation.

The second pieces is that the main page itself acts as a "master" controller. It responds to the OnSelect event from the dropdown control, and sends the selected keyword to the list control. The list control persists this information, and colors the keyword red when it renders a new list. You can see this behavior by selecting a keyword and then clicking Submit. Notice that all of the interactions are done through events, not some awkward mechanism that is parsing form data and responding by calling various controls directly.

The final piece that really ties the MVC concept together is the main page. Here, you'll see we are only dealing with abstractions (IController<KeywordModel>). This is where I feel my framework breaks with many traditional models I've seen. Most attempts at MVC in WebForms still require a strong affinity between the page and the control. How many times have you found yourself embedding a user control by using the <%Register%> tag to point to a control? This leaves little room for flexibility (for example, grabbing a different view if the user is on a PDA, but using the same controller).

In our example, we simply put placeholders in the page where the views can render. We use a view factory to grab the view. There is a default view mapped to the dropdown, and a more specific view requested for the Repeater. The same controller manages both views, proof of true abstraction of the controller from the view it is controlling. The page, as a "master" controller, generates a context that both controllers can share, and coordinates the controllers by taking an event from one and pushing the value to the other. The controllers themselves are ignorant both of each other's existence and the actual implementation of the view they manage — despite the dropdown and list, the controller still simply works with an IControl<KeywordModel>.

This is the "skeleton" for the page:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="Default.aspx.cs" Inherits="DynamicControls.Default" %>

<!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>Dynamic Control Example</title>
</head>
<body>
    <h1>Dynamic Control Example</h1>
    <form 
        id="_form1" 
        runat="server">
        <asp:ScriptManager ID="_sm" runat="server" />
        <asp:HiddenField ID="_hdnGlobalContext" runat="server" />
        <table>
            <tr valign="top">
                <td>
                    <asp:Panel 
                        ID="_pnlLeft" 
                        runat="server" />
                </td>
                <td>
                    <asp:Panel 
                        ID="_pnlRight" 
                        runat="server" />
                </td>
                <td>
                    <asp:Button 
                        ID="_btnSubmit"     
                        runat="server" 
                        Text=" Submit " />                
                </td>
            </tr>
        </table>        
    </form>
</body>
</html>

And, here is the "master controller" code-behind. Notice how we reference the interface and the factory. We wouldn't even reference the control at all except to manage the event args, this would normally be hidden inside a "master controller."

using System;
using System.Web.UI;
using Domain;
using DynamicControls.Control;
using DynamicControls.Factory;
using Interface.Controller;

namespace DynamicControls
{
    /// <summary>
    ///     Default page
    /// </summary>
    public partial class Default : Page
    {
        /// <summary>
        ///     Controller for the page
        /// </summary>
        private IController<KeywordModel> _ctrlrLeft;

        /// <summary>
        ///     Another controller
        /// </summary>
        private IController<KeywordModel> _ctrlrRight;

        private Guid _context = Guid.Empty;

        /// <summary>
        ///     Init
        /// </summary>
        /// <param name="e"></param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            _ctrlrLeft = ControllerFactory.GetDefaultController(
              ViewFactory.GetDefaultView<KeywordModel>(_pnlLeft));
            _ctrlrRight = ControllerFactory.GetDefaultController(
              ViewFactory.GetKeywordListView(_pnlRight));

            _ctrlrLeft.OnSelect += _CtrlrLeftOnSelect;
        }

        /// <summary>
        ///     This is when viewstate is first available
        ///     to us to wire in the appropriate context
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreLoad(EventArgs e)
        {
            base.OnPreLoad(e);
            // bind a global context so the controllers and controls
            // all can talk to each other with the same 
            // instance of state
            if (string.IsNullOrEmpty(_hdnGlobalContext.Value))
            {
                _context = Guid.NewGuid();
                _hdnGlobalContext.Value = _context.ToString();
            }
            else
            {
                _context = new Guid(_hdnGlobalContext.Value);
            }

            _ctrlrLeft.CurrentContext = _context;
            _ctrlrRight.CurrentContext = _context;
        }

        /// <summary>
        ///     Let the right hand know what the left hand is doing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void _CtrlrLeftOnSelect(object sender, EventArgs e)
        {
            KeywordSelectArgs args = e as KeywordSelectArgs;
            if (args != null)
            {
                _context = args.CurrentContext;
                _ctrlrRight.CurrentContext = _context;
                _ctrlrRight.CurrentModel = args.SelectedModel;
            }
        }

        /// <summary>
        ///     Page load event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsCallback && !IsPostBack)
            {
                _ctrlrLeft.Initialize();
                _ctrlrRight.Initialize();
            }
        }
    }
}

I purposefully used a straight script reference for the list control (instead of an embedded resource) so you can get a feel for the differences between the two options.

Note how the state management works in the list control. It shoves the list and the selected model into an array of objects, and then sends these to the IControlCache. The GUID controls the keys for the cache. The master page sets this initially. When the dropdown fires the callback, it passes the context to the server so the server can reset the controls with the context and make sure they are pulling the correct state. Otherwise, the control would have no "idea" of what the list was or what the currently selected model is.

That explains the sample application and the framework. You can play with a working copy of it at http://apps.jeremylikness.com/mvcwebform/. This is a bare bones proof-of-concept, no fancy graphics or fonts.

Points of Interest

I always like to view the source and see just how much is being rendered: what and why. Of course, the first thing you'll notice is the view state. Because we are managing our own state via the GUID that is embedded in the page (just view source and search for _hdn), we could actually turn viewstate off.

ASP.NET wires in the __doPostBack function which is bound to controls like the Submit button. After this function, you can see the JavaScript for our custom controls. The first embedded one references WebResource.axd which is responsible for pulling the JavaScript out of the assembly and rendering it to the browser. The next is a straight reference to KeywordListView.js because we did not embed it. This is followed by a few includes that wire up the AJAX framework, and then we're in the midst of our actual controls.

Note the wired "onchange" on the select box. We bound this on the server side and emitted the context as well as the client IDs of the various controls. We do this because the code must work no matter how nested the control is. Each control wires its respective init function, and the last code emitted is the initialization for the AJAX framework itself.

To extend this framework, you will want to create different types of views (IListControl, IUpdateControl, etc.) that perform different functions (the most fun I had with my own company was creating the controller and control for grids that manage large data sets and paging). You can even experiment with Unit Tests and building "test" controls that don't inherit from UserControl and therefore don't require HttpContext to work. There are lots of possibilities, but hopefully, this example forms a decent foundation for you to work from.

History

This is the first release of this example.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here