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...

No comments:

Post a Comment