I apologize to my faithful readership for being incommunicado for the past several days. I know you are all dying to see me bring all this MVP nonsense together into something useful. So strap yourself in and get ready for a wild ride. For those of you just joining us make sure you read through my previous posts on MVP and the Castle MicroKernel before you try to digest the information in this post.
I want to start by showing you this screen shot of our sample BatchTracker application in Visual Studio
. What your are looking at is the Web Application that will host our previously created Presenters and Views. Notice anything strange here? We have no references to any of our other BatchTracker assemblies! Our Web Application knows nothing about BatchTracker.Views or BatchTracker.Presenters. Why is this a big deal? We now have a completely generic hosting application that can easily be reused in future projects. What's more, we can add new View/Presenter combinations as needed without touching or re-compiling our Web Application Project. Our neat MVP Framework takes care of everything with a sprinkle of additional magic.
Most of the MVP magic comes courtesy of the Castle MicroKernel covered in small detail in my previous posts. We somehow need to incorporate the MicroKernel into our Web Application. The best place to do that is in Global.asax because we can tap into Application_Start and Application_End events. We will use these events to initialize our MicroKernel with all of the Views and Presenters created in other assemblies. The code below shows the MvpApplication class from the MVP Framework which we subclass in Global.asax.
using System;
using System.Web;
using Castle.Windsor;
namespace WCPierce.MVP.Framework
{
public abstract class MvpApplication : HttpApplication, IContainerAccessor
{
private static MvpContainer _container;
protected void Application_Start(object sender, EventArgs e)
{
_container = new MvpContainer();
IoC.Initialize(_container);
}
protected void Application_End(object sender, EventArgs e)
{
_container.Dispose();
}
#region IContainerAccessor Members
public virtual IWindsorContainer Container
{
get { return _container; }
}
#endregion
}
}
Nothing too spectacular. We initialize our MicroKernel (or container) and also dispose of it whenever our Web Application is shutdown or restarted. NOTE: If we add a new View/Presenter to our BatchTracker assemblies, we will need to re-start our web application so the new code will be read from our assemblies. In a future article we will look at a way to overcome this limitation. One other item to note is the IoC class. IoC is a simple static wrapper for our MicroKernel for easier access. This is another great little gem from Oren. With all the work done in MvpApplication your Global.asax.cs is nice and simple:
using System;
using System.Web;
using WCPierce.MVP.Framework;
namespace BatchTracker.Web
{
public class Global : MvpApplication
{
}
}
Now things will get a little heavier. In a typical web application you create pages (.aspx) files and your users access these pages by typing the name of the page into their browser or clicking a link that will transfer them to the page. The MVP Framework does something very similar only instead of entering the name of the page, users will enter the name of the Presenter they want to interact with. When I say name, I am actually referring to the key given to the Presenter when we load it into our MicroKernel. Let me take you back to MVP Continued and look again at the code for CreateBatchPresenter:
[MvpComponent("CreateBatch", typeof(ICreateBatchPresenter))]
public class CreateBatchPresenter : PresenterBase<ICreateBatchView>, ICreateBatchPresenter
{
public CreateBatchPresenter(ICreateBatchView view) : base(view)
{
}
#region ICreateBatchPresenter Members
// ...
#endregion
}
The MvpContainer attribute allows you to define the key used to retrieve your Presenters from the MicroKernel. We could use the code snippet below to get an instance of our CreateBatchPresenter from the MicroKernel:
// Get an instance of CreateBatchPresenter
CreateBatchPresenter presenter = IoC.Resolve<CreateBatchPresenter>("CreateBatch");
Armed with this new found knowledge of the MicroKernel, lets put it to good use in the MVP Framework. We want to allow our users to enter the name of the Presenter they wish to interact with, in a web environment it makes perfect sense to use a QueryString parameter for this specification. Below is a snippet from MvpPage which derives from System.Web.UI.Page. MvpPage is responsible for loading the correct Presenter from the MicroKernel based on a QueryString parameter specified by the user.
protected virtual IPresenter ResolvePresenter()
{
string presenterKey = Request.QueryString["__PRESENTER"] ?? this.DefaultPresenterKey;
if (presenterKey == null) throw new InvalidOperationException("DefaultPresenterKey cannot be null");
IPresenter presenter = null;
try
{
presenter = IoC.Resolve<IPresenter>(presenterKey);
}
catch (ComponentNotFoundException ex)
{
throw new HttpException(404, String.Empty, ex);
}
catch (InvalidCastException ex)
{
string message = String.Format("Component for key '{0}' is not of type {1}", presenterKey, typeof(IPresenter).FullName);
throw new InvalidOperationException(message, ex);
}
return presenter;
}
Hopefully the code is just a C# expression of what we've been building up to :) We look to the QueryString for the name of the Presenter to load and then ask the MicroKernel to provide us an instance. Most of what you are seeing is error handling. If the MicroKernel cannot find the Presenter we asked for, we translate that into an HTTP 404 Not Found error. Makes sense right? Some of you are saying "Bill, you expect me/my user to craft my URLs like http://localhost/BatchTracker/Default.aspx?__PRESENTER=CreateBatch ???" Don't worry, we'll sprinkle a little more magic at the end to make this seamless.
Once we obtain an instance of the correct Presenter we need to display its associated View to the user. We do that in the OnInit event of the MvpPage. Let's walk through the snippet below:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
IPresenter presenter = this.ResolvePresenter();
IPresenter presenterToInitialize = null;
// ...
else
{
Control view = presenter.View as Control;
if (view == null)
{
string message = String.Format("{0}.View must derive from {1}", presenter.GetType().FullName, typeof(Control).FullName);
throw new InvalidOperationException(message);
}
this.View.Controls.Add(view);
presenterToInitialize = presenter;
}
try
{
presenterToInitialize.Initialize(!Page.IsPostBack);
}
catch (SecurityException ex)
{
throw new HttpException(403, String.Empty, ex);
}
}
I feel the excitement building! Once again, the actual code is very to the point wrapped in some liberal error handling. We make the call to ResolvePresenter() discussed above and verify that the View associated with the Presenter is of type System.Web.UI.Control. Remember your Views can be UserControls or custom Server controls, whichever suits your taste. Next comes the call to this.View.Controls.Add(view). this.View is the place holder on our MvpPage to host the Presenter's View. You can use a Panel or PlaceHolder or TableCell or practically any other host control. Lastly we initialize the Presenter and away she goes! Similar to MvpApplication, since all the hard work is done in MvpPage, Default.aspx in our BatchTracker.Web application is nice and simple:
<%@ Page Language="C#" AutoEventWireup="false" CodeBehind="Default.aspx.cs" Inherits="BatchTracker.Web._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>BatchTracker Web</title>
</head>
<body>
<form id="form1" runat="server">
<asp:PlaceHolder runat="server" ID="phView" />
</form>
</body>
</html>
And Default.aspx.cs:
using System;
using System.Web.UI;
using WCPierce.MVP.Framework;
namespace BatchTracker.Web
{
public partial class _Default : MvpPage
{
protected override string DefaultMasterPresenterKey
{
get { return "Default"; }
}
protected override string DefaultPresenterKey
{
get { return "Dashboard"; }
}
protected override Control View
{
get { return phView; }
}
}
}
All you need to specify in the Default page are some...default...values and the place holder for Presenter Views. Don't stress too much about MasterPresenters, I didn't cover them this time (I gotta give you a reason to come back :) in a nutshell they are just an extension of a Presenter that facilitaes MasterPage functionality in the MVP Framework.
The last bit o' magic in todays post comes courtesy of another sweet piece of open source software, (CORRECTION: I was collecting references and licenses for this article and incorrectly sited UrlRewriter.Net from Seth Yates instead of UrlRewritingNet from Albert Weinert & Thomas Bandt. It was 5am ,apologies all around. Thanks for setting me straight Josh). I'm not going to regale you on the awesomeness and simplicity of this tool. I'm just going to give you the quick and dirty version as it relates to the MVP Framework. Below is the configuration found in the Web.Config of our Web Application project:
<urlrewritingnet
rewriteOnlyVirtualUrls="true"
contextItemsPrefix="QueryString"
defaultPage = "Default.aspx"
defaultProvider="RegEx"
xmlns="http://www.urlrewriting.net/schemas/config/2006/07" >
<rewrites>
<add
name="Presenter"
virtualUrl="~/(.*)/Default.aspx"
rewriteUrlParameter="ExcludeFromClientQueryString"
destinationUrl="~/Default.aspx?__PRESENTER=$1"
rewrite="Application"
redirectMode="Permanent"
ignoreCase="true" />
</rewrites>
</urlrewritingnet>
The real meat here is in the rewrites section, specifically virtualUrl="~/(.*)/Default.aspx. What UrlRewriting is doing for us is mapping any url with the previous structure to the following url: ~/Default.aspx?__PRESENTER=$1 where $1 is mapped to (.*). The examples make it a little easier to follow:
http://localhost/BatchTracker/CreateBatch/Default.aspx -> http://localhost/BatchTracker/Default.aspx?__PRESENTER=CreateBatch
http://localhost/BatchTracker/Dashboard/Default.aspx -> http://localhost/BatchTracker/Default.aspx?__PRESENTER=Dashboard
So, to recap, UrlRewriting coupled with the MVP Framework allows us to craft our URLs as such http://<Server>/<Application>/<Name_of_Presenter>/Default.aspx
Lots of code again. MVP has a lot of benefits but the trade off in application complexity can be daunting. Hopefully you will agree that my MVP Framework is making it much simpler for you to implement this design pattern in your own projects. The Framework is still a work in progress and I will continue to post on it but I think its current state is useable. So feel free to download, play, and above all provide feedback.
posted @ Monday, September 04, 2006 12:42 PM