With modern frameworks available that were built with loose coupling and separation of concerns in mind, working in WinForms may seem like a testability wasteland. But there are times when the options of WPF with MVVM or MVC on the web are not available, and you’re stuck with WinForms. But fear not, the Mode-View-Presenter pattern is here to save the day!
If you are not familiar with the MVP pattern, the idea is that you have a presenter, which handles all the interactions between the user and the model. The presenter “Talks” to the UI through an abstraction. There are a couple ways of doing the MVP pattern. I’m going to explore the Passive View style, meaning all logic is taken out of the view and moved into the presenter. I’m not going to go into differences in the styles, but if you’re interested, Martin Fowler has in depth descriptions of the Passive View, and Supervising Controller patterns, which are both derivatives of MVP.
I’m going to do a walkthrough for creating a simple MVP application. We need a premise for our demo. I’ll go with something simple, an application that organizes notes. Here our note will be a bit of textual data with a title. Lets implement it.
The Model
We need the note and title, as well as created and last edited dates for tracking. We’ll want to validate our model; lets make sure we actually have a note, and I’m going to use files for storage, so we need to make sure we can use our title as a filename:
1 : public class NoteCard
2 : {
3 : [Required] 4 : [
RegularExpression(@"^[_0-9a-zA-Z\s\-_]+$",
ErrorMessage = "Title cannot contain special characters.")
] 5 : public string Title {
get; set;
}
6 : [Required] 7 : public string NoteText { get; set; }
8 : public DateTime ? CreateDate { get; set; }
9 : public DateTime ? EditDate { get; set; }
10:
}
I also created a repository that will handle the persistence:
1 : public interface INoteCardRepository
2 : {
3 : IEnumerable<string> NoteCards { get; }
4 : 5 : NoteCard GetNote(string title);
6 : bool NoteExists(string title);
7 : void Save(NoteCard note);
8 : void Delete(NoteCard note);
9:
}
So, we have a nice way to represent our notes, now we need a way to display it.
The View
Our main form has a file menu, a status bar for displaying the number of notes, a list of notes, and a display/edit area for a note.
Keep in mind, this form IS our view. We need to abstract away our view by creating an interface that defines the capabilities of our UI:
1 : public interface IMainView
2 : {
3 : event EventHandler NewNote;
4 : event EventHandler NoteSelected;
5 : event EventHandler SaveNote;
6 : event EventHandler DeleteNote;
7 : 8 : string SelectedNote { get; set; }
9 : string StatusText { get; set; }
10 : string Title { get; set; }
11 : 12 : void DisplayMessage(string title, string message);
13 : void ClearSelectedNote();
14 : void LoadNotes(IEnumerable<string> notes);
15 : void LoadNote(NoteCard note);
16 : NoteCard GetNote();
17:
}
IMainView is a contract that defines the UI, we need to make sure our form fulfills that contract. Since we’re using the passive view style, the implementation will be vary anemic. The only job of MainForm will be translating the messages into UI actions (databinding, manipulating controls, etc.) and notifying interested parties (the presenter) of user interaction:
public partial class MainForm : Form, IMainView
The Presenter
Our presenter is going to be doing all the real work. So it will need access to the view and the repository. Since our goal here is loose coupling, we’re going to inject those dependencies via the constructor:public MainPresenter(IMainView view, INoteCardRepository repository)
We now have enough to write our first test. So lets pause here and write a test before we implement the constructor. To mock the dependencies I’m using Moq.
1 : [TestMethod] 2 : public void LoadTest() 3 : {
4 : var notes = new[] { "Note1", "Note2" };
5 : var repo = new Mock<INoteCardRepository>();
6 : var view = new Mock<IMainView>();
7 : repo.Setup(m => m.NoteCards) 8 :.Returns(notes) 9 :.Verifiable();
10 : 11 : var presenter = new MainPresenter(view.Object, repo.Object);
12 : 13 : repo.Verify();
14 : view.Verify(m => m.LoadNotes(It.IsAny<IEnumerable<string>>()));
15 : view.VerifySet(m => m.StatusText = "2 Notes");
16:
}
We’re testing what happens when the application starts. Don’t worry if you’re unfamiliar with Moq, just understand that the test is making sure the the note list gets loaded and the status gets updated upon loading. But I highly recommend checking out the quickstart and familiarizing yourself with Moq.
Now that we have a presenter, lets fill in the constructor and make the test pass:
1 : _view = view;
2 : _repository = repository;
3 : 4 : view.NewNote += NewNote;
5 : view.NoteSelected += NoteSelected;
6 : view.SaveNote += SaveNote;
7 : view.DeleteNote += DeleteNote;
8 : 9 : var notes = _repository.NoteCards.ToList();
10 : 11 : _view.LoadNotes(notes);
12 : _view.StatusText = String.Format("{0} Notes", notes.Count);
We’re loading the notes, setting the status, so now our tests pass. And in the real implementation the application works as expected. You’ll also notice we’re wiring up events. This is so we can get notified when the user does something we care about. Lets take a look at one of the handlers:
1 : private void NewNote(object sender, EventArgs e) 2 : {
3 : _view.ClearSelectedNote();
4 : _view.LoadNote(new NoteCard());
5:
}
As you can see, all we’re doing here is clearing the selection, and loading a blank note. We could easily write a test for this too by using Moq’s RaiseEvent method.
Conclusion
Now we have an application that is loosely coupled, largely UI and persistence agnostic, and has well separated concerns. All the things that Engineers love and end users don’t care about. But users do care that an application works, so what we’ve really achieved is testability, which means (hopefully) software that works!
Check out the full sourcecode, which is available in my github NoteCards Repository. Now go write some code!
posted on Tuesday, February 21, 2012 4:01 AM