Friday, October 23, 2009

Introducing MvpFramework, a framework for "the rest of us"

A framework for webforms that makes using the MVP pattern a breeze.

 Part 2

If only we could always use MVC, but some of us aren't that lucky... So, I've been playing around with a MVP framework for a while now. I actually blogged about the Presenter factory some time ago. Since then I've spent quite a bit of time refining it. It now supports Validation and Navigation along with all the IOC goodies that structure map gives you. I figured I'd go ahead and open source it, and blog a quick demo of its features. You can download it from goggle code at http://code.google.com/p/mvpframework/source/checkout. You can just download the zip if you'd like, it's right in the trunk under lib.

So, for the demo we have a very simple domain.

image

 

It also has a Repo for each object.

 

First things first, lets make a page that allows people to create a "new" person.  I typically start with my view, since I usually have UI mock ups from my product owners and to me, it's the logical place to start.

 

Here's the view I came up with.

 

    1 using System;

    2 using System.Collections.Generic;

    3 using Demo.Business;

    4 using MvpFramework;

    5 

    6 namespace Demo.Presentation

    7 {

    8     public interface IEditPersonView:IMvpView

    9     {

   10         IEnumerable<Person> AllPeople { set; }

   11         Guid SelectedPersonId { get; }

   12         event EventHandler PersonSelected;

   13         event EventHandler PersonSaved;

   14         String Name { get; set; }

   15     }

   16 }

 

 

Note the 2 events, One for when the user selects a person to edit, and one for when they click save. If they wanna create a new one? Uhh.. we'll figure that out when we get there.

 

Also note that I've inherited IMvpView. There's actually only one property on that (bool IsInitialLoad), and you get that for free if you inherit my base classes, but more on that later.

 

Let's get to work on the presenter.

 

First I wrote the test to ensure that the "AllPeople" property is set. Here it is.

 

    1 using System.Collections.Generic;

    2 using Demo.Business;

    3 using Demo.Presentation;

    4 using NUnit.Framework;

    5 using Rhino.Mocks;

    6 

    7 namespace Demo.UnitTests.Presentation

    8 {

    9     [TestFixture]

   10     public class When_editing_a_person

   11     {

   12         private IPersonRepository repository;

   13         private List<Person> allPeople;

   14         private IEditPersonView view;

   15         private EditPersonPresenter presenter;

   16 

   17         [SetUp]

   18         public void Arrange()

   19         {

   20             repository = MockRepository.GenerateMock<IPersonRepository>();

   21             allPeople = new List<Person>();

   22             view = MockRepository.GenerateMock<IEditPersonView>();

   23 

   24         }

   25         public void Act()

   26         {

   27             repository.Stub(r => r.GetAll()).Return(allPeople);

   28             presenter = new EditPersonPresenter(view, repository);

   29         }

   30         [Test]

   31         public void all_people_is_set_to_all_people_from_the_repo()

   32         {

   33             Act();

   34             view.AssertWasCalled(v => v.AllPeople = allPeople);

   35         }

   36     }

   37 }

 

 

So, lets go make it pass.

    1 using Demo.Business;

    2 using MvpFramework;

    3 

    4 namespace Demo.Presentation

    5 {

    6     public class EditPersonPresenter:Presenter<IEditPersonView>

    7     {

    8         private readonly IPersonRepository repository;

    9 

   10         public EditPersonPresenter(IEditPersonView view, IPersonRepository repository) : base(view)

   11         {

   12             this.repository = repository;

   13             HydrateView();

   14         }

   15 

   16         private void HydrateView()

   17         {

   18             View.AllPeople = repository.GetAll();

   19         }

   20     }

   21 }

 

Ok, a few things to note.

Inherit Presenter<T> (where T is an IMvpView). Then chain your ctor up (note the :base(view)). That'll give you a property named View that you can do with what you'd like.  Another thing that's important due to the StructureMap magic is that you actually name the parameter for the view "view"... I wish there was a better way, but I couldn't make it happen without doing that... Patch anyone? ;)

So, there's really the simplest use of MvpFramework, but there's a heck of a lot more...

Let's move on.

Ok, so now, lets handle when a user selects a person from the dropdown. Here's the test.

   39 [Test]

   40         public void when_a_person_is_selected_the_view_is_bound_to_it()

   41         {

   42             //Add a person to the repo

   43             var person = new Person {Name = "Test Person"};

   44             allPeople.Add(person);

   45             view.Stub(v => v.SelectedPersonId).Return(person.Id);

   46             repository.Stub(r => r.GetById(person.Id)).Return(person);

   47 

   48             Act();

   49             view.Raise(v => v.PersonSelected += null,null,EventArgs.Empty);

   50 

   51             view.AssertWasCalled(v => v.Name = person.Name);

   52         }

 

Ok, so this test  makes a person, and then makes the repo return it when GetById is called.  It then makes the View return that persons id for SelectedPersonId and raises the PersonSelected Event.

The assertion is that the name is set.

Let's make it pass.

 

    1 using System;

    2 using Demo.Business;

    3 using MvpFramework;

    4 

    5 namespace Demo.Presentation

    6 {

    7     public class EditPersonPresenter:Presenter<IEditPersonView>

    8     {

    9         private readonly IPersonRepository repository;

   10 

   11         public EditPersonPresenter(IEditPersonView view, IPersonRepository repository) : base(view)

   12         {

   13             this.repository = repository;

   14             HydrateAllPeople();

   15             RegisterEvents();

   16         }

   17         private void HydrateAllPeople()

   18         {

   19             View.AllPeople = repository.GetAll();

   20         }

   21         private void RegisterEvents()

   22         {

   23             View.PersonSelected += HydrateView;

   24         }

   25 

   26         private void HydrateView(object sender, EventArgs e)

   27         {

   28             var selectedPerson = repository.GetById(View.SelectedPersonId);

   29             View.Name = selectedPerson.Name;

   30         }

   31 

   32     }

   33 }

 

Nothing really to point out about the repo now. Just making stuff work. Moving on...

 

Let's handle the Save.

 

Unit test.

   53 [Test]

   54         public void when_the_user_clicks_save_the_person_is_persisted()

   55         {

   56             //Add a person to the repo

   57             var person = new Person { Name = "Test Person" };

   58             allPeople.Add(person);

   59             repository.Stub(r => r.GetById(person.Id)).Return(person);

   60 

   61             view.Stub(v => v.SelectedPersonId).Return(person.Id);

   62             var theName = "Modified";

   63             view.Stub(v => v.Name).Return(theName);

   64 

   65             Act();

   66             view.Raise(v => v.PersonSaved += null,null,EventArgs.Empty);

   67 

   68             var personSentToRepo = (Person)repository.GetArgumentsForCallsMadeOn(r => r.Save(person))[0][0];

   69             Assert.AreEqual(person.Id,personSentToRepo.Id);

   70             Assert.AreEqual(theName,personSentToRepo.Name);

   71 

   72         }

 
Doesn't Rhino rock?! Anyway, this test just makes sure that the person is indeed persisted with the modifications sent.
 
Lets make it pass.
 

   21  private void RegisterEvents()

   22         {

   23             View.PersonSelected += HydrateView;

   24             View.PersonSaved += PersistPerson;

   25         }

   26 

   27         private void PersistPerson(object sender, EventArgs e)

   28         {

   29             var selectedPerson = repository.GetById(View.SelectedPersonId);

   30             selectedPerson.Name = View.Name;

   31             repository.Save(selectedPerson);

   32         }

 
Simple (and kinda boring actually). Lets keep going.
 
Now, for some crazy reason, we decided that you can't save 2 people with the exact same name. So we write this unit test. This is where the Framework really starts to pay off.

   73 [Test]

   74         public void you_can_not_save_2_people_with_the_same_name()

   75         {

   76             var person1 = new Person {Name = "Some Dude"};

   77             allPeople.Add(person1);

   78             var person2 = new Person {Name = "Some Other Dude"};

   79             allPeople.Add(person2);

   80             repository.Stub(r => r.GetAll()).Return(allPeople);

   81             repository.Stub(r => r.GetById(person1.Id)).Return(person1);

   82 

   83             view.Stub(v => v.SelectedPersonId).Return(person1.Id);

   84             // now set the views name to the name of the other person

   85             view.Stub(v => v.Name).Return(person2.Name);

   86 

   87             Act();

   88             view.Raise(v => v.PersonSelected += null,null,EventArgs.Empty);

   89 

   90             Assert.IsFalse(presenter.Valid);

   91 

   92         }

 

So, lets brake this test down. First we set up the repo to have to people. We make the view select the first person, but set the name to the seconds person name. Then we raise the save event. We expect that some property name "Valid" is set to false.

 

Ok, first things first, to use Validation, we've gotta make the view inherit IValidatedView (it's just a marker and it inherits IMvpView so it's free) like so.

 

    1 using System;

    2 using System.Collections.Generic;

    3 using Demo.Business;

    4 using MvpFramework;

    5 

    6 namespace Demo.Presentation

    7 {

    8     public interface IEditPersonView:IValidatedView

    9     {

   10         IEnumerable<Person> AllPeople { set; }

   11         Guid SelectedPersonId { get; }

   12         event EventHandler PersonSelected;

   13         event EventHandler PersonSaved;

   14         String Name { get; set; }

   15     }

   16 }

 

Ok, now lets go make some changes to the presenter. Presenter<T> gives you a IsValid property that does a few things for you. First and foremost it tells you if the view is valid. Since it's marked as internal, I had to wrap it in a property called "Valid".  Like so

   10  internal bool Valid

   11         {

   12             get { return IsValid; }

   13         }

 

Now, running the test it fails. So what to do?  It's really simple, all we need to do is create a Specification for the view.
Specification<T> has one method, bool IsSatisfiedBy(T obj). All you've gotta do is make it return true if there isn't a person with the same name, and false if it does. That's it... The MvpFramework finds all the specs for a view for you and uses them anytime you call IsValid. It actually sets validation labels on the page for you too, but we'll get to that later.

Enough talk though, lets make it go...

 

This is actually really easy... Here goes.

    1 using System.Linq;

    2 using Demo.Business;

    3 using MvpFramework.Specifications;

    4 

    5 namespace Demo.Presentation

    6 {

    7     public class CanNotHaveSameNameSpecification : Specification<IEditPersonView>

    8     {

    9         private readonly IPersonRepository repository;

   10 

   11         public CanNotHaveSameNameSpecification(IPersonRepository repository)

   12         {

   13             this.repository = repository;

   14         }

   15 

   16         public override bool IsSatisfiedBy(IEditPersonView obj)

   17         {

   18             return repository.GetAll().FirstOrDefault(p => p.Name == obj.Name) == null;

   19         }

   20     }

   21 }

 

That's it! The test now passes... Well, kinda, I actually had to make StructureMap work, like so...

   20  [SetUp]

   21         public void Arrange()

   22         {

   23             repository = MockRepository.GenerateMock<IPersonRepository>();

   24             allPeople = new List<Person>();

   25             view = MockRepository.GenerateMock<IEditPersonView>();

   26             SetupStructureMap();

   27 

   28         }

   29 

   30         private void SetupStructureMap()

   31         {

   32             ObjectFactory.Initialize(

   33                 registry => registry.Scan(x =>

   34                                               {

   35                                                   x.AssemblyContainingType<IPersonRepository>();

   36                                                   registry.ForRequestedType<IPersonRepository>().TheDefault.Is.Object(

   37                                                       repository);

   38 

   39                                               }));

   40 

   41         }


If you don't understand that, then PLEASE PLEASE PLEASE google "Structure Map", It makes IOC work like it should... All it does is make the MVP framework (or anything else using ServiceLocation) return that mocked repository whenever a a IPersonRepository is requested. For the "real" site, we'd have it return a real one.
Next post, I'll actually wire up the view, but I'm done for tonight...
Happy Coding!

No comments:

Post a Comment