Contents
Model-View-Controller and some variants
The prime sources of information for this article are: Martin Fowler,
Patterns of Enterprise Application Architecture,
MSDN and subsequent pages and Object Arts,
the creators of Dolphin Smalltalk.
Basic MVC
Although the title of this refers to MVC, it is really about different
techniques for presenting web pages. Basic MVC came out of the Smalltalk world
in the late 70's and appears to be pretty much built in to Smalltalk IDE's.
Possibly the canonical description is by
Steve Burbeck.
The static class diagram is simple:
In this diagram, Model represents domain classes such as Customer, Order, etc,
none of which have any knowledge of how the data they supply will be presented.
View and Controller both have knowledge of the Model; Controller additionally
knows about the View.
A variant of MVC which is sometimes mentioned is MVP: Model-View-Presenter, in
which the Presenter may be a composite of more atomic Presenters (e,g,
textPresenter).
In a basic MVC there is usually a one-to-one relationship between views and
controllers: each view has one controller and each controller uses one view. As
a result of this, in many cases View and Controller are combined - ASP pages,
WinForms forms, etc.
Extending basic MVC
Active Models
Basic MVC as described above employs a Passive Model: the model supplies data
when asked for it and responds to requests to change that data. However if the
model's data is changed somewhere else there is no way for the model to inform
any current presentation of that data. Enter the Active Model. This employs the
GoF Observer pattern where the Model exposes events which the View and/or the
Controller can subscribe to.
Multiple Controllers
It is possible for a single view to have multiple controllers - one for
Read/Write access, say, and one for read-only. This can be implemented by
making the controllers GoF Strategies which are given to the View as
appropriate.
Pros and Cons of basic MVC
Pros (these are mostly to do with the separation of the View from the Model):
-
Presentation and Model are fundamentally different and are developed with
different concerns and different techniques.
-
Separating the Model from its presentation allows multiple representations to
be developed.
-
Automated tests for a separate model are easy; automated tests for a UI are
complex to set up and fragile. Separating the model from its presentation
allows it to be tested relatively easily.
-
The Views can be changed without impacting the underlying Model or even the
Controller.
-
Multiple views of the Model are easy: having a Web, a WinForm, a WAP Phone, and
a character-based presentation have no impact on the model whatsoever.
Cons:
-
MVC introduces extra levels of indirection and thus complexity
-
Active Models introduce yet more complexity and can also, in extreme cases,
flood a view with update events.
-
Basic MVC concentrates on the separation of the Model and the View; the
controller is less important.
-
Controllers are likely to have a lot of duplicate code for tasks such as
authentication, parsing a query string, reading and writing Session
information, etc. This is inefficient.
Page Controller
This is a simple extension of basic MVC; the Controller is replaced by a Page
Controller and the separation of the View and the Controller is emphasised.
ASP.NET web forms follow this pattern, with the aspx file being the view and
the code-behind being the Page Controller.
Generally there is one Page Controller for each page on the site.
This is the first step away from basic MVC to take when using dynamic pages
rather than static.
A Page Controller has three basic responsibilities:
-
Decode the URL requested and extract any form data
-
Access the Model to retrieve or process the data requested. All HTTP data
required should be extracted and passed to the Model so that the Model objects
don't have any dependencies of their being an HTTP Context.
-
Determine which View is appropriate for the results and forward the Model
information to it.
Note that these responsibilities apply as much to an initial page request as to
a PostBack.
Looking at the responsibilities, it seems likely that some of the work on every
Page Controller in a site is going to be duplicated. This duplication can be
removed by either using helper classes (e.g. UrlDecoder) or by creating a
BaseController class containing the commonality with GoF Template methods for
the real Page Controllers to override and/or implement:
public class BaseController {
protected void Page_Load(object sender, EventArgs e) {
if (this.InvalidUrlForSomeReason) {
transferToErrorPage(invalidUrlReasons);
}
DataSet ds = LoadData();
if (HasNoData(ds)) {
transferToErrorPage("No data matched request");
}
ApplyDomainLogic(ds);
DataBind(ds);
}
protected virtual bool InvalidUrlForSomeReason() {
// validate url
// maybe set private string invalidUrlReason
}
protected abstract DataSet LoadData();
protected virtual void ApplyDomainLogic(DataSet ds) {
// do something with the data - add a calculated column etc
// or just leave blank as here if no logic to apply
}
}
The templated Page_Load method breaks down the request into a number of steps,
each of which can be overridden by Page Controller implementations. The
LoadData method in this example has to be implemented by each inheritor.
The DataBind method is properly part of the View (I am assuming that this
system makes heavy use of data binding; other methods can be used by adjusting
the template method appropriately).
There is nothing to prevent the base class - or its implementors - from using
Helper classes.
This example is more likely to be called ListBaseController, and have matching
DataEntryBaseControllers for example.
Pros and Cons of Page Controller
Pros:
-
Because each dynamic web page is handled by a specific Page Controller the
controllers have limited scope and are thus relatively simple.
-
This pattern is built-in in ASP.NET and is thus easy to implement.
-
Controller Base classes reduce duplication and promote re-use
-
If Page Controller is not suitable for every page in the site it need not be
used by every page in the site.
Cons:
-
In a site with complex dynamic navigation it is not always possible to abstract
the navigation completely to a Base controller. The navigation thus becomes
spread out over the site and difficult to maintain.
-
The Page Controllers are dependant on the Http context and are thus difficult
to test. If they contain too much logic this decreases the testability of the
system. The parts which do not depend on Http (or from which that dependence
can be removed such as the ApplyDomainLogic method above) can be removed to
separate classes but although these can then be tested separately it introduces
another layer of indirection and thus complexity.
Front Controller
This is more complex than Page Controller. Microsoft list the forces which make
it applicable as:
-
If common logic is duplicated in different views of the system; this needs to
be centralised to reduce duplication and increase maintainability
-
If data retrieval is best handled in a central location
-
BasePageControllers can bloat with code not common to all implementers.
-
If BasePageController is refactored in different base controllers to remove
unused abilities from inheritors, you can end up with a large deep inheritance
hierarchy. This can lead to brittle designs and implementations and is hard to
understand.
-
Page Controller breaks down when you need to control or coordinate a process
across multiple pages.
This pattern works by channelling all calls through one Handler.
The Handler then delegates to one of a number of GoF Command objects. This
Handler retrieves just enough information from the requested URL to decide
which type of action to initiate and then delegates to a Command to carry out
the action. The command does what it needs to and then chooses which view to
use to render the page:
The Handler is usually implemented as a class rather than a Page, as are the
Commands. The commands also should have no knowledge of HTTP although handed
HTTP-derived data by the Handler. The Handler can decide on the Command to use
(despatch) in a number of ways:
-
Statically: using conditional logic on the URL
-
Dynamically: putting the name of the Command in the URL
-
Dynamically: using a config file keyed on all or part of the URL to map to a
command class name
Static decision making has the advantage of explicit logic, compile-time type
checking, and flexibility in URL's. The Dynamic methods allow addition of
commands and pages without changing the Handler.
The Handler can also be Decorated by what are known as Intercepting Filters to
add behaviour to the Handler. Examples would deal with things like
authentication, logging, locale. As a decorator, these Intercepting Filers can
be applied multiple times creating a Filter Chain which could be loaded
dynamically.
There is a variation which splits the Handler into two: one which received the
Get or Post and interacts with HTTP to strip out the data (the ExamineUrl
method in the sequence diagram above). It then hands that to a �despatching'
Handler which knows nothing about HTTP, but makes the decision on which command
to use. The advantage of this is that the coupling to HTTP is reduced making
automated testing easier.
Pros and Cons of Front Controller
Pros:
-
The single Handler is in the perfect place to apply system-wide policies with
no duplication
-
Since each Command is instantiated as a new object, Thread Safety is not an
issue. (Note that it is an issue with the Model.)
-
Using Dynamic despatching is very flexible
Cons:
-
The Handler can become a performance bottle-neck. If it has to make a database
query or parse say a large complex XML document to decide which command to
despatch performance could be affected.
-
Front Controller is more complex than Page Controller. MSDN has an example
implementation of in which it says: "Because Page Controller is built into
ASP.NET, the additional effort required to implement Front Controller rather
than Page Controller is very large. In fact, you must build the whole framework
for Front Controller. You should do so only if your application warrants that
amount of complexity. Otherwise, review Page Controller to determine whether it
is sufficient."
-
The is no way inherent in the pattern to send the data collected by the
ConcreteCommand into the View. One could always stuff a DataSet into the
session object, but this re-introduces a dependency on the HTTP context into
the Command. Other schemes are imaginable, though.
In other words, it's neat and it's powerful but make sure you really need it
before sailing off into the complexities!
Page Controller Implementation
In this implementation, taken from
MSDN, we have a BasePage web form with a blank form containing the
following code in addition to the generated stuff:
protected Label eMail;
protected Label siteName;
virtual protected void PageLoadEvent(object sender,
System.EventArgs e) {
}
protected void Page_Load(object sender, System.EventArgs e) {
if(!IsPostBack) {
string name = Context.User.Identity.Name;
eMail.Text = DatabaseGateway.RetrieveAddress(name);
siteName.Text = "PageControlled-site";
PageLoadEvent(sender, e);
}
}
This form is the Base Controller for other pages, and puts the email address of
the validated user in the header of each on the implementing forms. The email
address is retrieved from the DatabaseGateway class, which stands for the Model
in this example. Each implementing form has the following line:
<!-- #include virtual="BasePage.inc" -->
This include file contains:
<table width="100%" cellspacing="0" cellpadding="0" ID="Table1">
<tr>
<td align="right" bgcolor="#9c0001" cellspacing="0"
cellpadding="0" width="100%" height="20">
<font size="2" color="#ffffff">Welcome:
<asp:Label id="eMail" runat="server">username</asp:Label>
</font>
</td>
</tr>
<tr>
<td align="right" width="100%" bgcolor="#d3c9c7" height="70">
<font size="6" color="#ffffff">
<asp:Label id="siteName"
Runat="server">Micro-site Banner</asp:Label>
</font>
</td>
</tr>
</table>
This sets up the header of the page and includes two ASP.NET labels - "eMail"
for the email address and "siteName" for the site name. These are filled in by
the Page_Load event in the BasePage. After filling them in, BasePage calls
PageLoadEvent, a virtual function which will be overridden in implementing Page
Controller pages, such as:
namespace ghytred.PageController {
public class Page1 : BasePage {
protected System.Web.UI.WebControls.Label pageNumber;
#region Web Form Designer generated code
#endregion
protected override void PageLoadEvent(object sender,
System.EventArgs e) {
pageNumber.Text = "1";
}
}
}
This simple page has a label which it fills in with a page number, but could
have far more. This is an unrealistically simple case for Page Controller and
BaseController but should get the idea across.
Front Controller Implementation
In keeping with the realism of the Page Controller example above, let's say
that we now want to change the site name and get the email address from
different databases depending on the URL. In the Page Controller example we can
change the BasePage class as follows:
protected void Page_Load(object sender, System.EventArgs e) {
if(!IsPostBack) {
string site = Request["site"];
if (site != null && site == "macro") {
LoadMacroHeader();
} else {
LoadMicroHeader();
}
PageLoadEvent(sender, e);
}
}
The LoadxxxHeader methods get the address and set
the site name appropriately and we're done. However the base class now contains
conditional logic; in this case it's probably OK, but in more complex cases it
can become problematic quite quickly. Hence we'll do this with Front
Controller.
For Front Controller, we need a Handler:
using System;
using System.Web;
namespace ghytred.FrontController {
public class Handler : IHttpHandler {
public Handler() {
}
public void ProcessRequest(HttpContext context) {
ICommand command =
CommandFactory.Command(context.Request.Params);
command.Execute(context);
}
public bool IsReusable {
get { return true; }
}
}
}
We need to have a Command (we're using an interface - ICommand - in this
example; there is no useful inherited behaviour). We also use a factory to
return the appropriate command:
using System;
using System.Web;
using System.Collections.Specialized;
namespace ghytred.FrontController {
public interface ICommand {
void Execute(HttpContext context);
}
public class CommandFactory {
private CommandFactory() {
}
public static ICommand Command(NameValueCollection parameters) {
string siteName = parameters["site"];
switch (siteName) {
case "Macro":
return new MacroCommand();
case "Micro":
return new MicroCommand();
default:
return new UnknownCommand();
}
}
}
}
Now we configure
the site to use the Handler by adding the following:
<httpHandlers>
<add verb="*" path="Page*.aspx"
type="ghytred.FrontController.FCHandler,FrontController" />
</httpHandlers>
in the system.web section of the Web.Config.
OK, this is all set up to call our
handler (class FrontController in assembly ghytred.FrontController.FCHandler.dll)
on every access (verb="*") of URL's pointing to pages with names of the
form Page*.aspx. The Handler is set up to get the appropriate Command object,
and tell the command to execute the request. What are the commands like?
Each command is going to get the user's email address and the site name, and store
them in the HttpContext. They are then going to redirect the request to a page
that implements the View required.
Each command object will inherit from an
abstract RedirectingCommand:
public abstract class RedirectingCommand : ICommand {
protected abstract void OnExecute(HttpContext context);
public void Execute(HttpContext context) {
OnExecute(context);
string url = String.Format("{0}?{1}",
UrlMap.Instance[context.Request.Url.AbsolutePath],
context.Request.Url.Query);
context.Server.Transfer(url);
}
}
This abstract
class implements the Execute method and provides a hook for its inheritors -
OnExecute - to slip in their own stuff. It calls this hook and then uses a
utility class (UrlMap) to translate the Url requested to the actual page which
implements the view. Finally, it transfers to that page.
The Commands look like:
public class MacroCommand : RedirectingCommand {
protected override void OnExecute(HttpContext context) {
string name = context.User.Identity.Name;
context.Items["address"] =
MacroUsers.retrieveAddress(name);
context.Items["site"] = "Macro-site";
}
}
public class MicroCommand : RedirectingCommand {
protected override void OnExecute(HttpContext context) {
string name = context.User.Identity.Name;
context.Items["address"] =
MicroUsers.retrieveAddress(name);
context.Items["site"] = "Micro-site";
}
}
public class UnknownCommand : RedirectingCommand {
protected override void OnExecute(HttpContext context) {
string name = context.User.Identity.Name;
context.Items["address"] =
"user " + name + " is unknown";
context.Items["site"] = "Unknown";
}
}
UrlMap would most likely work off a
configuration file, but this example is hard-coded:
public class UrlMap {
private UrlMap() {
}
// Show off Singleton implementation
private static UrlMap instance;
private static object thingToLock = new object();
public static UrlMap Instance {
get {
if (instance == null) {
lock(thingToLock) {
if (instance == null) {
instance = new UrlMap();
}
}
}
return instance;
}
}
public string this[string requestedPage] {
get {
// prefix page name with "Actual"
string[] parts = requestedPage.Split('/');
int i = parts.Length - 1;
if (i < 0) {
i = 0;
}
parts[i] = "Actual" + parts[i];
string actualPage = String.Join("/", parts);
return actualPage;
}
}
}
So now our Handler catches all requests for
Page*.aspx pages; gets hold of an ICommand object which does some work and
translates the request to a page which creates the view required and transfers
the call on to the view.
We will have the same structure as with the Page
Controller - a BasePage plus two implementations. The BasePage class has
changed though. Now it simply retrieves the data the Commands stored in the
Context and puts them in the Labels; it's Page_Load looks like this:
protected void Page_Load(object sender, System.EventArgs e) {
if(!IsPostBack) {
eMail.Text = (string)Context.Items["address"];
siteName.Text = (string)Context.Items["site"];
PageLoadEvent(sender, e);
}
}
The actual views
will be called ActualPagexxx.aspx. They are exactly the same as the Page1.aspx
in the Page Controller example - save that they should be called
ActualPage1.aspx etc.
Notes on the implementations
If the page to be shown has
to show data from a database, rather then a hard-coded number, then in Page
Controller the work to get the data would have to be done in the View
(Page1.aspx). While the BasePage can set something in the Context, say, to
indicate which database should be used (Micro or Macro) the page itself has to
test that and then do the work - unless it is abstracted out into the BasePage
along with the email lookup. However, now the base page contains code
applicable only to this particular query; perhaps we should have a different
base page for other views which want different data. Now we're starting to
build up a hierarchy of bases and the system is getting complex. Still, if all
we want is to display a small amount of data from varying sources - or use a
different CSS for instance - then it looks promising.
The Front Controller
looks promising for cases where there is more work to be done than in this
example. True there is a load of extra complexity is setting it up, but once
set up it's easy to add more commands without changing the Handler. Also the
Handler can be decorated with Filters to add new functionality; this would
require adding a HandlerFactory (implementing IHttpHandlerFactory) which adds
the decorators and specifying that factory in the Web.Config instead of the
Handler, but that's not difficult.
So Front Controller is more flexible and
allows simpler Views (in real cases, not the noddy bit of fluff above) than
Page Controller. Front Controller is also Open for Extension and Closed to
Modification. This is an important principle of OO design and Front wins here
hands down over Page Controller. It's open to extension because all that is
required to add extra functionality is to add more commands or more Handler
decorators; you don't have to modify existing code to do that as you would in
Page Controller.
On the other hand, Front Controller could lead to performance
problems, and this must be watched for. Also, as MSDN says:
Cruel and unusual punishment. This implementation is a lot more complicated than Page Controller.
This implementation does provide more options, but at the cost of complexity
and a lot of classes. You must weigh whether or not it is worth it. After you
have taken the leap and built the framework, it is easy to add new commands and
views. However, due to the implementation of Page Controller in ASP.NET, you
would not expect to see as many implementations of Front Controller as you
would in other platforms.
I think I'll leave it at that.