Next in my series (table of contents) on agile UI development in .NET is the presenter. The presenter is responsible to drive the UI workflow. This means that the presenter is the control center to react to:
- events from the model. For example that data has changed.
- events from embedded presenters
- calls from parent presenter
- calls from UI commands
That’s a bit abstract, I know. Therefore, let’s have a look at the dashboard presenter in ProCollEE (see previous posts to learn more about ProCollEE).
The dashboard shows all messages of all the channels the user is registered. The presenter initially loads all messages relevant for the current user and pushes them to the dashboard view-model. Once running, the presenter listens to events that new messages are available and will again push them over to the dashboard view-model. Once the data is in the view-model the view gets automatically updated by data-binding:
Let’s have a closer look at the code:
/// <summary> /// The presenter for the dashboard. /// </summary> public class DashboardPresenter : IDashboardPresenter { /// <summary> /// The ViewModel of the dashboard. /// </summary> private readonly IDashboardViewModel viewModel; private readonly IModelCommandProcessor commandProcessor; /// <summary> /// The resolution root to create instances. /// </summary> private readonly IResolutionRoot resolutionRoot; /// <summary> /// Initializes a new instance of the <see cref="DashboardPresenter"/> class. /// </summary> /// <param name="viewModel">The view model.</param> /// <param name="eventBroker">The event broker on which is listened for new messages.</param> /// <param name="commandProcessor">The command processor.</param> /// <param name="resolutionRoot">The resolution root to create instances.</param> public DashboardPresenter( IDashboardViewModel viewModel, IEventBroker eventBroker, IModelCommandProcessor commandProcessor, IResolutionRoot resolutionRoot) { Contract.Requires<ArgumentNullException>(viewModel != null, "viewModel"); Contract.Requires<ArgumentNullException>(eventBroker != null, "eventBroker"); Contract.Requires<ArgumentNullException>(resolutionRoot != null, "resolutionRoot"); Contract.Requires<ArgumentNullException>(commandProcessor != null, "commandProcessor"); this.viewModel = viewModel; eventBroker.Register(this); this.commandProcessor = commandProcessor; this.resolutionRoot = resolutionRoot; } /// <summary> /// Shows this instance. /// </summary> public void Show() { this.FillDashboard(); } /// <summary> /// Handles the event broker event that a message was sent. /// </summary> /// <param name="sender">The sender.</param> /// <param name="message">The <see cref="bbv.Common.Events.EventArgs{IMessage}"/> instance containing the event data.</param> [EventSubscription(ClientEventTopics.SentMessage, typeof(UserInterface))] public void HandleSentMessage(object sender, EventArgs<IMessage> message) { Contract.Requires<ArgumentNullException>(sender != null, "sender"); Contract.Requires<ArgumentNullException>(message != null, "message"); this.PushMessageToViewModel(message.Value); } /// <summary> /// Fills the dashboard with messages. /// </summary> private void FillDashboard() { var messages = this.GetMessages(); this.PushMessagesToViewModel(messages); } /// <summary> /// Gets the messages to show in the dashboard. /// </summary> /// <returns></returns> private IEnumerable<IMessage> GetMessages() { var command = this.resolutionRoot.Get<IRequestDashboardMessagesModelCommand>(); this.commandProcessor.Execute(command); return command.Messages; } /// <summary> /// Pushes the messages to view model. /// </summary> /// <param name="messages">The messages.</param> private void PushMessagesToViewModel(IEnumerable<IMessage> messages) { foreach (IMessage message in messages) { this.PushMessageToViewModel(message); } } /// <summary> /// Pushes the message to view model. /// </summary> /// <param name="message">The message.</param> private void PushMessageToViewModel(IMessage message) { var model = this.resolutionRoot.Get<IDashboardMessageViewModel>(); model.Text = message.Text; model.Poster = message.Poster; model.SentTime = message.SentTime; foreach (string channel in message.Channels) { var channelViewModel = this.resolutionRoot.Get<IDashboardChannelViewModel>(); channelViewModel.ChannelName = channel; model.Channels.Add(channelViewModel); } this.viewModel.Messages.Add(model); } }
The presenter gets the following dependencies injected in the constructor:
- dashboard view-model: the presenter sets the messages on the view-model
- event broker: the presenter uses the event broker to listen for new messages
- model command processor: used to execute the command to get all messages.
- resolution root: the presenter uses the resolution root (Ninject) to create view-models for the individual messages
Show
The presenter provides a Show method that has to be called by the parent presenter when the dashboard is shown for the first time. The presenter loads all messages relevant for the current user and pushes them to the view-model.
It is a convention that all presenters in my UI design pattern have a Show method. This helps readability because there is a consistent pattern.
React To Events From Model
The presenter registers itself on the event broker to get events for the event topic SentMessage. When this event is fired anywhere in the application then the HandleSentMessage method is invoked.
The presenter subscribed to the event on the user interface thread: [EventSubscription(ClientEventTopics.SentMessage, typeof(UserInterface))]. This way, only the presenter has to be multi-threading aware.
Push Data To View-Model
When the presneter is shown or a new message arrives then the presenter creates a new view-model for a message:
IDashboardMessageViewModel model = this.resolutionRoot.Get<IDashboardMessageViewModel>(); model.Text = message.Text; model.Poster = message.Poster; model.SentTime = message.SentTime;
Then, for each channel the message was sent on, a channel view-model is created:
<pre>IDashboardChannelViewModel channelViewModel = this.resolutionRoot.Get<IDashboardChannelViewModel>(); channelViewModel.ChannelName = channel; model.Channels.Add(channelViewModel);
Finally, the message is pushed to the dashboard view-model.
Conclusions
The presenter is responsible to initialize the view-model at start-up and to update it as reaction to events from outside the scope of the current screen.
The presenter is also responsible that code on the view-model or view is always executed on the user interface thread. Thread synchronization has to be taken care of in a single place, otherwise multi-threading gets quickly too complex and error prone.