EPiServer  /  CMS April 06, 2010

Introducing EPiMVP – A Framework for using Web Forms MVP with EPiServer CMS

This weekend me and Mattias Johansson released a first CTP version of a new open source project, EPiMVP. The project is a framework for using the Web Forms MVP project with EPiServer CMS.

Model View Presenter

While Web Forms code behind system offers some feeling of separation of concerns it’s tightly coupled to a web context making it hard to test and offering little real separation of concerns except for between markup and code. That is, it’s often hard to run code in a Web Forms project without a HTTP context and it’s common to have most of an applications logic in pages and user controls, albeit in their code behind files.

ASP.NET MVC addresses many of these problems, but unfortunately there are occasions where we still need to use Web Forms. As for EPiServer CMS there has been a few attempts to build sites with it using ASP.NET MVC (this is one) and while it’s possible EPiServer isn’t “native” to MVC. There are some concepts in it which are tailored for Web Forms and it comes with quite a few useful web controls that help us build sites very quickly. My guess is that EPiServer will adapt to ASP.NET MVC quite soon, but until then we need to find ways of achieving some of the benefits that MVC offers with Web Forms, and one solution, that has been discussed and even adopted by some for while now is to use the Model View Presenter pattern (MVP).

While it doesn’t give us as clean and simple architecture as MVC does it does give us improved separation of concern and testability.

Web Forms MVP

The Web Forms MVP project is a framework for using the Model View Presenter pattern with ASP.NET Web Forms. While we don’t need a framework to use MVP it can certainly make it easier for us as much of the architectural heavy lifting is already done. The projects, whose current release is named CTP 6 and is considered stable as it’s used by several sites in production.

EPiMVP in turn aims to make it possible, and easy, to use the ASP.NET Web Forms MVP framework with EPiServer CMS. It does this by offering a PresenterFactory which instantiates presenters aware of EPiServer details such as the current (PageData) page and by providing base classes for views and presenters. On top of that it also enables you to use Inversion of Control by means of dependency injection, either by creating your own PresenterFactory inherited from EPiMVP’s to use your favorite IoC container or by using one of the ones we ship with EPiMVP, supporting Ninject and StructureMap.

Release details

It’s important to note that this is just a first CTP of the project. By releasing it early we hope to get feedback from the community. So don’t hesitate to send us bug reports, feature suggestions or patches. You should also note that you are currently required to use Page Type Builder to use EPiMVP.

How to use EPiMVP – an example

We hope to provide more detailed examples and preferably also an example project later on. For now let’s look at a simple example of how to use EPiMVP.

Our goal is to create a page which displays a button and a message instructing visitors to press the button in order to get flattered. When the button is pressed the message should change to something flattering for the user and the button should be hidden. Both messages should be set by the editor in EPiServer CMS’s edit mode. We will create this functionality using Test-Driven Development.

Setting up the project

In this example we’ll start off from a newly installed EPiServer CMS site with the public templates installed. To it we add references to EPiMVP.dll, PageTypeBuilder.dll and WebFormsMVP.dll. We also need to make sure that Castle.Core.dll and Castle.DynamicProxy2.dll exist in our projects bin folder as they are required by Page Type Builder.

Setting up a test project

As much of the benefits of using EPiMVP revolves around testability this example uses Test-Driven Development. I should however mention that if you are just interested in what EPiMVP is and how it works you skip this and other parts about testing.

Anyway, next we add a new class library project to the solution for our tests. In this unrealistic scenario, where our web project is still named PublicTemplates, the name of the project isn’t very important. I named it FlatterProject.Tests. To this project we’ll need to add references to the web project, EPiMVP.dll, EPiServer.dll, EPiServer.BaseLibrary.dll, PageTypeBuilder.dll and WebFormsMVP.dll.

We will also need a unit testing framework and a mocking framework. You can of course use any frameworks you want. I prefer xUnit.net and Moq so I added references to xunit.dll and Moq.dll to the test project.

Finally we should rename the auto generated class and file in the test project, Class1.cs, to FlatterPresenterTests.cs.

Creating the first test

We begin by creating a test for the most first requirement, that a message should be displayed to visitors. The message should be fetched from a property set by the editor. In other words our view should display the contents of a string property from a PageData object. A test for this could look like this:

[Fact]
public void WhenViewIsLoaded_ThenMessageIsSetToInstructionsFromPageData()
{
    Mock<IFlatterView> fakeView = GetFakeView();
    FlatterPage pageData = new FlatterPage();
    pageData.Instructions = Guid.NewGuid().ToString();
    new FlatterPresenter(fakeView.Object, pageData);

    fakeView.Raise(view => 
        view.Load += null, new object[] { null, null});

    Assert.Equal(pageData.Instructions, fakeView.Object.Model.Message);
}

private Mock<IFlatterView> GetFakeView()
{
    Mock<IFlatterView> fakeView = new Mock<IFlatterView>();
    fakeView.SetupGet(view => view.Model)
        .Returns(new FlatterModel());
    return fakeView;
}

The first four lines in the test method (attributed with a Fact attribute since we’re using xUnit.net) is the set up phase of the test. In them, and the GetFakeView method, we can see that we will need an interface named IFlatterView which should have a Model property of type FlatterModel. We can also see that there should be a page type class named FlatterPage with an Instructions property. Finally there should also be a FlatterPresenter class. This may all seem a bit confusing now, and normally we wouldn’t write the test in it’s entirety before we start writing the actual code. Please bear with me and it will hopefully become less confusing when we make the test compile by creating the components that it uses.

Creating the view-model, FlatterModel

While the page type, FlatterPage is sort of the domain model in this little example Web Forms MVP, just like what is recommended when building sites with ASP.NET MVC, uses the concept of a view model. That is a model object that is tailor made for the view. Using view models allows us to keep our views very simple, or dumb if you will. In our case we only want to communicate a single piece of data to the view, a string message. Therefore the FlatterModel class, which I placed in a new folder named Models in the web project, is very straight forward.

public class FlatterModel
{
    public string Message { get; set; }
}

Creating the abstraction of the view, IFlatterView

While we at this point only are interested in developing the presenter it will have to be coupled to the view. However, we want to be able to develop the presenter without having to first create the view. We also don’t want the presenter to be coupled to any specific view implementation. Therefore we create an abstraction, in the form of the interface IFlatterView, that specifies the interface of the view, allowing us to focus on the presenter.

using System;
using EPiServer.Models;
using WebFormsMvp;

namespace EPiServer.Views
{
    public interface IFlatterView : IView<FlatterModel>
    {
    }
}

We’ll soon add more features to the view, but for now it’s enough that it implements the IView interface that ships with EPiMVP. In doing so the IFlatterView interface will contain two members that a view will need to have in order to implement it, a property named Model of type FlatterModel and an event named Load.

I placed the IFlatterView interface in a new folder named Views in the web project.

Creating the page type, FlatterPage

In order to satisfy our first test it’s enough that our page type class, FlatterPage, has a single property named instructions. We’ll add more properties later, and we’ll add the stuff necessary for Page Type Builder to recognize it as a page type when we create the concrete view.

public class FlatterPage : TypedPageData
{
    public virtual string Instructions { get; set; }
}

I placed FlatterPage in yet another new folder in the web project which I named PageTypes.

Creating the presenter, FlatterPresenter

Our test will almost compile now. All that’s left is the presenter, FlatterPresenter, which I placed in a new folder in the web project that I named (drum roll) Presenters.

public class FlatterPresenter 
    : EPiPresenter<IFlatterView, FlatterPage>
{
    public FlatterPresenter(IFlatterView view, 
        FlatterPage pageData)
        : base(view, pageData)
    {
    }
}

Having created the above presenter our test will compile. As you can see the presenter inherits from EPiPresenter<IFlatterView, FlatterPage>. In doing so we must provide a constructor that accepts an IFlatterView as first parameter and a FlatterPage as the second parameter and pass both parameters on to the base class’ constructor.

While our test does compile with the above presenter it will of course fail as the presenter so far doesn’t do anything. We run the test anyway to prove to ourselves that it does fail. Doing so should (depending on test runner, I use TestDriven.Net) produce an error message such as this:

TestCase 'FlatterProject.Tests.FlatterPresenterTests.
    WhenViewIsLoaded_ThenMessageIsSetToInstructionsFromPageData'
failed: Assert.Equal() Failure
Expected: 7a99b23a-91ad-4a26-9e6c-5d9f9588739b
Actual:   (null)
    FlatterPresenterTests.cs(23,0): at FlatterProject.Tests.
        FlatterPresenterTests.WhenViewIsLoaded_ThenMessageIsSetToInstructionsFromPageData()

0 passed, 1 failed, 0 skipped, took 0,87 seconds (xunit).

The test fails telling us that it expected a string value equal to some GUID (a random value for the PageData.Instructions property assigned in the test) but found null in the model objects message property.

To make the test pass we create an event handler for the views load event which will set the model object’s Message property to the Instructions property from the FlatterPage object found in the presenters CurrentPage property (which it has inherited from EPiPresenter).

public class FlatterPresenter 
    : EPiPresenter<IFlatterView, FlatterPage>
{
    public FlatterPresenter(IFlatterView view, 
        FlatterPage pageData)
        : base(view, pageData)
    {
        View.Load += View_Load;
    }

    private void View_Load(object sender, EventArgs e)
    {
        View.Model.Message = CurrentPage.Instructions;
    }
}

We compile and run our tests again, with success!

1 passed, 0 failed, 0 skipped, took 0,86 seconds (xunit).

The example continued

Feeling all warm and fuzzy having made the test pass we should of course do a little dance, have a few beers (or carrots, much healthier) and move on to the next test. This test should test that the message changes from the Instructions property of the PageData object to it’s Praise property when the views FlatterRequested event occurs.

[Fact]
public void WhenFlatterRequested_ThenMessageIsSetToPraiseBody()
{
    Mock<IFlatterView> fakeView = GetFakeView();
    FlatterPage pageData = new FlatterPage();
    pageData.Praise = Guid.NewGuid().ToString();
    new FlatterPresenter(fakeView.Object, pageData);

    fakeView.Raise(view => view.FlatterRequested += null);

    Assert.Equal(pageData.Praise, fakeView.Object.Model.Message);
}

At first glance this test may seem identical to the previous one, but there are a few differences. First of all the test tells the (fake/mock) view to fire a different event, the FlatterRequest event which does not yet exist. Also, the assert is different as it now checks that the model’s Message property is equal to the PageData object’s Praise property (which again doesn’t exist) instead of it’s Instructions property.

To make it our test compile we add a FlatterRequested event to IFlatterView

public interface IFlatterView : IView<FlatterModel>
{
    event Action FlatterRequested;
}

And a Praise property to FlatterPage.

public class FlatterPage : TypedPageData
{
    public virtual string Instructions { get; set; }

    public virtual string Praise { get; set; }
}

We run the test and to our great satisfaction it fails.

TestCase 'FlatterProject.Tests.
    FlatterPresenterTests.WhenFlatterRequested_ThenMessageIsSetToPraiseBody'
failed: Assert.Equal() Failure
Expected: 1ac797b8-153e-4dfc-9b9b-f5048e391d90
Actual:   (null)
    FlatterPresenterTests.cs(45,0): at FlatterProject.Tests.
        FlatterPresenterTests.WhenFlatterRequested_ThenMessageIsSetToPraiseBody()

0 passed, 1 failed, 0 skipped, took 0,84 seconds (xunit).

To make the test pass we need the presenter to handler for the view’s FlatterRequested event in which it will set the model’s Message property to the CurrentPage’s Praise property.

public class FlatterPresenter 
    : EPiPresenter<IFlatterView, FlatterPage>
{
    public FlatterPresenter(IFlatterView view, 
        FlatterPage pageData)
        : base(view, pageData)
    {
        View.Load += View_Load;
        View.FlatterRequested += View_FlatterRequested;
    }

    private void View_Load(object sender, EventArgs e)
    {
        View.Model.Message = CurrentPage.Instructions;
    }

    private void View_FlatterRequested()
    {
        View.Model.Message = CurrentPage.Praise;
    }
}

Compiling and running our tests shows that we now have two passing tests.

2 passed, 0 failed, 0 skipped, took 0,94 seconds (xunit).

A final test – interacting with the view

So far we have only communicated with the view by modifying it’s Model property. We do have one last thing to test however, which will demonstrate another way to communicate with the view. The requirements said that when a visitor presses the button to get flattered the button should be hidden. In less graphical terms this means that the view should stop accepting requests for flatter, which we’ll create a test for.

[Fact]
public void WhenFlatterRequested_ThenViewDoesNotAcceptMoreRequestsForFlatter()
{
    Mock<IFlatterView> fakeView = GetFakeView();
    FlatterPage pageData = new FlatterPage();
    new FlatterPresenter(fakeView.Object, pageData);

    fakeView.Raise(view => view.FlatterRequested += null);

    fakeView.Verify(view => view.StopAcceptingFlatterRequests());
}

As usual this test doesn’t compile, this time because the IFlatterView interface doesn’t have a StopAcceptingFlatterRequests method. That’s easily fixed.

public interface IFlatterView : IView<FlatterModel>
{
    event Action FlatterRequested;

    void StopAcceptingFlatterRequests();
}

Again our test project compiles but the last test fails as the presenter isn’t telling the view to stop accepting requests for flatter when a request is made. One line of code in the presenter’s View_FlatterRequested method fixes that.

private void View_FlatterRequested()
{
    View.Model.Message = CurrentPage.Praise;
    View.StopAcceptingFlatterRequests();
}

Having done that our tests all pass and the requirements have been met. We now have a fully functional presenter!

Implementing the view

In theory our view could be just about anything, a web service, a talking donkey, anything that implements IFlatterView. Boring as we are we of course want it to be an ASPX page. Here’s the markup for it:

<%@ Page Language="C#" AutoEventWireup="true" 
MasterPageFile="~/Templates/Public/MasterPages/MasterPage.master" 
CodeBehind="FlatterView.aspx.cs" 
Inherits="EPiServer.Views.FlatterView" %>

<asp:Content ID="Content1" ContentPlaceHolderID="MainBodyRegion" runat="server">
    <%= Model.Message %>
    <asp:Button ID="btnFlatterMe" 
        Text="Tell me how good I am!" 
        OnClick="btnFlatterMe_Click" 
        runat="server" />
 </asp:Content>

As you can see it’s pretty straight forward. It uses the MasterPage from the public templates and displays the Model’s Message property along with a button for flatter requests.

It’s code behind file looks like this:

using System;
using WebFormsMvp;
using EPiMVP;
using EPiServer.Models;
using EPiServer.Presenters;

namespace EPiServer.Views
{
    [PresenterBinding(typeof(FlatterPresenter))]
    public partial class FlatterView 
        : EPiView<FlatterModel>, IFlatterView
    {
        public event Action FlatterRequested;

        protected void btnFlatterMe_Click(
            object sender, EventArgs e)
        {
            if (FlatterRequested != null)
                FlatterRequested();
        }

        public void StopAcceptingFlatterRequests()
        {
            btnFlatterMe.Visible = false;
        }
    }
}

Worth noticing here is the PresenterBinding attribute which instructs Web Forms MVP to instantiate a FlatterPresenter when a request is made for the view. You shold also note that it, apart from implementing IFlatterView, inherits EPiView, a base class for views inheriting from EPiServer’s TemplatePage that ships with EPiMVP.

With the view done we only have two things left. We need to add some attributes to the page type class (FlatterPage) to make Page Type Builder recognize it and interpret it as a page type.

[PageType(Filename = "~/Views/FlatterView.aspx")]
public class FlatterPage : TypedPageData
{
    [PageTypeProperty(EditCaption = "Instructions", HelpText =
        "Instructions for the visitor on how to get flattered",
        UniqueValuePerLanguage = true)]
    public virtual string Instructions { get; set; }

    [PageTypeProperty(EditCaption = "Praise Body",
        HelpText = @"The praise that you want to display to the 
            user when she clicks the Flatter me button",
        UniqueValuePerLanguage = true)]
    public virtual string Praise { get; set; }
}

Finally we need to configure Web Forms MVP to use a presenter factory from EPiMVP, EPiPresenterFactory (EPiMVP also ships with two other presenter factories, NinjectPresenterFactory and StructureMapPresenterFactory but that’s beyond the scope of this post). That’s done in the Application_Start method in global.asax.

protected void Application_Start(Object sender, EventArgs e)
{
    WebFormsMvp.Binder.PresenterBinder.Factory = new EPiMVP.EPiPresenterFactory();
    XFormControl.ControlSetup += new EventHandler(XForm_ControlSetup);
}

With that done we’re all done. All that’s left is to create a new page in edit mode and try it out!

EPiMVP Example1 EPiMVP Example2

Source code

You can grab a zip file with the relevant files for the example project here. If you’d like to run the code, install a site with the public templates and then either modify it to look like the downloaded solution or copy the downloaded projects into it’s solution.

Give us feedback!

As I’ve mentioned before this is just a CTP release of something that is still very experimental and it’s main purpose is to see what others think of the project. So, since you have read this far you might as well leave a comment and tell us what you think! :)

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

My book

Want a structured way to learn EPiServer 7 development? Check out my book on Leanpub!

More about EPiServer CMS