ASP.NET: Model View Presenter Pattern

For a while now, I've been trying to determine a more efficient way of testing my company's applications.  We don't have a QA team, so we are forced to repeatedly develop, unit test, system test and regression test our applications. 

In the past, we had many developers to share this responsibility with - so it felt like less of a burden.  Now, though, as our applications get bigger and bigger and our developer pool gets smaller and smaller, the impact of the weeks of testing cause an inordinate amount of strain on our project timelines and on our developer pool.

I've researched automated testing tools - but the ones that simulate clicks on a web application are quite cost prohibitive.  We are beginning a big .net 2.0 push - so it makes sense to re-evaluate our coding practices to make us as efficient as possible.

After reviewing the various methods to perform more systematic testing and less manual testing, I found a few references to the Model View Presenter (MVP) design pattern.  The concept is to keep as much of the Page level logic abstracted from the actual page itself by having a view that can easily be implemented once on the page and once in the test harness.  Billy McCaffery has an excellent article here

This gave me some great insight on the appropriate way to structure the project and the base concepts.  Then I found a release from MS called the Web Client Software Factory.  This is a complete starter kit that ties all of the various pieces of a web application/site into a nice complete package.  It has some dependencies on .net 3.0 for the main page flow engine - but actually packages everything together quite nicely.  The item that really jumped out at me, though, was the SampleWebClient solution included with the download.  It - surprise surprise - uses the MVP pattern for its ASP.NET 2.0 application.  Very cool stuff. 

To test out the concepts, I created a simple solution in ASP.NET 2.0 and VB.Net (no snickering, you C# guys).  Since a Search screen is a great beginner example, I started with that.  You can download the entire solution here

Essentially, every Search screen will consist of a series of classes:

  1. Web Page (and code-behind)
  2. Search View Interface
  3. Search Presenter
  4. Search Model (the data)

First, I'll start with the Search View Interface:

Public Interface ISearchView

    WriteOnly Property SearchResults() As List(Of Model.Person)

    WriteOnly Property ResultCount() As Int32

End Interface

As you can see, there are 2 WriteOnly properties that any class that uses this interface must implement.

To put this into perspective, though, you should see the webpage.  The HTML looks like:

    <div>
        <asp:Label ID="Label1" runat="server" Text="Keyword:"></asp:Label>
        <asp:TextBox ID="txtKeyword" runat="server"></asp:TextBox>
        <asp:Button ID="btnSearch" runat="server" Text="Search" /></div>        
    <div>
        <asp:Label ID="lblResults" runat="server" 
            Text="Results Returned: {0}"></asp:Label>
        <asp:HyperLink ID="HyperLink1" runat="server" 
            NavigateUrl="AddressForm.aspx?Mode=add"
            Target="_self">Add Entry</asp:HyperLink></div>
    <div>
        <asp:GridView ID="gvResults" runat="server" AutoGenerateColumns="false">
            <Columns>
                <asp:BoundField HeaderText="First Name" DataField="firstName" />
                <asp:BoundField HeaderText="Last Name" DataField="lastName" />
                <asp:BoundField HeaderText="E-mail Address" 
                    DataField="emailAddress" />
                <asp:BoundField HeaderText="Source" DataField="sourceName" />
                <asp:HyperLinkField HeaderText="" DataNavigateUrlFields="ID" 
                    DataNavigateUrlFormatString="AddressView.aspx?EntryID={0}" 
                    Text="View" />
                <asp:HyperLinkField HeaderText="" DataNavigateUrlFields="ID" 
                    DataNavigateUrlFormatString=
                        "AddressForm.aspx?EntryID={0}&mode=edit"
                    Text="Edit" />                
            </Columns>
        </asp:GridView>    
    </div>

This is a very simple Search Screen - containing a textbox for the search criteria, a button to initiate the search and a grid for the results.

Next is to implement the ISearchView into the code behind for the page.  That code behind looks like the following:

Imports SamuraiProgrammer.MVP.Presenters
Partial Public Class Search
    Inherits System.Web.UI.Page
    Implements MVP.Presenters.ISearchView

    Private _presenter As SearchPresenter

    Protected Sub Page_Load(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles Me.Load
        Dim myPresenter As New SearchPresenter(Me)
        _presenter = myPresenter
    End Sub

    Protected Sub btnSearch_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles btnSearch.Click
        _presenter.Search(Me.txtKeyword.Text)
    End Sub

    Public WriteOnly Property ResultCount() As Integer _
            Implements ISearchView.ResultCount
        Set(ByVal value As Integer)
            Me.lblResults.Text = String.Format(Me.lblResults.Text, value)
        End Set
    End Property

    Public WriteOnly Property SearchResults() As _
        System.Collections.Generic.List(Of MVP.Model.Person) Implements _
            MVP.Presenters.ISearchView.SearchResults
        Set(ByVal value As System.Collections.Generic.List(Of MVP.Model.Person))
            With Me.gvResults
                .DataSource = value
                .DataBind()
            End With
        End Set
    End Property
End Class

There are two things you should gleam here:

  1. The WriteOnly properties specified in the ISearchView Interface correspond to setting the values of the controls on the page.
  2. The page delegates all of the true logic and business processing to that SearchPresenter class (shown below). 

This gives you an idea of the power behind this method.  With making the code behind for the page a simple delivery mechanism of the processing done in the Presenter class, it becomes very easy to test the page functionality separately from the page itself.

Lastly, there is the Presenter class - where the real "work" gets done:

Public Class SearchPresenter

    Private _sView As ISearchView

    Public Sub New(ByVal sView As ISearchView)
        _sView = sView
    End Sub

    Public Sub Search(ByVal searchText As String)
        '// Search code here
        Dim ds As New DataSet

        Dim listOfEntries As New List(Of Model.Person)

        Dim myEntry As New Model.Person()
        With myEntry
            .FirstName = "Greg"
            .LastName = "Varveris"
            .EmailAddress = "greg@samuraiprogrammer.com"
            .SourceName = "Me"
            .ID = 1
        End With

        listOfEntries.Add(myEntry)
        _sView.SearchResults = listOfEntries
        _sView.ResultCount = listOfEntries.Count()
    End Sub
End Class

In the constructor here - we receive a concrete instance of a class that implements the ISearchView interface - and as such, we now have a handle on those properties.  Therefore, we can easily pass data to the SearchResults and ResultCount property.

So now that you have seen how the MVP pattern can be applied to a page, let's take a look at testing.  Using VS Team Suite, there is a special project called a "Test Project" that allows us to create a Test Harness around a particular method or class.  It leverages many of the same method attributes as NUnit, but it is integrated into VS as only Microsoft can do.

Before we can create the Test Method, though, since we will not have a reference to the actual ISearchView implemented in the page, we need a mock SearchView class that implements the ISearchView interface:

    Private Class MockSearchView
        Implements MVP.Presenters.ISearchView

        Private _presenter As _
                    MVP.Presenters.SearchPresenter
        Private _resultCount As Int32
        Private _searchResults As _
                    List(Of MVP.Model.Person)

        Public WriteOnly Property ResultCount() _
                As Integer Implements _
                    Presenters.ISearchView.ResultCount

            Set(ByVal value As Integer)
                _resultCount = value
            End Set
        End Property

        Public WriteOnly Property SearchResults() _
                As System.Collections.Generic.List(Of Model.Person) _
                Implements Presenters.ISearchView.SearchResults

            Set(ByVal value As System.Collections.Generic.List(Of Model.Person))
                _searchResults = value
            End Set
        End Property

        Public ReadOnly Property GetResults() _
                As List(Of MVP.Model.Person)

            Get
                Return _searchResults
            End Get
        End Property

        Public ReadOnly Property GetResultCount() As Int32
            Get
                Return _resultCount
            End Get
        End Property
    End Class

Nothing is truly crazy here - except that you will see 2 additional properties in this class:

  • GetResults() - Returns the generic List of people.
  • GetResultCount() - Returns the number of results.

These are necessary because the interface specified the SearchResults and ResultCount properties as being WriteOnly.  They simply provide accessors into the member variables of the class.

Finally, we will show the Test method itself:

    <TestMethod()> _
    Public Sub SearchTest()
        Dim sView As New MockSearchView()

        Dim target As SearchPresenter = New SearchPresenter(sView)

        Dim searchText As String = "greg"

        target.Search(searchText)

        Dim actual As MVP.Model.Person = sView.GetResults()(0)
        Dim expected As New MVP.Model.Person()
        With expected
            .FirstName = "Greg"
            .LastName = "Varveris"
            .EmailAddress = "greg@samuraiprogrammer.com"
            .SourceName = "Me"
        End With

        Assert.IsTrue(expected.Equals(actual), _
            "These do not match!  The test has failed")
    End Sub

The test method simply simulates a search operation using the SearchPresenter class and then compares the actual results to the Expected results.  If it fails, it will return a "Failure" message and will report the failure to VSTS.

As you can see, by abstracting the main page functionality away from the actual page itself - into the Presenter class - you can test the main functionality systematically instead of manually.

You can download the entire solution here

Any input or thoughts are greatly appreciated.

Published 18 June 07 09:26 by Greg

Comments

No Comments
Anonymous comments are disabled