Wednesday, February 18, 2009

PresenterFactory and MVP base

Been awhile! Sorry!

Well, MVP / Observer pattern rocks, you all know I feel that way. Well, there are those who don't. I heard rumblings that basically it's an abuse of the observer pattern to make an academic point. I think that's a silly argument because using the observer pattern makes your view contract stronger.

With the observer pattern, you can not only define what the view HAS, but also what it CAN DO. It also adheres to Tell, Don't Ask.

Well, there's more too. I'm a firm believer in DRY. MVP pages all have a presenter on their page, and I always new them up in onload. I figured I could move that into a base class.

So I did.


    5 public abstract class MvpPage:Page

    6     {

    7         ///

    8         /// The Presenter for this page

    9         ///

   10         private object Presenter

   11         {

   12             get; set;

   13         }

   14         protected override void OnLoad(System.EventArgs e)

   15         {

   16             base.OnLoad(e);

   17             Presenter = PresenterFactory.GetPresenterFor(this);

   18         }

   19     }


I didn't want to use Type parameters since that would defeat the whole purpose of making the page unaware of it's presenter. 

The work is all really done in PresenterFactory, so let's get to that.


Here is the unit test for GetPresenterFor.


    1 using System;

    2 using MyDumbView.Presentation;

    3 using MyDumbView.UnitTests.Presentation.TestAssembly.Presentation.Presenters;

    4 using MyDumbView.UnitTests.Presentation.TestAssembly.Presentation.Views;

    5 using NUnit.Framework;

    6 using NUnit.Framework.SyntaxHelpers;

    7 

    8 namespace MyDumbView.UnitTests.Presentation

    9 {

   10     [TestFixture]

   11     public class PresenterFactoryTestFixture:AssertionHelper

   12     {

   13         [Test]

   14         public void GetPresenterFor_returns_the_right_type_of_presenter()

   15         {

   16             //Arrange

   17             var view = new TestView();

   18             //Act

   19             var presenter = PresenterFactory.GetPresenterFor(view);

   20             //Assert

   21             var expectedType = typeof (TestPresenter);

   22             Expect(presenter.GetType(),Is.EqualTo(expectedType));

   23         }

   24 

   25     }

   26 

   27     namespace TestAssembly.Presentation.Views

   28     {

   29         ///

   30         /// My Test Interface

   31         ///

   32         public interface ITestView{}

   33         ///

   34         /// Concrete class of

   35         ///

   36         public class TestView:ITestView{}

   37     }

   38     namespace TestAssembly.Presentation.Presenters

   39     {

   40         public class TestPresenter

   41         {

   42             public TestPresenter(ITestView view)

   43             {

   44             }

   45         }

   46     }

   47 }


Makes sense right?


Well, here's the factory.


    1 using System;

    2 

    3 namespace MyDumbView.Presentation

    4 {

    5     ///

    6     /// A factory that that returns presenters for views

    7     ///

    8     public class PresenterFactory

    9     {

   10         ///

   11         /// Returns a presenter for the view, very opinated code based on naming conventions.

   12         ///       

   13         ///

   14         ///

   15         ///

   16         public static object GetPresenterFor(object view)

   17         {  

   18             var type = view.GetType();

   19             var viewTypeName = GetViewTypeName(type);

   20             var presenterType = GetPresenterType(viewTypeName);

   21             return GetPresenter(presenterType, view);

   22         }

   23         private static string GetViewTypeName(Type type)

   24         {

   25             return PresenterFactoryNameParsingStrategy.GetTypeNameForView(type);

   26         }

   27         private static Type GetPresenterType(string viewTypeName)

   28         {

   29             var presenterName = GetPresenterTypeName(viewTypeName);

   30             var presenterType = Type.GetType(presenterName);

   31             if (presenterType == null)

   32             {

   33                 throw new Exception(String.Format("Can not find class {0}.", presenterName));

   34             }

   35             return presenterType;

   36         }

   37         private static string GetPresenterTypeName(string viewTypeName)

   38         {

   39             return PresenterFactoryNameParsingStrategy.GetTypeNameForPresenter(viewTypeName);

   40         }

   41         private static object GetPresenter(Type presenterType, object view)

   42         {

   43             var presenter = Activator.CreateInstance(presenterType, new[] { view });

   44             if (presenter == null)

   45             {

   46                 throw new Exception(String.Format("Can not instanciate class {0}. Does the class have a public constructor taking a single argument?", presenterType));

   47             }

   48             return presenter;

   49         }

   50 

   51 

   52     }

   53 }



So this code uses whatever naming conventions are in place to figure out the correct presenter.  This could easily be replaced with something more robust or and IOC container. I just know that my code will always adhere to naming conventions so I personally like this. It's all beside the point anyway, since I've moved all the code that actually determines thy types to PresenterFactoryNameParsingStrategy.

Oh, yeah, if you're interested, here it is.

    1 using System;

    2 

    3 namespace MyDumbView.Presentation

    4 {

    5     ///

    6     /// Encapsulates our opinionated naming stategy for Views and Presenters

    7     ///

    8     internal static class PresenterFactoryNameParsingStrategy

    9     {

   10         ///

   11         /// Returns the Type name for a MVP view

   12         ///

   13         ///

   14         ///

   15         public static string GetTypeNameForView(Type type)

   16         {

   17             foreach (var iface in type.GetInterfaces())

   18             {

   19                 if (iface.Name.EndsWith("View"))

   20                 {

   21                     return iface.AssemblyQualifiedName;

   22                 }

   23             }

   24             throw new Exception("View does not adhere to Naming conventions.");

   25         }

   26         ///

   27         /// Returns the name for a MVP presenter given it's view

   28         ///

   29         ///

   30         ///

   31         public static string GetTypeNameForPresenter(string viewTypeName)

   32         {

   33             var name = viewTypeName.Replace(".I",".")

   34                 .Replace("Views", "Presenters");

   35             var nameParts = name.Split(',');

   36             var typeName = name.Split(',')[0];

   37             var assemblyName = nameParts.Length > 1

   38                                    ?

   39                                        nameParts[1]

   40                                    :

   41                                        String.Empty;

   42             typeName = typeName.Replace(".I", ".").Replace(".Views.", ".Presenters.");

   43             var endIndex = typeName.Length - "View".Length;

   44             typeName = typeName.Substring(0, endIndex) + "Presenter";

   45             return String.Format("{0}, {1}", typeName, assemblyName);

   46         }

   47     }

   48 }


Lot's of hard coded strings in here, and this is definitly worth refactoring. But you get the drift....

So, running the unit tests... Everything passes.
Lets go make a page use it.

    1 using System;

    2 using MyDumbView.DomainModel;

    3 using MyDumbView.Presentation;

    4 using MyDumbView.Presentation.Views;

    5 

    6 

    7 public partial class _Default :MvpPage,IPersonEditorView 

    8 {

    9 

   10     ///

   11     /// Wires up events

   12     ///

   13     ///

   14     protected override void OnInit(EventArgs e)

   15     {

   16         base.OnInit(e);

   17         LoadButton.Click += delegate { PersonLoaded(this, e); };

   18         SaveButton.Click += delegate { PersonSaved(this, e); };

   19     }

   20     #region IPersonEditorView Methods

   21 

   22     ///

   23     /// The , null if we're adding a new one.

   24     ///

   25     public int? PersonId

   26     {

   27         get

   28         {

   29             int x;

   30             if (int.TryParse(PersonIdControl.Text,out x))

   31             {

   32                 return x;   

   33             }

   34             return null;

   35         }

   36         set { PersonIdControl.Text = value.ToString(); }

   37     }

   38     ///

   39     /// The to display

   40     ///

   41     public string FirstName

   42     {

   43         get { return PersonFirstNameControl.Text; }

   44         set { PersonFirstNameControl.Text = value; }

   45     }

   46     ///

   47     /// The to display

   48     ///

   49     public string LastName

   50     {

   51         get { return PersonLastNameControl.Text; }

   52         set { PersonLastNameControl.Text = value; }

   53     }

   54     ///

   55     /// Event raised when Saved is Clicked

   56     ///

   57     public event EventHandler PersonSaved = delegate { };

   58     ///

   59     /// Event raised when Load is clicked

   60     ///

   61     public event EventHandler PersonLoaded = delegate { };

   62 

   63     #endregion

   64 }


Beautiful!

Note that the page has absolulty no idea about its presenter anymore. That all sits in the base class.

And the page works the same as it originally did.

I'll post the entire solution soon... 



3 comments:

  1. Great stuff Elliot, thanks for sharing your ideas.

    ReplyDelete
  2. Thanks Matt...
    BTW, your http://blog.mattwynne.net/2007/06/13/mvp-smells/ article is bookmarked and I send developers new to MVP there constantly!

    ReplyDelete