The ViewModel/MVVM pattern continues to gain popularity, with a blog post showing up every so often, and with tweets and retweets popping up even more often :-). At the same time, there are some interesting topics beyond the core pattern that continue to fuel experimentation. A big one amongst those is how should applications use dialogs when using the view model pattern.
The crux of the problem is the desire to keep the view model independent of UI concerns, and ensure it can be tested in a standalone manner, but that often comes to odds when you want the view model to launch a dialog, and/or do some work after the dialog is closed.
The most recent version of Silverlight.FX (v3.2) that I published earlier this week, addresses this scenario using a Task pattern. This blog post discusses that pattern, and opens it up for your thoughts.
The screenshot on the right represents my updated TaskList sample application (Click to run and create a task item, and double click on it to launch an edit dialog). All of the sample code is available along with Silverlight.FX for running on your end, and using the same pattern in your own applications of course.
Silverlight.FX provides a concrete notion of a view model representing a task with commit and cancel semantics in the form of a TaskViewModel base class. This serves as the base class for view models associated with dialogs. The view model associated with the main/parent window creates and initializes an instance of a TaskViewModel-derived class to represent the task as hand (when the view decides it needs to launch the dialog). The TaskViewModel raises completion notifications that can be used to do additional work in the parent view model. The main/parent window then creates a Form and assigns the resulting TaskViewModel as Form's view model, and finally shows the dialog. The user interacts with the dialog. The Form implements OK and Cancel commands that the OK/Cancel buttons invoke, and these commands are wired up to call into the TaskViewModel to commit it or cancel it.
I'll use some actual sample code to hopefully make this more concrete.
First lets look at the Form and its associated view model.
<fxui:Form ...>
...
<TextBox Text="{Binding Task.Name, Mode=TwoWay}" />
...
<Button Content="OK" fxui:Interaction.Command="OK" />
<Button Content="Cancel" fxui:Interaction.Command="Cancel" />
...
</fxui:Form>
public class EditTaskModel : TaskViewModel {
private Task _originalTask;
private Task _task;
public Task Task {
get { return _task; }
}
public void Initialize(Task t) {
_originalTask = t;
_task = t.Clone();
}
protected override void Commit() {
_originalTask.Copy(_task);
Complete(/* commit */ true);
}
}
The EditTaskModel is like any other view model, other than deriving from TaskViewModel which introduces the Commit/Cancel methods, that the Form calls into from its implementation of its OK and Cancel commands.
The EditTaskModel implements and encapsulates the logic of what it means to commit the form. When it is done committing the task it represents, the view model calls Complete on its base class. This raises a notification that signals the Form that it should close itself. This call to Complete can be made within the override of Commit as shown. Alternatively, it can also be called after the completion of some asynchronous work if the act of committing is itself asynchronous. This is simple, yet flexible.
Now that we have our Form and its view model ready, lets look at how it is used from the main/parent window and its associated view model. The main window contains the UI that launches the dialog (in this case upon double clicking) and the associated view model creates the TaskViewModel (in this case to represent editing of a task item).
<ItemsControl ItemsSource="{Binding Tasks}">
...
<!-- TextBlock bound to a particular Task item within the ItemsControl -->
<TextBlock Text="{Binding Name}">
<fxui:Interaction.Triggers>
<fxui:DoubleClickTrigger>
<fxaction:ShowForm FormType="TaskList.EditTaskForm"
FormModelExpression="$model.EditTask($dataContext)" />
</fxui:DoubleClickTrigger>
</fxui:Interaction.Triggers>
</TextBlock>
...
</ItemsControl>
public class TaskListWidgetModel : ViewModel {
public IEnumerable<Task> Tasks {
get { ... }
}
public EditTaskModel EditTask(Task task) {
EditTaskModel model = new EditTaskModel();
model.Initialize(task);
model.Completed += delegate(object sender, EventArgs e) {
if (model.IsCommitted) {
SaveTasks();
}
};
return model;
}
}
Basically my view model encapsulates what needs to be done to create the child view model (the EditTaskModel), initialize it, and what to do once it is committed. The view on the is responsible for implementing the user gesture, and creating and showing the Form.
Note that I used declarative triggers and actions to invoke the view model and create/show the dialog. If you don't like triggers, you could write the following imperative code instead in the code-behind:
private void HandleDoubleClick() {
// Get the view model associated with the main window
// and the task to be edited
TaskListWidgetModel mainModel = (TaskListWidgetModel)Model;
Task task = (Task)((Button)sender).DataContext;
// Invoke the view model to create the child view model to
// use as the Form's view model
EditTaskModel editModel = mainModel.EditTask(task);
EditForm editForm = new EditForm(editModel);
editForm.Show();
}
The declarative DoubleClickTrigger and ShowForm action are doing exactly the same thing, but from within XAML. Whether you use the declarative approach or not, is a your choice… this Task-based pattern I am describing works with either style. I personally prefer the declarative approach for specifying view logic.
To recap, let's look at the roles and responsibility's of each piece in this task pattern:
- The main window
- Implements gesture for launching dialog
- Invokes its view model to create an instance of a task
- Creates a Form using the task as its model, and shows it
- The main window's view model
- Creates and initializes the task as needed
- Subscribes to completion of the task to do any work it needs to do once the task has been committed by the user
- The Form
- Allows user to perform the task at hand
- Implements UI of the dialog including the OK/Cancel buttons
- Exposes OK/Cancel commands that invoke the Commit/Cancel methods on the associated view model
- The Form's view model
- Represents and encapsulates the logic behind the task
- Derives from TaskViewModel to support commit/cancel semantics
- Marks the task as completed by calling Complete(bool commit) when the task has been completed
Based on this you can see the view concerns stay confined to the view. The view has flexibility of how to launch the dialog, the kind of dialog UI it wants, etc. These are things that can be "designed" by a "designer". On the other hand, the core logic behind the dialog have been moved into the view model in the form of a task. The task itself cares little about it is presented to the user, as long as the view creates it, and calls commit or cancel at some point. As such, the pattern helps maintain the separation of concerns.
Its interesting to mention some of the motivators behind this approach:
- First, the I wanted the solution to be as similar to what people are used to doing today in code-behind. In other words, moving logic to the view model should be as close to refactoring out the non-UI parts from the view. This has been my general stance on the view model pattern.
- Second, just because I want to use a dialog in my UI, I shouldn't have to pick up relatively more complicated solutions like an event aggregator, and messaging across view models, which in turn introduce conceptual dependencies like IoC. This messaging based solution is often presented as the way to implement dialogs, but these concepts seem complex enough that they shouldn't be prerequisites to something as commonplace and simple as showing a dialog.
- Third and finally, I should be able to implement the bits that belong to the view both imperatively in code-behind, and declaratively through behaviors, triggers, and actions (as shown above).
What do you think? I would love to hear any and all feedback on thoughts, further ideas, and even alternative approaches, so as to further shape the implementation in Silverlight.FX.