Presenting MvpWebTemplate, a template of an ultra lightweight MVP ASP.NET architecture

by sven 7 december 2008 21:42

The need for a scalable, testable, understandable Web application architecture.

At one point in time, we had to design an architecture for a moderately large and complex enterprise web application. The intranet application has to serve 100.000+ users in over 18 languages, using some complex security. And since it has to replace a legacy ASP application, some valuable lessons from the past where already learnt (the hard way). So testability, understandability, code robustness, separation of concerns, keeping-it-simple, consistency and all those other good things were very high on the wish list !

MVP architecture ? Isn't the whole world moving to MVC ?

First and foremost to get this out of the way, yes, we like MVC (Model-View-Controller) better then MVP (Model-View-Presenter) from a technical point of view. It is a nicer to have a request entering the controller then entering the view, but that also imposes a major paradigm shift to what asp.net developers have been doing for years. And while the blogosphere might give you the idea that everyone does MVC today, the truth is that there are lots and lots developers out there still productively programming ASP.NET Webforms (for quite some years to come). Finding developer resources can be a significant factor in the decision to choose MVP over MVC. This is something that is not to be taken lightly. After all, what good is a project if you can't find resources to work on it ? 

This does not mean however we have compromised the architecture. In aiming for lightweight and simplicity, the presented MVP here is merely a way of working, and is far from being a framework ! This makes the learning curve for existing ASP.NET developers incredibly small (something that cannot be said of the WCSF) and we have no bloat, only providing some baseclasses and interfaces to enforce consistency and good practices.

Enough with the advertising blah, show me the good stuff !

The template solution consists of only 4 projects : Data, Business, Web and Test projects. The Web project will consists of the good old Asp.net pages we all know  The data layer will depend on your choice of technology, be it (typed) datasets, Linq to SQL,  nHibernate, or my personal favorite LLBLGen. The Test project obviously will hod our tests, so it goes without saying that the Business project will be the main concern of the architecture, and houses the Domain and Application layers as described in Evan's DDD.  The business project is organized in some subfolders, being our DomainObjects, the Components & their ComponentInterfaces, the ViewInterfaces and ViewDataObjects , the Presenters and some Helpers and Enumerations. The importance of these enumerations is not to be underestimated, since it will be a key factor in the testability of our UI navigation logic. Unit testing of the navigation logic in classic Asp.net is a daunting task, but following the simple rules of this MVP architecture turn it into a piece of cake. So what are the rules ?

  • Rule 1 : Every page or usercontrol shall implement one viewinterface and shall be attached to one presenter.
  • Rule 2 : Every page or usercontrol shall be uniquely identified by a Page- or ControlIdentifier (which is an enum type)
  • Rule 3 : No page or usercontrol shall have codebehind that user ViewState, Context, Session, Request or Response objects ! These are just awful in terms of testability

So, that wasn't too hard. Now we're going to build a simple sample application to demonstrate some features of the architecture, along with samples how to test the functionality.

A simple sample

To demonstrate some of the core aspects while keeping it short, we will make a really simple application consisting of 2 pages and 2 controls. The first page let's the user type in a username and select a language from a dropdown, and the second page just shows this info, with the help of usercontrols and some ajax.

When making a page or usercontrol in this architecture, the first step always consist of asking yourself : What data will this page display, and what events will it trigger ? The answer to this question is actually the 'contract' of the page, and we will put this contract in ...it's interface ofcourse ! So in our example, our page will need to display some languages in a dropdown, also it will need to report what name was typed in, and what language was selected, and it will need to trigger and event when the user clicked a button to continue to the second page. So the interface for this page will be something along :

   1: public interface ITestPage1 : IPageView
   2: {
   3:     ICollection<Language> Languages { set; }
   4:     string UserName { get; }
   5:     int SelectedLanguageId { get; }
   6:     event EventHandler ContinueClicked;
   7: }

This covers it all .. we can set a collection of languages (note: we are here passing a collection of the domain object 'Language' to the view. We could as well made a ViewDataObject or VDO for our domain object, and passed that one to the view. It actually means that we are using the 'Supervising Controller' flavor of the MVP pattern, in contrast to the 'Passive view'. We found that using a mixture of these flavors, depending on the view-complexity and overhead for creating VDO's yields best results. Be we don't judge, you can use either flavor ) . We can also get the username that was typed and the language that was selected, and we expressed that there is an event. Note that the interface inherits from and IPageView, defined by

   1: public interface IPageView  : IView
   2: {
   3:     PageIdentifier PageIdentifier { get; } 
   4: }

  where the PageIdentifier is a unique enum identifier for every page in our application (analog we have IControlView which has a ControlIdentifier). Both of these inherit from IView, defined by:

   1: public interface IView
   2:  {
   3:      event EventHandler Load;
   4:      bool IsPostBack { get; }
   5:      NameValueCollection QueryString { get; }
   6:  }

The Load event will correspond to the Asp.net Load event, IsPostBack will indicate if we had a postback and finally QueryString will return the QueryString parameters for the view, so we can access them in our presenters (using convenience methods as we will see later).

But back to the example. The ITestPage1 interface is defined, so we can start coding the presenter. A presenter is the actual workhorse, instantiated by the view when the page is requested, handling the views event, and talking to our domain model. In our architecture, we modeled a baseclass for all presenters (Pages & Controls), that takes an IView interface in the constructor, provides properties to some external dependencies (such as the ReferenceData, the Navigation and Session), provides abstract functions to subscribe to the view's event and load the model into the view, and finally some convenience functions to get strongly typed querystring parameters out of the view. (As a side note on using querystring parameters, some people argue they are ugly and unmaintainable. But on the other hand, they are the most scalable way of passing information from page to page, and the strongly typed approach in this architecture makes them very maintainable and refactor friendly.

To show just how easy it is to code the presenter, and with the incredible productive help of the developer's best friend Resharper , i'll code the presenter in just a few short steps :

After typing the classname and baseclass, Resharper suggests to implement the constructor and interfaces. So

image

suddenly becomes

image

without typing a letter Applause ! Now, in the SubscribeViewtoEvents method, we attach handlers to every event in the View, using intellisense, yielding :

image

An now we start coding for real .The actual code we have to write by hand is :

image

It's really that simple ! On initial load (when not posting back), we load the view from the model. So we get the domain objects from our ReferenceData component (remember this external dependency was added in the BasePresenter), and set in on the View. Then in the eventhandler for the ContinueClicked event on the view, we take the value for username that was typed in, and put it in the SessionComponent  (again, the dependency was added in BasePresenter), we take the selectedLanguageId from the view, and navigate to the second page, passing in that LanguageId in the querystring. Now all this is strongly typed, and perfectly testable. For example, imagine we want to test that when clicking continue, we actually navigate to the second page (not taking the passed values into account). A test for this would look like  (using Moq mocking framework):

   1: private ISessionComponent session;
   2: private Mock<INavigationComponent> navigation;
   3:  
   4: [TestInitialize]
   5: public void sSetup()
   6: {
   7:     ServiceLocator.Instance().InitializeForUnitTesting();
   8:     //register a stubbed session in the container
   9:     session = new StubSession();
  10:     ServiceLocator.Instance().RegisterInstance<ISessionComponent>(session);
  11:     //register a mocked navigation component in the container
  12:     navigation = new Mock<INavigationComponent>();
  13:     ServiceLocator.Instance().RegisterInstance<INavigationComponent>(navigation.Object);
  14: }
  15:  
  16: [TestMethod]
  17: public void Should_navigate_to_page2_on_ContinueClicked_method()
  18: {
  19:     //set expectation that we will navigate to a certain page
  20:     bool isCalled = false;
  21:     navigation.Expect(n => n.NavigateTo(PageIdentifier.TestPage2, It.IsAny<string[]>()))
  22:         .Callback(() => isCalled = true);
  23:     //directly call the continue clicked method on the presenter 
  24:     //it should be 'internal' to make this work
  25:     var mockedView = new Mock<ITestPage1>();
  26:     var presenter = new TestPage1Presenter(mockedView.Object);
  27:     presenter.View_ContinueClicked(null, null);
  28:     Assert.IsTrue(isCalled);
  29: }

A more elaborate test would raise the event on the mocked view interface (so also testing the correctness of the "SubscribeViewToEvents") :

   1: [TestMethod]
   2: public void Should_navigate_to_page2_on_ContinueClick_Event_Raised_On_View()
   3: {
   4:     //set expectation that we will navigate to a certain page
   5:     bool isCalled = false;
   6:     navigation.Expect(n => n.NavigateTo(PageIdentifier.TestPage2, It.IsAny<string[]>()))
   7:         .Callback(() => isCalled = true);
   8:     //initiate the ContinueClicked via the event on the view 
   9:     //a bit awkward code to initiate the event
  10:     var mockedView = new Mock<ITestPage1>();
  11:     MockedEvent<EventArgs> ev = mockedView.CreateEventHandler();
  12:     mockedView.Object.ContinueClicked += ev;
  13:     var presenter = new TestPage1Presenter(mockedView.Object);
  14:     ev.Raise(EventArgs.Empty);
  15:     Assert.IsTrue(isCalled);
  16: }

But wait ..how can we navigate from one page to another, or access the session in the presenter ? We certainly at all cost want to prevent adding a reference to 'System.Web' in our business project, so how does it work ? It's actually also very simple, made possible due to the simple 'Program against abstractions, not implementations' principle that is used over and over again in this architecture. So, in the business project, we defined the interfaces for our navigation and session components :

   1: public interface ISessionComponent
   2:  {
   3:      string User { get; set; }
   4:  }
   5:  
   6: public interface INavigationComponent
   7: {
   8:     void NavigateTo(PageIdentifier pageIdentifier, NameValueCollection queryString);
   9:     void NavigateTo(PageIdentifier pageIdentifier, params string[] queryParams);
  10:     string GetUrl(PageIdentifier pageIdentifier, params string[] queryParams);
  11:     string GetUrl(ControlIdentifier identifier);
  12: }

while ofcourse the actual implementations will live in the Web project (where we have access to the Session and Response intrinsic asp.net objects). The INavigationComponent would stay constant over projects, while the ISessionComponent interface would change from project to project.  (although typically, put the user in the session, and for the rest keep it as small as possible for scalability reasons)

The advantages of this approach are multiple :

  • Whatever goes into the session is centrally controlled and strongly typed, thus refactor friendly
  • The navigation is testable, and all physical file paths are centrally controlled in the navigation component implementation. In our architecture, it's a dictionary mapping PageIdentifiers to ASPX files
  • We can write unit test to check existence of physical files. This keeps our application "healthy" (no more moving files around and then getting 404's). This is a major thing for large projects

A unit test for this last item could look like :

   1: [TestMethod]
   2: public void All_pageidentifiers_should_be_mapped_to_physical_exisitng_files()
   3: {
   4:     NavigationComponent navi = new NavigationComponent();
   5:     foreach (PageIdentifier pageId in Enum.GetValues(typeof(PageIdentifier)))
   6:     {
   7:         string url = navi.GetUrl(pageId);
   8:         url = url.Replace("~/", @"..\..\..\Template.Web\");
   9:         Assert.IsTrue(File.Exists(url),pageId + " is mapped  to an non- existing file : " + url);
  10:     }
  11: }

 

So we've got everything, right ? Our interface, the presenter, the unit tests ...but wait, there is still something missing ...the ASPX page ! That's right, the page comes last. That way, we are sure that whatever we are building now, it's usage is allready tested and verified. The implementation is also very simple. For convenience, we create BasePage and BaseUserControl classes that all pages and controls must inherit from, forcing them to "do the right thing". These baseclasses contain some abstract functions like CreatePresenter and CreateEventhandlers (so it cannot be 'forgotten'), the abstract property PageIdentifier, the QueryString property and some methods to get/set pageparameters (using the ViewState, more on this later) .

 

So let's build the page ! First we add a new aspx webform page to the web project, and add the markup :

image

That was easy. Now the codebehind. First of all, delete the OnLoad method, we won't need it. The let the class inherit from BasePage and our viewinterface, and get ready for some more Resharper lovin'

image

This gives us :

image

Now the job is simple. Return the unique identifier for the page, create the presenter and eventhandlers, and get/set our data ! The final code for our page looks like :

   1: public partial class TestPage1 : BasePage, ITestPage1 
   2: {
   3:     protected override IPresenter CreatePresenter()
   4:     {
   5:         return new TestPage1Presenter(this);
   6:     }
   7:  
   8:     protected override void CreateEventhandlers()
   9:     {
  10:         ButtonContinue.Click += (s, e) => ContinueClicked(s, e);
  11:     }
  12:  
  13:     public override PageIdentifier PageIdentifier
  14:     {
  15:         get { return PageIdentifier.TestPage1; }
  16:     }
  17:  
  18:     public ICollection<Language> Languages
  19:     {
  20:         set
  21:         {
  22:             DropDownListLanguages.DataSource = value;
  23:             DropDownListLanguages.DataValueField = "LocaleId";
  24:             DropDownListLanguages.DataTextField = "Name";
  25:             DropDownListLanguages.DataBind();
  26:         }
  27:     }
  28:  
  29:     public string UserName
  30:     {
  31:         get { return TextBoxName.Text; }
  32:     }
  33:  
  34:     public int SelectedLanguageId
  35:     {
  36:         get { return int.Parse(DropDownListLanguages.SelectedValue); }
  37:     }
  38:  
  39:     public event EventHandler ContinueClicked;
  40:  
  41: }

This should give you an idea how little time and coding it takes to produce testable, maintainable and understandable webpages, that also nicely fit into existing projects.

So, that's it for now ! The post turned out to be a bit longer then I expected, so i'll post the remainder of the example, along with source code in a next post. Stay tuned !

Currently rated 3.3 by 6 people

  • Currently 3,333333/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

C# | Design Patterns | Mocking | MVP

Comments are closed
(c) 2008 Qsoft.be