Bradley Braithwaite
1 import {
2 Learning,
3 JavaScript,
4 AngularJS
5 } from 'bradoncode.com'
6 |

ASP.Net MVP Design Pattern Tutorial

A tutorial and example of the MVP design pattern using ASP.Net.

on
on Design Patterns

Been thrown into a project that uses the MVP design pattern? This information will help you keep within the rules of the design pattern. The .Net platform is used here for code examples and context however the rules listed are not language specific.

The pattern itself is straight forward in concept however implementing it is underpinned by many other design principles which can make doing it right a little difficult. The good news is that by implementing an MVP code base you will learn a bunch of other design principles by proxy.

MVP is an acronym for Model-View-Presenter. The Model is your business logic, the View would be your UI and the Presenter is the go between from the View to the Model.

mvp diagram

Typical graphical illustration of the MVP pattern

##The MVP Pattern In Plain English

The simplest way to explain the pattern is to consider a checkout at a supermarket. Visualise that you, the customer, are the View, the cashier is the Presenter and the cash register is the Model.

mvp pattern

When it’s time to pay for your goods would you, the customer (View), reach over the counter and put your money into the cash register (Model) directly? No, you would hand your money to the cashier (Presenter) and they would make the necessary amendments to the cash register by depositing your money and taking money out to give change where necessary. You would never have direct access to the cash register.

Key point: The view will never access the model directly.

Once you have paid for your items you will leave the store and it’s likely that the next customer will pay for their items and the same cashier will process the transaction. The point being that the customer may change but the cashier will remain the same and can still conduct the transaction even with a different customer without having to be re-trained to serve that customer. The customer can change but the cashier doesn’t have to.

Key point: We need to be able to replace the View without having to change the Presenter.

Imagine if the customer was responsible for deciding whether or not they were eligible for a discount based on the goods they wish to buy? If this were the case the customer would need to be trained how to do this, then the next customer and so on. This would never happen. The likely scenario is the cashier would scan all of the items and the cash register would compute whether a discount was applicable. Logic is never the responsibility of the customer and in many cases not the responsibility of the cashier.

Key point: The Presenter will make all the decisions about interactions and the Model will make the business logic decisions.

##Survival Tips For Working With The MVP Pattern

###DOs

  • Relax
  • Learn how to write unit tests for the Presenter(s)
  • Know the difference between passive view and supervising controller. This will earn you “show off” points.
  • Have an understanding of what IoC means (This is taken from SOLID, however you can get away with learning S, I, D from the acronym for now). This free video about dependency injection and inversion of control from Tekpub may help.
  • Learn the SoC design principle
  • Understand why Interfaces are so useful in removing dependencies. Watch this great free video about loose design with interfaces from Tekpub.

###DON’Ts

  • Panic
  • Add properties to your view interfaces that are types of the ASP.Net Web Control library (or any other library that ties you to an implementation detail) e.g. don’t create a property of type GridView on your view interface
  • Add logic to your view, even innocent looking IF statements that may toggle the visibility of a control
  • Get confused by the use of ‘Model’ and think you need a view and a model for every page. Model is just a loose term that describes the business logic
  • Add anything that is Framework specific into your Presenter i.e. having lines of code like Response.Redirect(…), Request.QueryString(…) or MyGridViewControl.DataBind(). This ties you to an implementation and makes unit testing very difficult
  • Call the presenter from the design layer. An ASP.Net example would be something as follows in your .aspx page. <%=Presenter.GetDiscount() %>

Code Example

Using our checkout experience previously described let us now visualise this in code. The below code represents a simple page that lists the products in the basket, the price details and provides a method for the user to provide basic payment details and to complete their purchase. The basic UI looks as follows:

###The Presenter

The Initialize() method acts at the Load method for the page. The details of the checkout are obtained by using a session id and the products and price summary are displayed via the view. The Checkout() method will be called from the UI once the ‘Checkout’ button is clicked.

public class CheckoutPresenter
{
    private readonly ICheckoutService _checkoutService;

    private ICheckoutView _view;

    private readonly INavigationService _navigation;

    public CheckoutPresenter(ICheckoutView view, INavigationService navigationService, ICheckoutService checkoutService)
    {
        _view = view;
        _navigation = navigationService;
        _checkoutService = checkoutService;
    }

    public override void Initialize()
    {
        // get the checkout summary from the Model
        CheckoutSummary summary = _checkoutService.GetSummary(_view.SessionID);

        if (summary != null)
        {
            IList<ProductItem> productItems = new List<ProductItem>();

            foreach (Product item in summary.Products)
            {
                productItems.Add(new ProductItem() { ProductName = item.ProductName, UnitPrice = item.UnitPrice });
            }

            // set the properties to be displayed to the View
            _view.Products = productItems;
            _view.SubTotal = summary.SubTotal;
            _view.Discount = summary.Discount;
            _view.Total = summary.Total;
        }
        else
        {
            _navigation.GoTo(ViewPages.SessionNotFound);
        }
    }

    public void Checkout()
    {
        // take payment and authorise
        CheckoutPaymentDetail checkoutDetail = new CheckoutPaymentDetail();
        checkoutDetail.CardNumber = _view.CardNumber;
        checkoutDetail.CustomerName = _view.CustomerName;

        _checkoutService.Checkout(checkoutDetail);

        // redirect to confirmation page
        _navigation.GoTo(ViewPages.Confirmation);
    }
}

###The View

This is a simple Interface that contains the properties of the information we will either send to or receive from our view.

public interface ICheckoutView
{
    string SessionID { get; }
    IList<ProductItem> Products { set; }
    string CardNumber { get; }
    string CustomerName { get; }
    decimal SubTotal { set; }
    decimal Discount { set; }
    decimal Total { set; }
}

###The Model

The model contains the basic business logic to calculate the total amount of our items and apply a discount if applicable.

public class CheckoutSummary
{
    private const decimal DiscountRate = 0.10m;

    private const decimal DiscountThreshold = 10m;

    public IEnumerable<Product> Products { get; set; }

    public decimal SubTotal 
    {
        get
        {
            return this.Products.Sum(p => p.UnitPrice);
        }
    }

    public decimal Discount 
    {
        get
        {
            return (SubTotal > DiscountThreshold) ? SubTotal / 1 - (DiscountRate * 100) : 0;
        }
    }

    public decimal Total 
    {
        get
        {
            return SubTotal - Discount;
        }
    }
}

###The View Implementation in ASP.Net

public partial class Checkout : System.Web.UI.Page, ICheckoutView
{
    private CheckoutPresenter _presenter;

    public string SessionID
    {
        get { return Request.QueryString["si"]; }
    }

    public IList<ProductItem> Products
    {
        set
        {
            this.uiProducts.DataSource = value;
            this.uiProducts.DataBind();
        }
    }

    public string CardNumber
    {
        get { return this.uxCardNumber.Text; }
    }

    public string CustomerName
    {
        get { return this.uxCustomerName.Text; }
    }

    public decimal SubTotal
    {
        set { this.uiSubTotal.Text = ConvertToCurrencyString(value); }
    }

    public decimal Discount
    {
        set { this.uiDiscount.Text = ConvertToCurrencyString(value); }
    }

    public decimal Total
    {
        set { this.uiTotal.Text = ConvertToCurrencyString(value); }
    }

    public Checkout()
    {
        _presenter = new CheckoutPresenter(this, new NavigationService(), new StubCheckoutService());
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        _presenter.Initialize();
    }

    public static string ConvertToCurrencyString(decimal value)
    {
        return value.ToString("c");
    }

    protected void uxCheckout_Click(object sender, EventArgs e)
    {
        _presenter.Checkout();
    }
}

###The .ASPX code for our ASP.Net WebForm Page

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h3>Products</h3>
        <ul>
        <asp:Repeater ID="uiProducts" runat="server">
            <ItemTemplate>
                <li><%#Eval("ProductName") %> : <%# Eval("UnitPrice", "{0:c}") %></li>
            </ItemTemplate>
        </asp:Repeater>
        </ul>

        Subtotal: <asp:Label ID="uiSubTotal" runat="server"></asp:Label><br />
        Discount: <asp:Label ID="uiDiscount" runat="server"></asp:Label><br />
        Total: <asp:Label ID="uiTotal" runat="server"></asp:Label>

        <h3>Checkout</h3>
        <div>
            <asp:Label ID="uiCardNumber" runat="server" AssociatedControlID="uxCardNumber">Card Number:</asp:Label>
            <asp:TextBox ID="uxCardNumber" runat="server" ></asp:TextBox>
        </div>
        <div>
            <asp:Label ID="uiCustomerName" runat="server" AssociatedControlID="uxCustomerName">Card Name:</asp:Label>
            <asp:TextBox ID="uxCustomerName" runat="server"></asp:TextBox>
        </div>
        <asp:Button ID="uxCheckout" runat="server" Text="Checkout" onclick="uxCheckout_Click" />
    </div>
    </form>
</body>
</html>

###The View Implementation in Windows Forms

public partial class Checkout : Form, ICheckoutView
{
    private readonly string _sessionID;

    private CheckoutPresenter _presenter;

    public string SessionID
    {
        get { return this._sessionID; }
    }

    public IList<ProductItem> Products
    {
        set 
        {
            this.uiProducts.DataSource = value;
        }
    }

    public string CardNumber
    {
        get { return this.uxCardNumber.Text; }
    }

    public string CustomerName
    {
        get { return this.uxCustomerName.Text; }
    }

    public decimal SubTotal
    {
        set { this.uiSubTotal.Text = ConvertToCurrencyString(value); }
    }

    public decimal Discount
    {
        set { this.uiDiscount.Text = ConvertToCurrencyString(value); }
    }

    public decimal Total
    {
        set { this.uiTotal.Text = ConvertToCurrencyString(value); }
    }

    public Checkout(string sessionID)
    {
        this._sessionID = sessionID;
        _presenter = new CheckoutPresenter(this, new NavigationService(), new StubCheckoutService());
        InitializeComponent();
    }

    public static string ConvertToCurrencyString(decimal value)
    {
        return value.ToString("c");
    }

    private void uxCheckout_Click(object sender, EventArgs e)
    {
        _presenter.Checkout();
    }

    private void Checkout_Load(object sender, EventArgs e)
    {
        _presenter.Initialize();
    }
 }

The key point in this code is that there are two different view implementations. One is coded in ASP.Net WebForms and the other is Windows Forms but they both function from the same Presenter.

###Unit Test

If you are creating unit tests for your presenters (and if not, why not?) a basic test covering the Initialize() method of our CheckoutPresenter would look something as per below:

/// <summary>
/// This tests that for a valid session id the view properties are set correctly
/// </summary>
[TestMethod]
public void InitializeWithValidSessionIdBindsCorrectViewProperties()
{
    // Arrange
    Mock<ICheckoutView> mockView = new Mock<ICheckoutView>();
    mockView.Setup(v => v.SessionID).Returns("123");
    mockView.SetupSet(v => v.Products = It.Is<List<ProductItem>>(i => i.Count == 3)).Verifiable();
    mockView.SetupSet(v => v.Total = It.Is<decimal>(t => t == 8.97m)).Verifiable();
    mockView.SetupSet(v => v.SubTotal = It.Is<decimal>(t => t == 8.97m)).Verifiable();
    mockView.SetupSet(v => v.Discount = It.Is<decimal>(t => t == 0m)).Verifiable();

    Mock<INavigationService> mockNavigator = new Mock<INavigationService>();

    CheckoutPresenter presenter = new CheckoutPresenter(mockView.Object, mockNavigator.Object, GetCheckoutServiceStub());

    // Act
    presenter.Initialize();

    // Assert
    mockView.Verify();
}

If you’re creating a new ASP.Net project using MVP consider using the ASP.Net Web Forms MVP Framework from the Microsoft team. If you are creating a new project that is web based only you should seriously consider MVC over MVP. Take a look at the ASP.Net MVC Framework.

###Get the Source Code

The code used in the examples can be found here.

SHARE
Don't miss out on the free technical content:

Subscribe to Updates

CONNECT WITH BRADLEY

Bradley Braithwaite Software Blog Bradley Braithwaite is a software engineer who works for the search engine duckduckgo.com. He is a published author at pluralsight.com. He writes about software development practices, JavaScript, AngularJS and Node.js via his website . Find out more about Brad. Find him via:
You might also like:
mean stack tutorial AngularJS Testing - Unit Testing Tutorials