Building the MVP StoreFront, 'Gutthrie-style'

by sven 26 December 2008 22:03

[note: some cautious readers might see a slight resemblance with Scott's first MVC post. While it seems like a total coincidence - it's not]

The reason for this post is I'm not all that impressed with what Microsoft is touting as a "a real world asp.net MVC example", being Oxite. And apparently, others are concerned as well.  I will show that the MVC Storefront can be built just as fast, just as testable and at least as understandable in our MVP architecture. For a primer on the MVP architecture that will be used here, make sure to check out this post.

A Simple E-Commerce Storefront Application

I'm going to use a simple e-commerce store application to help illustrate how our MVP architecture works. For today's post I'll be implementing a product listing/browsing scenario in it.

Specifically, we are going to build a store-front that enables end-users to browse a list of product categories when they visit the Categories page on the site:

image

When a user clicks on a product category hyperlink, they'll navigate to a product category page that lists the active products within the specific category. There the user will be able to click on the product to edit it, or add a new product. (This page and the actual edit-page will be implemented in a next post)

image

We'll build this functionality using the previously presented MVP architecture.  This will enable us to maintain a "clean separation of concerns" amongst the different components of the application, and enable us to more easily integrate unit testing and test driven development.

Creating a new MVP Application

Since we don't have a 'framework', we just create an empty solution called "MvpStoreFront" with 3 projects : Web, Business and Tests, and copy the files from our MvpWebTemplate to their respective places in the folder structure. Then we need to clean up the files a little, in particular delete the Testpages aspx's, and also delete their entries in the Identifier enums and in the NavigationComponent. Also, we won't need a SecurityComponent or SessionComponent, so we can delete those as well. Don't forget to remove them from the ServiceLocator and BasePresenter 'external dependencies' region

So, by now we are 5 minutes far in our development effort and our solution should look like :

image

Of course, any testing framework can be used !

Understanding the Folder Structure of the Project

The business project is by far the most important project. Here we have our

  • ViewInterfaces : the contracts for the views
  • Presenters : the application layer in Evan's DDD or "Controllers" in MVC
  • DomainObjects : the prime citizens of our architecture. These will hold the data, and encapsulate business logic
  • Components & their interfaces : Repositories and business logic that is not encapsulated in the DomainObjects.

We also have the enumerations here that will help us with that sweet strong typing, and the ServiceLocator for our external dependencies (we use Unity)

While we don't force to use this structure, our example project uses this layout so it's less work to just keep it this way. But you're free to do as you wish.

Mapping URLs to ASPX files

Since this pattern is built on ASP.NET WebForms, incoming URLs typically map to files stored on disk.  For example, a "/Products.aspx" URL typically has an underlying Products.aspx template file on disk that handles processing over to it's attached presenter. This is one of the main differences with the MVC, MVP still uses the Page Controller pattern instead of the Front Controller Pattern . So, to quote Fowler, 'the approach of one path leading to one file that handles the request is a simple model to understand'. Since there is nothing new to say about the URL routing (at least not until the fancy routing engine becomes available for WebForms in ASP.NET 4.0), let's just create a few empty pages, home.aspx, categories.aspx, list.aspx, detail.aspx that all derive from a masterpage site.master. (To be strict, it's not even necessary to add the empty aspx-pages here, we could do it when we're ready to implement them). Also we add identifiers in the PageIdentifier-enum, and add the mapping in the NavigationComponent:

In total, we did the following things, another 5 minutes worth of development effort :

imagein MvpStoreFront.Web

image in MvpStoreFront.Business -> Enumerations.PageIdentifier.cs

image in MvpStoreFront.Web -> Code.Navigationcomponent.cs

Building our DataModel Objects

To be concise with Scott's example, ill just put the NorthWind.dbml in the DomainObjects folder. Normally, it would belong in a Datalayer project, but for this contrived example, it'll  do. So make sure you have a copy of the NorthWind DB on your system (it can be found as a separate download on the MSDN site)

image

Although this example is contrived, I will not add the functions to retrieve categories and products in the datacontext class. After all this is a post to talk about the testability of the architecture, and it's much easier to test if our data-access method are behind an abstraction. So we'll add an INorthWindRepository interface and NorthwindRepository class to the ComponentInterfaces and Components folders. Within this repository we'll define a few methods that encapsulate some LINQ expressions that we can use to retrieve the unique Category objects from our database, retrieve all Product objects within a specific category in our database, as well as retrieve an individual Product object based on a supplied ProductID:

image

   1: public interface INorthwindRepository
   2: {
   3:     IList<Category> GetCategories();
   4:     IList<Product> GetProductsByCategory(string Category);
   5:     Product GetProductById(int id);
   6: }
   7:  
   8: public class NorthWindRepository : INorthwindRepository
   9: {
  10:     NorthwindDataContext context = new NorthwindDataContext();
  11:     public IList<Category> GetCategories()
  12:     {
  13:         return context.Categories.ToList();
  14:     }
  15:     public IList<Product> GetProductsByCategory(string category)
  16:     {
  17:         return context.Products.Where(c => c.Category.CategoryName==category).ToList();
  18:     }
  19:     public Product GetProductById(int id)
  20:     {
  21:         return context.Products.Single(p => p.ProductID == id);
  22:     }
  23: }
  24:  

This only takes a few minutes of work, and now we have all of the data code/objects we need to implement our presenter functionality. 

Finishing the Implementation of our presenters

Finally we can start on our Application ! So we add presenters and view-interfaces for every page we will have. The interface all derive from IPageView, except for the masterpage's interface. This one will just derive from IView. So we added the following files, another 2 minutes worth of effort :

image

Given what we are trying to build here, we quickly find out the contracts of the view :

  • on the masterpage, we just need a URL for the home and browse categories pages
  • on our categories page, we need for each category a name and URL,
  • on the productlist page we'll need for each product in the category a name and edit-URL, and an "add product"-button 
  • and finally on the edit page, we'll need some more product info like supplier, price and also a "save"-button.

So, let's start on the MasterPagePresenter. We derive from BasePresenter<IMasterPage>, let Resharper generate all code, and manually type the following:

image

That was a quickie! On to the next, our CategoriesPresenter. Derive from BasePresenter<ICategories>, again let Resharper generate all code, and manually type :

image

Note that this code should be shortened if we moved the call to the NavigationComponent to get the URL in the View. Indeed, if we moved this logic to the View, all the View needs is a list of Categories, and the Presenter only needs a single line of code :

image

But we deliberately made the choice to do as much code as possible in the Presenters, and keep the Views thin.

To have easy access to the NorthwindRepository, use the following code in the BasePresenter (generated with the 'exdep' code snippet ):

image

Pfew, yet again another 2 minutes of work. The first presenters  are ready, so we can start testing them (as a matter of fact, in TDD the test would already be there) .

Unit Testing our Presenter

You might be surprised that the next step we are going to work on is to test our application logic and functionality.  You might ask - how is that even possible?  We haven't implemented our Views, and our application currently doesn't render a single tag of HTML.  Well, part of what makes an MVP approach attractive is that we can unit test the Presenter and Model logic of applications completely independently of the View/Html generation logic.  As you'll see below we can even unit test these before we create our Views.

We could for example verify that our categories presenter adds all the categories that are found in the repository. Notice also how we'll mock out (using Moq) the NorthwindRepository class. After all, if the unit test actually goes to the database, it's not a real unit test anymore, right ? 

So, in our testsetup we mock away the external dependencies, being NorthwindRepository and NavigationComponent, and then we create the test that verifies if we pass the Categories to the View :

   1: [TestClass]
   2: public class PresenterTests
   3: {
   4:     [TestInitialize]
   5:     public void Setup()
   6:     {
   7:         ServiceLocator.Instance().InitializeForUnitTesting();
   8:         var mockedNavigation = new Mock<INavigationComponent>();
   9:         ServiceLocator.Instance().RegisterInstance(mockedNavigation.Object);
  10:         var mockedRepository = new Mock<INorthwindRepository>();
  11:         mockedRepository.Expect(x => x.GetCategories()).Returns(new List<Category>(){
  12:             new Category() { CategoryID = 1 }, 
  13:             new Category() { CategoryID = 2 }});
  14:         ServiceLocator.Instance().RegisterInstance(mockedRepository.Object);
  15:     }
  16:     [TestMethod]
  17:     public void Categories_presenter_should_set_all_categories_in_view()
  18:     {
  19:         var mockedView = new Mock<ICategories>();
  20:         mockedView.StubAll();
  21:         var presenterUnderTest = new CategoriesPresenter(mockedView.Object);
  22:         presenterUnderTest.LoadViewFromModel();
  23:         Assert.IsNotNull(mockedView.Object.CategoryList);
  24:         Assert.AreEqual(2, mockedView.Object.CategoryList.Count);
  25:     }
  26: }

Our test should run just fine, and it does :

image

I think you'll find that this MVP architecture makes writing tests easy, and enables a nice TDD workflow.

Creating our views

Since the Web application is just a regular ASP.NET web application, and we already created the site.master master page, and created our empty aspx files, there is not much left to say here. The site.master will hold the menu and the ContentPlaceholder for the pages :

image

Rendering our Categories View:

If you remember from the screenshots at the very beginning of this post, we want to display a list of the product categories within our Categories view:

image

Rendering approach 1 : Using Inline code

ASP.NET Pages, User Controls and Master Pages today support <% %> and <%= %> syntax to embed rendering code within html markup.  We could use this technique within our Categories View to easily write a foreach loop that generates a bulleted HTML category list:

image

Rendering Approach 2: Using Server Side Controls

Another way to render the UI is using server controls. In fact, I prefer this approach, because I'll show (in a future post) a non-intrusive database-driven translation mechanism that requires servercontrols in the pages. The mechanism is non-intrusive because it can be added late to the development cycle, only one line of code has to be added to the BasePage or BasePresenter.

To start using server controls, change the markup to :

image

Then we code the setter of the CategoryList property as following : (remember that this property is not really a list but a dictionary because we did the URL-creation login in the presenter. If we chose to pass only a list of Category-objects, this would be the place to create the URL using the NavigationComponent) image

Since we used server controls and regular asp.net pages, we don't have the clean html output rendered as can be achieved with MVC. But it's up to you to decide if that's a showstopper.

Summary

So, this was a pretty long post but hopefully shows that while MVC is all the rage nowadays (and rightfully so !!), there is still the possibility to create testable, maintainable, understandable regular asp.net applications using this MVP approach. 

Hope this helps,

Sven.

MvpStoreFront.zip (553,73 kb)

Currently rated 2.8 by 4 people

  • Currently 2.75/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

C# | Design Patterns | MVP

Comments

Comments are closed
(c) 2008 Qsoft.be