MVP Continued

I started talking about an MVP Framework a few days ago, but then launched into a little background on the Inversion of Control framework that would be supporting it. Now it's time to get back to business.  My goals for the framework include:

  • As clean a separation as possible between Presenter logic and View logic
  • As simple as possible for someone to pickup and start working with
  • Something extensible than can be tweaked as needed to meet specific needs

I will only attempt to take the separation so far.  My goal is not to completely abstract away the fact that our code will be operating in a web environment.  You will not be able to simply code up some WinForms and hope to re-use the exact same Presenter logic as is.  If anyone has ever worked on a project where that was the case I would love to hear about it.

As for simple.  The first two interfaces introduced in my initial posting had two members a piece.  Sounds pretty simple so far, but I think we can improve on it.  The idea behind MVP is there will be a one-to-one relationship between a View and a Presenter.  This is not to say that you cannot have a composition where one Presenter/View contains one or more other Presenter/View combinations.  But suffice it to say that a Presenter is more or less tightly coupled to its View.  With that in mind we can use .Net Generics to improve upon our initial interfaces.  Below is a new Interface that all of our Presenters will implement:

using System;
 
namespace WCPierce.MVP.Framework
{
  public interface IPresenterBase<T> : IPresenter where T : IView
  {
    new T View { get; }
  }
}

Here we are simply stating that a Presenter will have a particular implementation of a View, rather than going with the less precise IView.  Similarly for our Views we will use the following Interface:

using System;
 
namespace WCPierce.MVP.Framework
{
  public interface IViewBase<T> : IView where T : IPresenter
  {
    void SetPresenter(T presenter);
  }
}

Again, a View will be tied to a particular implementaion of IPresenter.  Now that we have our interfaces straightened out, we need some base classes.  The PresenterBase class below implements our IPresenterBase and does some simple error checking:

using System;
using System.Collections;
using System.Web.UI;
 
using Castle.Model;
 
namespace WCPierce.MVP.Framework
{
  [Transient]
  public abstract class PresenterBase<T> : IPresenterBase<T> where T : IView
  {
    private T _view;
    private IContext _context;
 
    protected PresenterBase(T view)
    {
      if (view == null) throw new ArgumentNullException("view");
 
      Control c = view as Control;
      if (c == null)
      {
        string message = String.Format("View must derive from {0}", typeof(Control).FullName);
        throw new InvalidOperationException(message);
      }
 
      _view = view;
      _view.SetPresenter(this);
    }
 
    #region IPresenterBase<T> Members
 
    public T View
    {
      get { return _view; }
    }
 
    #endregion
 
    #region IPresenter Members
 
    IView IPresenter.View
    {
      get { return _view; }
    }
 
    public virtual void Initialize(bool isFirstCall)
    {
    }
 
    #endregion
  }
}

Notice in the constructor we verify that our View is of type System.Web.UI.Control.  Any Views created using the MVP Framework must be either UserControls or custom Server controls.  We are simply enforcing that requirement with a Type check.  One VERY IMPORTANT thing to note is the [Transient] attribute.  This attribute instructs the Kernel to create a new instance of this class everytime it is requested.  Because the Presenter is essentially a proxy for a traditional ASP.Net Page, it would be very bad if the Kernel attempted to re-use the same "Page" instance to service multiple requests.  MonoRail imposes the same requirement on its Controllers but this is taken care of transparently by that Framework.   

ViewBase is coded in a similar manner:

using System;
using System.Web.UI;
 
using Castle.Model;
using WCPierce.Model;
using WCPierce.MicroKernel.ComponentActivator;
 
namespace WCPierce.MVP.Framework
{
  [Transient]
  [ComponentActivator(typeof(WebUserControlComonentActivator))]
  public abstract class ViewBase<T> : UserControl, IViewBase<T> where T : IPresenter
  {
    private T _presenter;
 
    protected T Presenter
    {
      get { return _presenter; }
    }
 
    #region IViewBase<T> Members
 
    public void SetPresenter(T presenter)
    {
      if (presenter == null) throw new ArgumentNullException("Presenter");
      if (_presenter != null) throw new InvalidOperationException("Presenter is write-once");
 
      _presenter = presenter;
    }
 
    #endregion
 
    #region IView Members
 
    void IView.SetPresenter(IPresenter presenter)
    {
      SetPresenter((T)presenter); 
    }
 
    public virtual void Initialize(bool isFirstCall)
    {
    }
 
    #endregion
  }
}

Views are also marked Transient as well as designating the use of the WebUserControlCompentActivator discussed previously.  Now, enough Framework plumbing, lets look at how you might use this in your own application.

I am nursing a Batch Tracking appliction for example usage throughout this series of blog posts.  Here is a quick overview of the business domain as we understand it in this iteration.

The Document Management Group is responsible for receiving documents and putting it through a workflow including data entry, error correction, document prep, microfilming, imaging and desctruction.  Each of these operations takes place in separate physical locations managed by different organizational units also crossing departmental boundaries.  The Batch Tracking application is responsible for keeping track of where documents are at any given time.  One of the first use cases identifed is Create Batch where by an operator groups a set of documents into a batch of 50, enters relevant batch data, and assigns the batch a barcode generated by the system.  We've identified the need for a CreateBatchPresenter and a CreateBatchView for the application.  The operator will enter relevant batch data and save the batch, the Presenter will save the batch to the repository which will generate a unique batch id.  The View will then print a barcode with all of the information which the operator will attach to the batch.

From this overview we come up with two interfaces which we will place together in an assembly called BatchTracker.Interfaces:

using System;
 
using WCPierce.MVP.Framework;
 
namespace BatchTracker.Interfaces
{
  public interface ICreateBatchPresenter : IPresenterBase<ICreateBatchView>
  {
    void CreateBatch(int beginningValidationNumber, int endingValidationNumber);
  }
}
using System;
using System.Collections.Generic;
 
using BatchTracker.Models;
using WCPierce.MVP.Framework;
 
namespace BatchTracker.Interfaces
{
  public interface ICreateBatchView : IViewBase<ICreateBatchPresenter>
  {
    void PrintBatchSlip(Batch batch);
 
    void DisplayErrors(List<string> errors);
  }
}

And now the fun part.  The concrete implementations of our Presenters reside in their own assembly, BatchTracker.Presenters.  We only need references to BatchTracker.Interfaces and WCPierce.MVP.Framework to create our implementations.  Here is our concrete implementation of ICreateBatchPresenter:

using System;
using System.Collections.Generic;
 
using BatchTracker.Models;
using BatchTracker.Interfaces;
using Castle.Model;
using WCPierce.MVP.Framework;
 
namespace BatchTracker.Presenters
{
  [CastleComponent("CreateBatch", typeof(ICreateBatchPresenter), Lifestyle=LifestyleType.Transient)]
  public class CreateBatchPresenter : PresenterBase<ICreateBatchView>, ICreateBatchPresenter
  {
    public CreateBatchPresenter(ICreateBatchView view) : base(view)
    {
    }
 
    #region ICreateBatchPresenter Members
 
    public void CreateBatch(int beginningValidationNumber, int endingValidationNumber)
    {
      List<string> errors = new List<string>();
 
      if (beginningValidationNumber <= 0)
      {
        errors.Add("Beginning Validation Number must be greater than 0");
      }
      else if (endingValidationNumber <= beginningValidationNumber)
      {
        errors.Add("Ending Validation Number must be greater than Beginning Validation Number");
      }
 
      if (errors.Count > 0)
      {
        this.View.DisplayErrors(errors);
        return;
      }
 
      Batch newBatch = new Batch();
 
      newBatch.BeginningValidationNumber = beginningValidationNumber;
      newBatch.EndingValidationNumber = endingValidationNumber;
      newBatch.DateCreated = DateTime.Now;
 
      // The Repository will assign the batch a unique id
      //Repository<Batch>.Save(newBatch);
 
      this.View.PrintBatchSlip(newBatch);
    }
 
    #endregion
  }
}

Because of our lovely generic base class all we have to worry about is the business logic needed to fulfill the use case.  We perform some basic validation in CreateBatch and then save it to the Repository.  We have strongly typed access to our view via the View property from the generic base class.  Once everything is processed, we instruct the View to print the batch slip.  Our Views are also defined in a separate assembly called BatchTracker.Views.  Remember, we have to pre-compile this assembly using the aspnet_compiler inorder to use it with our Framework.  Here is the View that complements the Presenter:

<%@ Control Language="C#" AutoEventWireup="false" CodeBehind="CreateBatchView.ascx.cs" Inherits="BatchTracker.Views.CreateBatchView" %>
<asp:DataList runat="Server" ID="dlErrors" EnableViewState="false" />
<asp:Panel runat="server" ID="pnlCreate">
  Beginning Validation Number: <asp:TextBox runat="server" ID="txtBeginningValidationNumber" /><br />
  Ending Validation Number: <asp:TextBox runat="server" ID="txtEndingValidationNumber" /><br />
  <asp:Button runat="server" ID="btnCreate" Text="Create" OnClick="btnCreate_Click" /><br />
</asp:Panel>
<asp:Panel runat="server" ID="pnlPrint" Visible="false">
  Batch ID: <asp:Label runat="server" ID="lblId" /><br />
  Beginning Validation Number: <asp:Label runat="server" ID="lblBeginningValidationNumber" /><br />
  Ending Validation Number: <asp:Label runat="server" ID="lblEndingValidationNumber" /><br />
  Date Created: <asp:Label runat="server" ID="lblDateCreated" /><br />
</asp:Panel>

 The View is very straight forward and focused on only fulfilling the requirements set forth in our specific use case.  The corresponding code behind:

using System;
using System.Collections.Generic;
using System.Web.UI;
using System.Web.UI.WebControls;
 
using BatchTracker.Models;
using BatchTracker.Interfaces;
using Castle.Model;
using WCPierce.MVP.Framework;
 
namespace BatchTracker.Views
{
  [CastleComponent("CreateBatchView", typeof(ICreateBatchView), Lifestyle=LifestyleType.Transient)]
  public partial class CreateBatchView : ViewBase<ICreateBatchPresenter>, ICreateBatchView
  {
    protected void btnCreate_Click(object s, EventArgs e)
    {
      int beginningValidationNumber = Int32.Parse(txtBeginningValidationNumber.Text);
      int endingValidationNumber = Int32.Parse(txtEndingValidationNumber.Text);
 
      this.Presenter.CreateBatch(beginningValidationNumber, endingValidationNumber);
    }
 
    #region ICreateBatchView Members
 
    public void PrintBatchSlip(Batch batch)
    {
      pnlCreate.Visible = false;
      pnlPrint.Visible = true;
 
      lblId.Text = batch.Id.ToString("000000");
      lblBeginningValidationNumber.Text = batch.BeginningValidationNumber.ToString("0000");
      lblEndingValidationNumber.Text = batch.EndingValidationNumber.ToString("0000");
      lblDateCreated.Text = batch.DateCreated.ToShortDateString();
    }
 
    public void DisplayErrors(List<string> errors)
    {
      dlErrors.DataSource = errors;
      dlErrors.DataBind();
    }
 
    #endregion
  }
}

 Just like the Presenter had strongly typed access to the View, the View has strongly typed access to the Presenter.

When the code is broken down you can clearly see the separation of concerns.  The View is focused on displaying data given to it by the Presenter and providing data to the Presenter received from the user.  The Presenter orchestrates the various services of the application and contains the actual business logic.

Lots of code and info in this post.  We've now created the Model, View, and Presenter but we are still missing a few more pieces of the Framework that will tie it all together.  Tune in later this weekend (hopefully) and see the finishing touches.

posted @ Saturday, August 19, 2006 7:41 AM


Print

Comments on this entry:

# re: MVP Continued

Left by Srdjan at 9/3/2006 3:17 PM
Gravatar

your MVP articles are great so far... Is the finishing article coming soon?<br /><br />Srdjan

Your comment:



 (will not be displayed)


 
 
 
Please add 7 and 7 and type the answer here:
 

Live Comment Preview:

 
«October»
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678