If you're reading this post and wondering about 1-5, you might give the previous posts a perusal first. This post (hopefully) builds on the previous posts in this series.
Alright, I've purposely hid the View to Presenter communication in my previous posts on Supervising Controller and Passive View because I thought that subject was worthy of its own post. As I see it, there are 2 1/2 basic ways to communicate screen events back to the Presenter.
Just taking a straw poll of the blog posts out there about this topic, the majority of the world seems to favor events. Just to be contrary, I very strongly prefer direct communication and it's time for that approach to get some love .
Making the View to Presenter communication work through events has the indisputable advantage of maximizing loose coupling. The View doesn't even need to know that there's anything out there listening. It's just shouting in the dark hoping someone will answer with help. There's no need to pass around instances of the Presenter, so wiring MVP triads together is a little bit simpler. Let's take a look. The first step is to define the proper events on the View interface. Here's an example from the Shipping Screen first described in the Supervising Controller post.
public delegate void VoidHandler();
public interface IShippingScreen
{
event VoidHandler ShippingOptionChanged;
string ShippingOption { get;}
}
The ShippingOptionChanged event is the interesting thing here. When this event fires, the Presenter needs to recalculate the cost of the selected shipment and decide whether or not other shipment options for purchasing insurance or requiring a signature are still valid. The actual View implementation is to simply relay the screen event with no interpretation:
public ShippingScreen()
{
InitializeComponent();
shippingOptionField.SelectedIndexChanged += new EventHandler(shippingOptionField_SelectedIndexChanged);
}
void shippingOptionField_SelectedIndexChanged(object sender, EventArgs e)
{
if (ShippingOptionChanged != null)
{
ShippingOptionChanged();
}
}
Now, on the Presenter side we have to register the event handler. Since the event has a no argument signature, all we have to do is basically tell the screen which method on the Presenter to call. No ugly anonymous delegates or ugly new SomethingDelegate(blah, blah) syntax.
public class ShippingPresenter
{
private readonly IShippingScreen _screen;
public ShippingPresenter(IShippingScreen screen)
{
_screen = screen;
// Attach to the event in the constructor
_screen.ShippingOptionChanged += this.ShippingOptionChanged;
}
public void ShippingOptionChanged()
{
// fetch the state the Presenter needs inside this operation
// from the Vjiew
string option = _screen.ShippingOption;
// do whatever it is that you do to respond to this
// event
}
}
And yes, I would definitely make the event handler methods public. I think you should do at least one unit test to verify that you're wired to the correct event handler on the View interface, but otherwise I think you'll find it much more convenient to test the ShippingOptionChanged() method by calling straight into the ShippingOptionChanged() method. I know the semantics of the unit test are simpler when you do it that way.
Assuming that you are going to unit test by simulating the event being raised, your unit test for the functionality above will look something like this (assuming that you use RhinoMocks anyway):
[Test]
public void HandleTheShippingOptionChangedEvent()
{
MockRepository mocks = new MockRepository();
IShippingScreen screen = mocks.CreateMock<IShippingScreen>();
// Keep a reference to the IEventRaiser for ShippingOptionChanged
screen.ShippingOptionChanged += null;
IEventRaiser eventRaiser = LastCall.IgnoreArguments().GetEventRaiser();
// other expectations here for the Presenter interacting with both
// the backend and the View
mocks.ReplayAll();
// Raise the event
ShippingPresenter presenter = new ShippingPresenter(screen);
eventRaiser.Raise();
// Check the interactions
mocks.VerifyAll();
}
The thing to note is how we grab onto the event with RhinoMocks to simulate the event. In a normal test harness I usually build the presenter under test and all of the mock objects in the SetUp() method. That requires a little bit of knowledge about RhinoMocks to keep the tests running correctly. Check out the links at the bottom to Phil Haack and Jean-Paul for more information on how to do this.
Why do I prefer direct communication? Two reasons.
So the obvious downside is that now the View has to know about the Presenter, which spawns a new design question - who begat's who? The majority of the time I say that it's a Presenter-centric world. I create the Presenter first with the View coming in as a constructor argument.
public class ShippingScreenPresenter : IShippingScreenObserver
{
private readonly IShippingScreen _screen;
// The view is injected in through the constructor
public ShippingScreenPresenter(IShippingScreen screen)
{
_screen = screen;
}
// I suppose you could do this operation in the constructor instead, but
// it always feels so wrong to do much more than set fields in a constructor
public void Start()
{
// bootstrap the screen
_screen.AttachPresenter(this);
}
public void ShippingOptionChanged()
{
// do what you do so well
}
}
I usually have some sort of explicit call to a Start() method on my Presenter's to bootstrap the screen. In this case, the very first operation in Start() is telling the View about the Presenter. I know what you're thinking, this is going to tightly couple my View to the Presenter. What if I want to use a different Presenter with the same View (it's not that uncommon)? What if I want to test the View itself? The easy answer is to treat the Presenter as an Observer of the View. In my DevTeach talk I brought up the tight coupling issue and promptly did an "Extract Interface" refactoring with ReSharper to show how easy it was to decouple the View from the concrete Presenter. No one was impressed. The View only sees:
public interface IShippingScreenObserver
{
void ShippingOptionChanged();
// all other callback methods for screen events
}
The event handlers in the View itself get reduced to an immediate call to the Presenter (in bold).
public partial class ShippingScreen : Form, IShippingScreen
{
private IShippingScreenObserver _presenter;
public ShippingScreen()
{
InitializeComponent();
shippingOptionField.SelectedIndexChanged += new EventHandler(shippingOptionField_SelectedIndexChanged);
}
void shippingOptionField_SelectedIndexChanged(object sender, EventArgs e)
{
_presenter.ShippingOptionChanged();
}
public void AttachPresenter(IShippingScreenObserver presenter)
{
_presenter = presenter;
}
}
In the section on MicroController, I'll show how my project is creating the screen behavior for calls to the Presenter.
The single best advice I think I could give is to make the communication as simple as possible. As much as possible, make the callbacks and event handlers to the Presenter take in zero arguments. Make the Presenter go back to the View and/or the Model to get current state. On one had it makes the View simpler by eliminating code from the marginally testable View, on the other hand it decouples the View from needing to "Know" what the Presenter needs for its behavior determinations. The exception cases would be for things like double clicking a single line inside of a grid to launch a new screen. In that case I think the simplest thing to do is to send over just enough information in the handler for the Presenter to know which data member was the subject of the event.
The other advice I'll give is to make the method names for event handling on the Presenter be as descriptive as possible. Ideally, you should be able to almost entirely comprehend the behavior of a screen from looking at the Presenter alone.
I'll talk about integration testing in later posts in this series.
I'm fully aware that I'm almost a deviant by preferring direct calls from the View to the Presenter. Here's the other side:
In the end, just talk it over with the rest of your team and decide if you want to be a cool kid like me or a slave to Microsoft's obsession with ugly event handling syntax, not that I want to prejudice you in any way;)