Friday, November 18, 2005

MVP and Dialog

I recently received a request to show how Model View Presenter and a modal dialog could work together. I'm going to use the existing rich* client example I've been working with lately. For demonstration purposes we will assume that the UserData class has a password property that contains a user's password. When a user updates their information they will be prompted to enter their password. Successful authentication updates their information and navigates the user to the read only view. Unsuccessful authentication does nothing, but clicking "Cancel" will close the dialog without saving info and without navigating away from the update view.

The modification is to add a password to the existing UserData instance.
public class EntryPoint
{
private static UserData createUser()
{
UserData user = new UserData();
user.Name = "John Doe";
user.JobTitle = "Rockstar";
user.PhoneNumber = "212-555-1212";
user.Password = "password";
return user;
}

...
}

The only change to the UpdateView is to remove the NavigationDispatcher.RaiseNavigate method call from the saveButton click handler. We'll now handle navigation in the presenter (perhaps we should have moved it when we introduced the NavigationDispatcher anyway).

The SaveUserDialog is a Windows Form that allows a user to enter a password and save, or cancel and do not save.

SaveUserDialog exposes it's PasswordTextBox as a public field. When a user clicks the "Save" button SaveUserDialog raises the RequestAuthorization event. Clicking "Cancel" just closes the form.
public class SaveUserDialog : Form
{
private Label label1;
public TextBox PasswordTextBox;
private Button saveButton;
private Button cancelButton;
public event UserAction RequestAuthorization;
private Container components = null;

public SaveUserDialog()
{
InitializeComponent();
}

... deleted generated code...

private void saveButton_Click(object sender, EventArgs e)
{
RequestAuthorization();
}

private void cancelButton_Click(object sender, System.EventArgs e)
{
this.Close();
}
}

The presenter for the SaveUserDialog has the responsibility of verifying the password matches and then setting the dialog.DialogResult = DialogResult.Yes.
public class SaveUserPresenter
{
private readonly SaveUserDialog dialog;
private readonly string password;

public SaveUserPresenter(SaveUserDialog dialog, string password)
{
this.dialog = dialog;
this.password = password;
dialog.RequestAuthorization+=new UserAction(AuthorizeSave);
}

private void AuthorizeSave()
{
if (dialog.PasswordTextBox.Text==password)
{
dialog.DialogResult=DialogResult.Yes;
dialog.Close();
}
}
}

I considered passing the entire UserData object to the SaveUserPresenter; however, it seemed easier to keep this presenter as thin as possible and keep the save functionality in the UpdatePresenter. If more functionality were required I wouldn't hesitate to pass in more objects and give more responsibility to the SaveUserPresenter.

All of these changes and additions come together in the UpdatePresenter. The UpdatePresenter is now responsible for creating and showing the SaveUserDialog. Additionally, the UserData instance should only be updated if the SaveUserDialog returns DialogResult.Yes. Lastly, the UpdatePresenter is also responsible for calling NavigationDispatcher.RaiseNavigate if DialogResult.Yes is set.
public class UpdatePresenter : IPresenter
{
private readonly UpdateView view;
private readonly UserData user;

public UpdatePresenter(UpdateView view, UserData user)
{
this.view = view;
this.user = user;
view.Save+=new UserAction(updateUser);
Push();
}

public IView View
{
get { return view; }
}

public void Push()
{
view.NameTextBox.Text = user.Name;
view.JobTextBox.Text = user.JobTitle;
view.PhoneTextBox.Text = user.PhoneNumber;
}

private void updateUser()
{
SaveUserDialog dialog = new SaveUserDialog();
new SaveUserPresenter(dialog, user.Password);
if (dialog.ShowDialog()==DialogResult.Yes)
{
user.Name = view.NameTextBox.Text;
user.JobTitle = view.JobTextBox.Text;
user.PhoneNumber = view.PhoneTextBox.Text;
NavigationDispatcher.RaiseNavigate(typeof(ReadView));
}
}
}

If my application contained several modal dialogs I would also look at possibly moving the creation into a factory similar to the PresenterFactory. Each presenter could take the DialogFactory as a constructor argument allowing for easy access.

For testing purposes I would create a IDialog interface that contained the DialogResult property. Each Dialog would obviously implement the IDialog interface and the dialog presenters would take an IDialog as a constructor argument instead of the concrete class. This would allow me to easily mock the dialog when testing the SaveUserPresenter.

* thanks to Matt Deiters for pointing out that, contrary to popular belief, smart and rich are different. (luckily, Matt is both)
Post a Comment