Sunday, October 25, 2009

MVPFramework, part 3

Part 1 Part 2

So this post, we'll actually wire up specs to a view. First though, in the last paragraph of my last post I said that the presenter wouldn't save a person w/ the same name as another person in the system. That's not true, the Presenter's IsValid property returns false, but since the presenter doesn't check the property, it saves it anyway. Lets go fix that real quick.

So the original test was this

[Test]


        public void you_can_not_save_2_people_with_the_same_name()


        {


            


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


            allPeople.Add(person1);


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


            allPeople.Add(person2);


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


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


 


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


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


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


 


            Act();


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


            


            Assert.IsFalse(presenter.Valid);


 


        }


 


 




Note that it only checks the presenter.Valid property. That's not really a good test... Who cares what the property says, the fact that I wrapped a protected method shoulda sang of smell to me when I did it... Oh well, Lets change it.




   1:  [Test]


   2:          public void you_can_not_save_2_people_with_the_same_name()


   3:          {


   4:              


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


   6:              allPeople.Add(person1);


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


   8:              allPeople.Add(person2);


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


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


  11:   


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


  13:              // now set the views name to the name of the other person


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


  15:   


  16:              Act();


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


  18:              


  19:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


  20:              


  21:   


  22:          }




 



Not THAT fails... So lets go fix it. Really really easy....




   1:  private void PersistPerson(object sender, EventArgs e)




   2:          {




   3:              var selectedPerson = repository.GetById(View.SelectedPersonId);




   4:              selectedPerson.Name = View.Name;




   5:              if(!IsValid) return;




   6:              repository.Save(selectedPerson);




   7:              HydrateAllPeople();




   8:          }




 




Line 5 is all we did. Check that IsValid property. If it returns false, we don't save. Yep... the test passes.




 




Ok, so now that we've fixed that bug, lets go actually communicate to the user why when they clicked save, the person didn't get saved.  




 




All we need to do is drop a ValidationMessageControl on the page. First the Register directive




 




<%@ Register Assembly="MvpFramework" Namespace="MvpFramework" TagPrefix="mvp" %>





and then the control.




 




   1:          <div>


   2:              <asp:Label runat="server" AssociatedControlID="NameControl" Text="Name" />


   3:              <asp:TextBox runat="server" ID="NameControl" />


   4:              <span style="color:#F00; font-weight:bold">


   5:              <mvp:ValidationMessage runat="server" ID="NameMustBeUnique" Text="2 people can't have the same name!" />


   6:              </span>


   7:          </div>





Now we've gotta go set the SpecificationType property, for now, you've gotta do this in the codebehind. Hopefully I'll make that unnecessary soon. But even like this it's not bad.




 




Here's the change.




 




   1:  protected override void OnInit(EventArgs e)


   2:          {


   3:              base.OnInit(e);


   4:              AllPeopleControl.SelectedIndexChanged += (o, ev) => PersonSelected(o, ev);


   5:              AddNewButton.Click += (o, ev) => PersonAdded(o, ev);


   6:              SaveExistingButton.Click += (o, ev) => PersonSaved(o, ev);


   7:              NameMustBeUnique.SpecificationType = typeof (CanNotHaveSameNameSpecification);


   8:          }




We're interested in line 7. Just set the type and whenever the IsValid is called on the presenter, if the spec fails, the Text is shown.



Lets give it a test run. After adding a few "people"...



blog




 


I'll go select one of them.


blog1 


 


And save him as "Test" since I know that name is already taken.


 


blog2 


 


And vola... It just works....


 


Let's go make a new requirement. Say, the Name has to have at least 5 characters in it. 


 



   1:  [Test]


   2:          public void a_person_can_not_be_added_if_the_name_is_less_than_5_characters()


   3:          {


   4:              view.Stub(v => v.Name).Return("fail");


   5:              Act();


   6:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


   7:   


   8:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


   9:          }




 


 


Of course it fails...


 


Let's go write up the spec.


 



   1:  using MvpFramework.Specifications;


   2:   


   3:  namespace Demo.Presentation


   4:  {


   5:      public class NameMustBeLongerThan5CharactersSpecification:Specification<IEditPersonView>


   6:      {


   7:          public override bool IsSatisfiedBy(IEditPersonView obj)


   8:          {


   9:              return obj.Name.Length > 5;


  10:          }


  11:      }


  12:  }


 


And run the test...


But it fails?!


 


blog3 


 


So, I knew this would happen, but I figured I'd let it play out. Being the idiot that I am, I never checked IsValid for Adding... Only for saving..


In fact, I bet this will fail too.


 



   1:  [Test]


   2:          public void a_person_with_the_same_name_of_one_that_already_exists_can_not_be_added()


   3:          {


   4:              var testPerson = new Person {Name = "Test Person"};


   5:              allPeople.Add(testPerson);


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


   7:              view.Stub(v => v.Name).Return(testPerson.Name);


   8:              


   9:              Act();


  10:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


  11:   


  12:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


13: }








So yeah, we just add a person to the repo, and try to add another with the same name. Sure enough, it fails too (I'll save the pic, trust me, it fails). Let's go fix 'em both.




   1:  private void SaveANewPerson(object sender, EventArgs e)


   2:          {


   3:              var person = new Person {Name = View.Name};


   4:              if(!IsValid) return;


   5:              repository.Save(person);


   6:              HydrateAllPeople();


   7:          }





And vola! They pass. Actually, I had to go make a few other unit test changes, since I was persisting people with no name in some tests. Here's the whole fixture after tests.




   1:  using System;


   2:  using System.Collections.Generic;


   3:  using Demo.Business;


   4:  using Demo.Presentation;


   5:  using NUnit.Framework;


   6:  using Rhino.Mocks;


   7:  using StructureMap;


   8:   


   9:  namespace Demo.UnitTests.Presentation


  10:  {


  11:      [TestFixture]


  12:      public class When_editing_a_person


  13:      {


  14:          private IPersonRepository repository;


  15:          private List<Person> allPeople;


  16:          private IEditPersonView view;


  17:          private EditPersonPresenter presenter;


  18:   


  19:          [SetUp]


  20:          public void Arrange()


  21:          {


  22:              repository = MockRepository.GenerateMock<IPersonRepository>();


  23:              allPeople = new List<Person>();


  24:              view = MockRepository.GenerateMock<IEditPersonView>();


  25:              SetupStructureMap();


  26:              


  27:          }


  28:   


  29:          private void SetupStructureMap()


  30:          {


  31:              ObjectFactory.Initialize(


  32:                  registry => registry.Scan(x =>


  33:                                                {


  34:                                                    x.AssemblyContainingType<IPersonRepository>();


  35:                                                    registry.ForRequestedType<IPersonRepository>().TheDefault.Is.Object(


  36:                                                        repository);


  37:                                                        


  38:                                                }));


  39:   


  40:          }


  41:   


  42:          public void Act()


  43:          {


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


  45:              presenter = new EditPersonPresenter(view, repository);


  46:          }


  47:          [Test]


  48:          public void all_people_is_set_to_all_people_from_the_repo_on_initial_load()


  49:          {


  50:              view.Stub(v => v.IsInitialLoad).Return(true);


  51:              Act();


  52:              


  53:              view.AssertWasCalled(v => v.AllPeople = allPeople);


  54:          }


  55:          [Test]


  56:          public void when_a_person_is_selected_the_view_is_bound_to_it()


  57:          {


  58:              //Add a person to the repo


  59:              var person = new Person {Name = "Test Person"};


  60:              allPeople.Add(person);


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


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


  63:              Act();


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


  65:   


  66:              view.AssertWasCalled(v => v.Name = person.Name);


  67:          }


  68:          [Test]


  69:          public void when_the_user_clicks_save_the_person_is_persisted()


  70:          {


  71:              //Add a person to the repo


  72:              var person = new Person { Name = "Test Person" };


  73:              allPeople.Add(person);


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


  75:              


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


  77:              var theName = "Modified";


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


  79:              Act();


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


  81:   


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


  83:              Assert.AreEqual(person.Id,personSentToRepo.Id);


  84:              Assert.AreEqual(theName,personSentToRepo.Name);


  85:   


  86:          }


  87:          [Test]


  88:          public void you_can_not_save_2_people_with_the_same_name()


  89:          {


  90:              


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


  92:              allPeople.Add(person1);


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


  94:              allPeople.Add(person2);


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


  96:   


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


  98:              // now set the views name to the name of the other person


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


 100:   


 101:              Act();


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


 103:              


 104:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


 105:              


 106:   


 107:          }


 108:          [Test]


 109:          public void AllPeople_is_not_set_if_the_view_is_a_postback()


 110:          {


 111:              view.Stub(v => v.IsInitialLoad).Return(false);


 112:              Act();


 113:              view.AssertWasNotCalled(v => v.AllPeople = allPeople);


 114:          }


 115:          [Test]


 116:          public void when_PersonAdded_is_raised_a_new_person_is_added_to_the_repo()


 117:          {


 118:              var theNewName = "The New Person";


 119:              view.Stub(v => v.Name).Return(theNewName);


 120:              


 121:              Act();


 122:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


 123:   


 124:              var thePersonSaved = (Person)repository.GetArgumentsForCallsMadeOn(r => r.Save(null), o => o.IgnoreArguments())[0][0];


 125:              Assert.AreEqual(theNewName,thePersonSaved.Name);


 126:          }


 127:          [Test]


 128:          public void when_a_person_is_added_AllPersons_is_repopulated()


 129:          {


 130:              view.Stub(v => v.Name).Return("Some Guy");


 131:              Act();


 132:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


 133:              view.AssertWasCalled(v=> v.AllPeople = allPeople);


 134:          }


 135:          [Test]


 136:          public void when_a_person_is_saved_AllPeople_is_updated()


 137:          {


 138:              var someDude = new Person {Name = "Some Dude"};


 139:              allPeople.Add(someDude);


 140:              repository.Stub(r => r.GetById(someDude.Id)).Return(someDude);


 141:              view.Stub(v => v.SelectedPersonId).Return(someDude.Id);


 142:              view.Stub(v => v.Name).Return("Edited");


 143:              Act();


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


 145:              view.AssertWasCalled(v => v.AllPeople = allPeople);


 146:          }


 147:          [Test]


 148:          public void a_person_can_not_be_added_if_the_name_is_less_than_5_characters()


 149:          {


 150:              view.Stub(v => v.Name).Return("fail");


 151:              Act();


 152:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


 153:   


 154:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


 155:          }


 156:          [Test]


 157:          public void a_person_with_the_same_name_of_one_that_already_exists_can_not_be_added()


 158:          {


 159:              var testPerson = new Person {Name = "Test Person"};


 160:              allPeople.Add(testPerson);


 161:              view.Stub(v => v.Name).Return(testPerson.Name);


 162:              


 163:              Act();


 164:              view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);


 165:   


 166:              repository.AssertWasNotCalled(r => r.Save(null),options => options.IgnoreArguments());


 167:          }


 168:      }


 169:  }





The fact that I had to fix some tests brings up another point. All validation for presenters is delegated to a PresenterValidator object. When using the framework, if I have a lot of validation that takes place, I'll usually just inject a mocked validator and stub out IsValid so I don't have to mock up the view to pass all specs. I'll discuss this in depth in some future blog. Anyway, let's go hit the UI again.



 




   1:   <div>


   2:              <asp:Label runat="server" AssociatedControlID="NameControl" Text="Name" />


   3:              <asp:TextBox runat="server" ID="NameControl" />


   4:              <span style="color:#F00; font-weight:bold">


   5:              <mvp:ValidationMessage runat="server" ID="NameMustBeUnique" Text="2 people can't have the same name!" />


   6:              <mvp:ValidationMessage runat="server" ID="NameMustBeMoreThan5Characters" Text="The name must be at least 6 characters" />


   7:              </span>


   8:          </div>




And also, set the specificationtype for the new validation message.




   1:   protected override void OnInit(EventArgs e)


   2:          {


   3:              base.OnInit(e);


   4:              AllPeopleControl.SelectedIndexChanged += (o, ev) => PersonSelected(o, ev);


   5:              AddNewButton.Click += (o, ev) => PersonAdded(o, ev);


   6:              SaveExistingButton.Click += (o, ev) => PersonSaved(o, ev);


   7:              NameMustBeUnique.SpecificationType = typeof (CanNotHaveSameNameSpecification);


   8:              NameMustBeMoreThan5Characters.SpecificationType = typeof (NameMustBeLongerThan5CharactersSpecification);


   9:          }




Ok, let's test drive it... Save some dude named "s". 



blog5 



And it works...

So there you have it folks.. How to use validation with the MvpFramework. Hope it helps everyone. 
Next up is Navigation....



Happy coding..

Saturday, October 24, 2009

MvpFramework Part 2

Part 1

In my last post, I briefly introduced the MvpFramework.  It got late, and I got tired, so I didn't finish. So let's pick up where I left off.  We actually created a presenter that passed all the basic unit tests, but I can already think of one bug, due to the beauty (NOT) of webforms and how post back works... It'll actually rebind the DropDown list every postback. Can't believe I missed it. No harm done though, lets go write a unit test (failing) to prove the bug exists, and then fix it.

  111 [Test]

  112         public void AllPeople_is_not_set_if_the_view_is_a_postback()

  113         {

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

  115             view.Stub(v => v.IsInitialLoad).Return(false);

  116             Act();

  117             view.AssertWasNotCalled(v => v.AllPeople = allPeople);

  118         }

 

Note the IsInitialLoad property. When you inherit MvpPage or MvpUserControlBase, you'll get that for free, but we'll get to that eventually. For now, we just made the view return false and made sure that AllPeople was not set. It was, so the test failed. Lets go fix it.

Pretty simple actually, just wrap HydratePeople in a IsInitialLoad guard clause.

 

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

   16         {

   17             this.repository = repository;

   18             if(View.IsInitialLoad)

   19             {

   20                 HydrateAllPeople();

   21             }

   22             RegisterEvents();

   23         }

 

Test passes, but I'm pretty suspicious that we flunked that original test we wrote about populating it... Running the whole suite and...

image

Yep, that first one fails. Since the test isn't valid anymore, lets go fix it and make the view return true for IsInitialLoad, and while we're at it, lets rename it.

 

Here are the changes.

   47 [Test]

   48         public void all_people_is_set_to_all_people_from_the_repo_on_initial_load()

   49         {

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

   51             view.Stub(v => v.IsInitialLoad).Return(true);

   52             Act();

   53 

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

   55         }

 

That passes.

 

Ok, so one other thing. We need to let a user add a new person. There are 2 approaches here. We could add a new event for the add, or we could just look for a magic Id (Guid.Empty) and let the presenter sniff that out. I think the event is a little cleaner, so lets go with that.

 

  119 [Test]

  120         public void when_PersonAdded_is_raised_a_new_person_is_added_to_the_repo()

  121         {

  122             var theNewName = "The New Person";

  123             view.Stub(v => v.Name).Return(theNewName);

  124 

  125             Act();

  126             view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);

  127 

  128             var thePersonSaved = (Person)repository.GetArgumentsForCallsMadeOn(r => r.Save(null), o => o.IgnoreArguments())[0][0];

  129             Assert.AreEqual(theNewName,thePersonSaved.Name);

  130         }

 

If you don't get that repository.GetArgumentsForCallsMadeOn line, that's fine. It's not horribly intuitive, but man it rocks... As the name implies it returns the the person object that was sent to the repository.Save(Person) call. Right now we get an argument out of range exception, since the call never occurred. Lets go fix it.

 

Here's the code to make it pass.

 

   28   private void RegisterEvents()

   29         {

   30             View.PersonSelected += HydrateView;

   31             View.PersonSaved += PersistPerson;

   32             View.PersonAdded += SaveANewPerson;

   33         }

 

   45  private void SaveANewPerson(object sender, EventArgs e)

   46         {

   47             var person = new Person {Name = View.Name};

   48             repository.Save(person);

   49         }

 

And there we go... well... actually. We should go refresh AllPeople again, right? So the name shows up in the drop down. Here are the changes to make that happen.

 

Test

  131 [Test]

  132         public void when_a_person_is_added_AllPersons_is_repopulated()

  133         {

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

  135             Act();

  136             view.Raise(v => v.PersonAdded += null,null,EventArgs.Empty);

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

  138         }

And the presenter changes.

   45  private void SaveANewPerson(object sender, EventArgs e)

   46         {

   47             var person = new Person {Name = View.Name};

   48             repository.Save(person);

   49             HydrateAllPeople();

   50         }

 
Ok.. That's enough. Let's go wire up a page.
 
Create a web application, and I'll just modify the Default page to be the EditPerson view.
 
A little StructureMap wireup...
 

   10  protected void Application_Start(object sender, EventArgs e)

   11         {

   12             ObjectFactory.Initialize(

   13                 registry => registry.Scan(x =>

   14                 {

   15                     x.AssemblyContainingType<IPersonRepository>();

   16                     x.WithDefaultConventions();

   17 

   18                 }));

   19         }

 

Don't worry about all that, I made a silly little PersonRepository that saves stuff to HttpCache. If you wanna see it, I've got the code right in trunk under Demo...

 
Here's the markup (that we care about).
 

<form id="form1" runat="server">
       <div>
           <asp:Label runat="server" AssociatedControlID="AllPeopleControl" Text="Select A Person" />
           <asp:DropDownList runat="server" ID="AllPeopleControl" DataTextField="Name" DataValueField="Id" AutoPostBack="true"/>
       </div>
       <div>
           <asp:Label runat="server" AssociatedControlID="NameControl" Text="Name" />
           <asp:TextBox runat="server" ID="NameControl" />
       </div>
       <div>
           <asp:Button runat="server" ID="SaveExistingButton" Text="Save" /> or
           <asp:Button runat="server" ID="AddNewButton" Text="Add As New" />
       </div>   
   </form>

 

And the code behind.

    1 using System;

    2 using System.Collections.Generic;

    3 using Demo.Business;

    4 using Demo.Presentation;

    5 using MvpFramework;

    6 

    7 namespace DemoWeb

    8 {

    9     public partial class _Default : MvpPage,IEditPersonView

   10     {

   11         protected override void OnInit(EventArgs e)

   12         {

   13             base.OnInit(e);

   14             AllPeopleControl.SelectedIndexChanged += (o, ev) => PersonSelected(o, ev);

   15             AddNewButton.Click += (o, ev) => PersonAdded(o, ev);

   16             SaveExistingButton.Click += (o, ev) => PersonSaved(o, ev);

   17         }

   18         public IEnumerable<Person> AllPeople

   19         {

   20             set

   21             {

   22                 AllPeopleControl.DataSource = value;

   23                 AllPeopleControl.DataBind();

   24             }

   25         }

   26 

   27         public Guid SelectedPersonId

   28         {

   29             get

   30             {

   31                 return AllPeopleControl.Items.Count > 0

   32                            ?

   33                                new Guid(AllPeopleControl.SelectedValue)

   34                            : Guid.Empty;

   35             }

   36         }

   37 

   38         public event EventHandler PersonSelected = delegate { };

   39         public event EventHandler PersonSaved = delegate { };

   40         public event EventHandler PersonAdded = delegate { };

   41         public string Name

   42         {

   43             get { return NameControl.Text; }

   44             set { NameControl.Text = value; }

   45         }

   46     }

   47 }

 

Simply inherit MvpPage, and Implement the view, and everything just automagically works! In playing with it, I found a small bug (we're not rebinding AllPeople after save). But the point of this post wasn't to make a perfect app, it was to show how MvpFramework works.... and it does.

 

Ok, I'll fix it real quick :)

 

Test...

 

  139  [Test]

  140         public void when_a_person_is_saved_AllPeople_is_updated()

  141         {

  142             var someDude = new Person {Name = "Some Dude"};

  143             allPeople.Add(someDude);

  144             repository.Stub(r => r.GetById(someDude.Id)).Return(someDude);

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

  146             view.Stub(v => v.SelectedPersonId).Return(someDude.Id);

  147             view.Stub(v => v.Name).Return("Edited");

  148             Act();

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

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

  151         }

 

Code...

 

   39  private void PersistPerson(object sender, EventArgs e)

   40         {

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

   42             selectedPerson.Name = View.Name;

   43             repository.Save(selectedPerson);

   44             HydrateAllPeople();

   45         }

 

There we go!

 

 

So, no, I didn't forget about the Validation stuff.... I just saved that for the next post ;) For now, if you try and save someone with the same name as someone that already exists, it just doesn't save... We'll make it show nice pretty messages soon enough...

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!