|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionBefore you read this article, make sure you're caught up on Billy McCafferty's article on the MVP pattern. Not only does this article take a large inspiration from Bill's work, but he put a lot of effort into one of the better articles recently posted on The Code Project.Thanks, Billy! On to the task at hand. The ProblemLike most emerging patterns, spending even a little time with the MVP pattern reveals both strengths and weaknesses. Billy does well to explain the benefits of keeping the ASP.NET pipeline in tact, and the obvious ease of unit testing. One huge benefit not yet address is presentation layer reusability (see the sample WinForms app). But for all its benefits, the MVP pattern, as people understand it in its early stages, has its share of problems.Based on what I see as the public's current understanding of the MVP pattern, let's list a few of the major weaknesses:
The ChallengeLet's tackle each of these problems head on, one-by-one. By the end of this example we should have the beginnings of a more matured approach to MVP as it applies to .NET.No Code ReuseEvery single page contains an instance of a presenter. That's quite a bit of copy-paste if you want to convert a large website to use MVP. The obvious place to start is by creating a base class for all presenters that all the views can share. We'll add an abstract base presenter to the presentation project. The constructor takes a generic IView interface and provides a method (via generics) to cast the view to a more specific interface type. To keep the view's logic as simple as possible, the base presenter will expose a single abstract method, " /// <summary> /// Base functionality all Presenters should support /// </summary> public abstract class Presenter { protected readonly IView _view; public Presenter( IView view ) : this(view, null) { } public Presenter( IView view, ISessionProvider session ) { _view = view; if(session != null) { SessionManager.Current = session; } } /// <summary> /// Converts an object from IView to the type of view the Presenter expects /// </summary> /// <typeparam name="T">Type of view to return (i.e. ILoginView)</typeparam> protected T GetView<T>() where T : class, IView { return _view as T; } protected ISessionProvider Session { get { return SessionManager.Current; } } }Now that there's a base implementation for the presentation layer, a base web page is in order. We should be able to get rid of those four or five lines of code required by each page and move it to a few lines in the base page. On a site with 300 pages and controls we just saved 1200 lines of code! In short, the base page provides two methods to facilitate registering a view with the associated presenter. public class BasePage : System.Web.UI.Page, IView { protected T RegisterView<T>() where T : Presenter { return PresentationManager.RegisterView<T>(typeof(T), this, new WebSessionProvider()); } protected void SelfRegister(System.Web.UI.Page page) { if (page != null && page is IView) { object[] attributes = page.GetType().GetCustomAttributes(typeof(PresenterTypeAttribute), true); if (attributes != null && attributes.Length > 0) { foreach(Attribute viewAttribute in attributes) { if (viewAttribute is PresenterTypeAttribute) { PresentationManager.RegisterView((viewAttribute as PresenterTypeAttribute).PresenterType, page as IView, new WebSessionProvider()); break; } } } } } } The base page provides two differing ways of view registration. A view can pass in the type of presenter to load along with an instance of itself to the RegisterView<T> method. This is really more of a left over convention from the original code this article was based on. The more friendly method of registration is done by a page calling the SelfRegister method and passing itself, as an instance, as the solitary argument. The SelfRegister method then examines the page's attributes to find the correct presenter type to load. public static class PresentationManager { public static T RegisterView<T>(Type presenterType, IView view) where T : Presenter { return RegisterView<T>(presenterType, view, null); } public static T RegisterView<T>(Type presenterType, IView view, ISessionProvider session) where T : Presenter { return LoadPresenter(presenterType, view, session) as T; } public static void RegisterView(Type presenterType, IView view) { RegisterView(presenterType, view, null); } public static void RegisterView(Type presenterType, IView view, ISessionProvider session) { LoadPresenter(presenterType, view, session); } private static object LoadPresenter(Type presenterType, IView view, ISessionProvider session) { int arraySize = session == null ? 1 : 2; object[] constructerParams = new object[arraySize]; constructerParams[0] = view; // Add the session as a parameter if it's not null if (arraySize.Equals(2)) { constructerParams[1] = session; } return Activator.CreateInstance(presenterType, constructerParams); } } For this example, and for simplicity, I put a custom attribute on each view that defines what view interface type it operates on. This makes object creation easy, fast, and a great candidate for caching after the first call. A more complex example or framework may use a custom configuration section for more flexible mapping. This example also doesn't account for mapping multiple presenters to a single view; something that may be necessary for a real MVP framework. View Intelligence protected void loginButton_Click( object sender, EventArgs e) { _loginPresenter.LoginUser(this.userNameTextBox.Text, this.passwordTextBox.Text); }This isn't bad, but the view has to explicitly give the presenter the requited data. A cleaner approach would be for the view to request a type of execution and let the presenter get for the data it needs. public event EventHandler OnLogin; protected void Page_Load(object sender, EventArgs e) { base.SelfRegister(this); } protected void loginButton_Click( object sender, EventArgs e) { if(this.OnLogin != null) { OnLogin(this, EventArgs.Empty); } } Remember that one of the goals of MVP is to truly separate responsibilities. Letting the presenter respond to events fired y the view for data truly puts the responsibility on the presenter to take action. This design also works better for unit testing because your mock views in your unit test will more closely match your real-world views. Your unit tests can raise the same events, thereby simulating your UI very closely. State Management Actually, the answer is rather simple. Views interact with the presentation layer via the interfaces they implement. Application state (Session) shouldn't be any different. Simply put, all we need is a state management interface that defines how to interact with any state object. Then, just like a view implements an interface, ASP.NET's Session object can be wrapped in a class that implements the state management interface. /// This is in the presentation layer. public interface ISessionProvider { object this[string name] { get;set;} object this[int index] { get;set;} } Since presenters interact with interfaces, and state has been abstracted to adhere to an interface, our presentation layer now has access to any type of state management without ever having to know the details of the state's internals… including the ASP.NET Session object (See the WinForms sample for a custom state example). Where Are We?I said that by the end of the article we'd have the beginnings of maturing how MVP applies to .NET. Are we there yet... I don't know. I think there's still plenty of work left to be done. MVP is a relatively new approach (though MVC has been around the block many times), and the best ideas haven't been thought of yet. My hope these ideas give you a leg up if you're looking to use the pattern and want to make clean and usable. Better yet, what ideas do you have on how MVP can be improved upon? Take a look at the sample projects to see how the layers are truly interacting with each other. Then, more importantly, share your thoughts with your own super cool article. | ||||||||||||||||||||