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