MVP in Action
Back in August-September 2007 the concept of the MVP (Model-View-Presenter) pattern had finally 'clicked' with me, since attending Ron Jacobs' session at TechEd 2007 and reading related literature online. Since then I have had quite a bit more exposure to it, half of it being with my own personal projects.
I have to say there is nothing better when learning about a pattern than to try it out and develop it from something that doesn't work well and is cumbersome to maintain, to something more logical, cohesive and easy to follow.
Here's a recap of MVP and where it has evolved from, based on my understanding and experience:
- The Monolithic Mess. Everything is found inside one class, which is usually the code-behind of an ASPX page, form or window. Often, in such demo-grade code, such a class will contain database access code inside an event handler, such as a button click event. Internal state is also stored in this self-contained class, even though it has little relation to the UI itself.
Problems start to show when common functionality is identified and inevitably refactored into a separate method, especially as the interface becomes more complex with each additional element. What happens is that multiple controls invoke the same logic from their function handlers.
Where there's input, there's also output. As the result of some action, the UI will usually be updated in some way to provide some sort of feedback on the state of the system. Some of this would lend itself naturally to refactoring into common methods, while other parts of the UI would need subtle differences catered for. Because this is all within one class, everything becomes one jumbled mess that is hard to maintain.
- Two-Tier (Model & View). Here, separation of concerns has been introduced, possibly because of additional forms/pages/windows/controls, which share a common state (Model) and call common helper functions. Another reason to use this pattern is to support a domain model, which would naturally reside separately from the UI (View) classes. This achieves a level of redundancy-avoidance through shared functionality, but there are still issues.
The various parts of the UI have access to the unified state and its relevant parts, however most of the control logic is still kept in the UI. Granted, the UI is somewhat simplified with all state management being handled by the Model, but there are still the inter-dependencies between an input event and its corresponding output responses. This becomes more complicated when dependencies cross to other parts of the UI. We can do better.
- Model-View-Presenter (MVP). This pulls the co-ordination of input to output out of the View and puts it into a separate intermediate layer - the Presenter. As a result, the application is structured around the three layers, with the following responsibilities:
- View: This is responsible for defining the user interface itself. Its main concerns are responding to events and translating them into commands, and displaying output - i.e. determining how to display the data that is given to it.
- Model: This is the authoritative source of all data / state in the application, and a set of operations that directly relate to this data. Whenever the internal representation of what the application is all about changes, these changes are registered in the Model. This state transcends all levels of the persistence stack - memory, disk storage, database storage and/or web services. It is especially the database access and web service access components of the model where an interface would be beneficial to abstract away implementation details, particularly when the database engine used could be one of many.
- Presenter: This is responsible for co-ordinating interactions between the view and the model (and may involve the use of a BackgroundWorker). An important distinction is that it is interaction-focussed - the View is always involved, whether as a caller or callee. If some logic deals only with details of the Model, it belongs in the Model.
How do these components fit together? Basically in a chain, as illustrated by the following example of one of my earlier projects (HexCell, a WPF-based implementation of Sudoku and a few variants):
Now that we've seen (at a high level) what MVP is and where it came from, let's tackle the questions of how it can be implemented.
Getting The Ball Rolling
Think about what happens when the application starts - a Model has to be initialised, the View created with its UI controls, and the Presenter linking the two together. By the time the UI is displayed, it should reflect the initial state of the Model, so there's one partial ordering needed: Model before (displaying) View. The application startup sits outside this 'triad', though the UI technology used will usually determine how this will be defined (e.g. in App.xaml[.cs], or Program.cs).
Here is the approach I took in HexCell (not the Officially Mandated way of doing it):
- Application creates the main window (View), but does not display it.
- Application creates the main Presenter, passing the main View as a reference to an interface.
- Main Presenter creates the Model in its constructor and holds a reference to it (since it is purely in-memory, so no interface abstraction necessary - unless you wish to test the Presenter in isolation with mock View and Model).
- Main Presenter binds events / sets delegates as necessary for interaction between the View and Model.
- Main Presener calls the View to display itself in its initial state.
If the Model has components that would be abstracted behind an interface (e.g. database access), the first three steps would be as follows:
- Application creates the main window (View), but does not display it.
- Application creates the relevant Model components.
- Application creates the main Presenter, passing the main View and relevant Model components as references to their corresponding interfaces.
Handling Multiple MVP 'Triads'
As applications become more complex, the interface becomes composed of several components that have different areas of responsibility, therefore one Presenter and View is not enough to handle the diverse functionality. Hence, some literature talks of constructing MVP "triads". In my view, a way to make this practical is to think of these triads as similar in concept to mini-applications hosted within one large host application. Keep in mind that these triads do not necessarily map 1:1 to each composite control within a window, in a way that a button doesn't need its own MVP triad.
Dealing with multiple triads makes things a little trickier, but here are some principles I followed when structuring the application:
- The Main View instantiates itself and constructs its child View components.
- The Main View provides interface implementations to get its component View references, e.g. GetSettingsView().
- The Main Presenter initialises only its own state in the Model. This in turn initialises the complete Model for the entire application.
- The central state object contains references to the components' state objects, much like declaring a 'tree of knowledge'.
- The Main Presenter creates its child presenters, passing in the corresponding component View references obtained by GetSettingsView(), etc.
- As before, the Main View does not get displayed until the Main Presenter tells it to do so.
- If a View needs access to a Presenter (to query the Model), it can only knows about its own direct Presenter.
- A component Presenter cannot interact directly with another View apart from its own.
- A component Presenter cannot interact directly with another component state in the Model apart from its own (or the main state).
- A component Presenter cannot interact directly with another Presenter except the Main Presenter (or an ancestor, if multiple hierarchical levels are defined).
- If a change in state requires multiple component Views to be updated, this should be done by allowing the respective Presenters to subscribe to the appropriate event in the Model.
- The Model never interacts with any Presenter - either a Presenter accesses the Model or an event is raised.
So far, these principles have guided the architecture of my after-hours project and seem to be working out. A good example of this in action is the changing of the language of the entire application from within the Settings View. When the setting is applied, the Settings Presenter calls the Main Presenter, which changes the current interface language in the Model (and loads the corresponding UI text). This change causes an event to be raised, which allows another component to refresh its View with the updated language data (since this UI text is applied programmatically, instead of using WPF data binding).