Context Sensitive History. Part 1 of 2

A Desktop and Silverlight user action management system, with undo, redo, and repeat. Allowing actions to be monitored, and grouped according to a context (such as a UI control), executed sequentially or in parallel, and even to be rolled back on failure.

Introduction

At some point in the development of most applications, the capability to allow a user to undo and action becomes a requirement.
Neither WPF nor Silverlight 4, provide any real infrastructure for accomplishing this,
but instead require developers to build this functionality into each component.
There are some real advantages to providing an application wide mechanism for managing, what the author calls tasks; which are units of work performed within the application.
Advantages that include allowing tasks to be monitored, and grouped according to a context (such as a UI control),
executed sequentially or in parallel, and even to be rolled back on failure.

The main features of the task management system provided in this article are:

Tasks can be undone, redone, and repeated.

Task execution may be cancelled.

Composite tasks allow sequential and parallel execution of tasks with automatic rollback on failure of an individual task.

Tasks can be associated with a context, such as a UserControl, so that the undo, redo, and repeat actions can be, for example, enabled according to UI focus.

Tasks can be global, having no context association.

The task system can be wired to ICommands.

Tasks can be chained, in that one task can use the Task Service to perform another.

Return to a point in history by specifying an undo point.

Coherency in the system is preserved by disallowing the execution of tasks outside of the Task Service.

Task Model compatible with both the Silverlight and Desktop CLRs

While the examples presented in this article are predominately based around Calcium,
the Task Service and tasks are completely separate from Calcium, and can in fact be consumed
by any Desktop CLR or Silverlight CLR application. There will be two articles in this series.
Part one, this article, details the Task Model and how to use it.
Part two will show how the Task Model has been integrated into Calcium, and will demonstrate a simple diagram designer module.

Background

Back in 2007 I wrote about a Command Management system
that I created for a game. The code I present in this article follows some of the same principles I explored in that article, but this time around I have expanded the scope and depth to a far greater extent.

The WPF (and now Silverlight 4) commanding infrastructure does not provide support for an undo redo mechanism directly via the CommandManager. Controls such as the TextBox provide internal support by means of RoutedCommands. So, a unified system can be difficult to obtains with the existing infrastructure. The system that I have
devised leverages existing Undo/Redo capabilities of such controls; adding handlers for unhandled Routed Commands; while providing new features such as
execution cancellation, and allowing for a task to be repeated.

The Task Model

As stated in the introduction, tasks are what the author defines as a unit of work performed within an application. Tasks themselves are instances of an ITask, which encapsulate the data and logic required to carry out an objective, such as moving an item in a UI.

Two Approaches

There are two approaches to task management. The first approach is to dictate
that every action causing a change in state is encapsulated in a task. This approach can be laborious, as a new task
instance must be created for each action. There is also a risk with this approach that external changes, performed outside of a task may change the state of a component, thereby rendering the undo task capability ineffective.

The second approach is to provide for state awareness. This means to take a snapshot of a component's state when a change
in state is occurring. The implementation of an undo action is to simply to restore the previous state of a UI component. This approach may be mandatory for interfaces where many actions are simply not undoable, for example a paint program where
use of filters, such as blur, are not able to be undone. This approach is compelling because it has a onetime cost of creating the capability to serialize and deserialize the component state. The downside is that storage efficiency needs to be considered, because each new task will save the entire component state, and not merely the change. One can imagine employing, for example, a diffgram
like approach to contend with the extra storage requirements, but that is outside the scope of this article.

Both approaches are attainable with the Task Model infrastructure presented here.
We will, however, be focussing on the first.

Capture Logic within a Task

By encapsulating the logic and state for a particular action within a task, we are able to
better manage tasks. Most notably we can queue tasks, undo the work performed by a task
(or set of tasks), and repeat that work if supported by the task. We can also provide a notification and cancellation system for tasks, so that subscribers to a task service are able to intervene when a particular task is being performed. We also have support for a context system. That is, we can provide the Task Service with an identifier,
which is used to partition a set of tasks, so that all actions around those tasks can be managed separately from other tasks.
For example, the context could be a view within the UI, so that when the view
loses focus, undoing of tasks for that view is disabled. We also allow for global tasks, which have no association with any particular UI context.

Task Service

All task execution activities are performed by the Task Service. In fact, only the Task Service is able execute a task. If it were otherwise, it could allow the state of the UI, for example, to be placed out of sync with the Task Service Undo or Repeat stacks, so that a subsequent undo or repeat of a task would produce unexpected results. This is important when the order of tasks is important, which is usually the case.

The following flow chart diagram shows how the Task Service goes about performing a task.

Figure: Flowchart of Task Execution

The Task Service itself contains several stacks, which are associated with execution contexts. An execution context may be a control, or a
control's view model for example. In the demonstration Diagram Designer module, we use a guid identifier for the
view model. By using an execution context we are able to associate a set of Tasks with a particular UI element, thereby allowing a different set of undo/repeat tasks to be displayed in the
Edit menu, depending on what control has focus.

The default implementation of the ITaskService is the TaskService. As an aside, I further extend this TaskService
in the Calcium implementation to provide for application wide notifications using event aggregation.

Figure: ITaskService and default implementation TaskService.

Tasks and Undoable Tasks

A task represents a unit of work performed by the ITaskService implementation. Some tasks are undoable, while others are not.
The ITask interface represents the base functionality for all tasks. The base Task class (TaskBase)
inherits from IInternalTask. IInternalTask provides capabilities directly associated with the TaskService
implementation, and allows the TaskService to repeat the execution of a task. As stated previously, user code is prevented
from executing tasks without the TaskService. This has been accomplished with explicit implementation of internal interfaces.
Other reasons for prohibiting the execution of a task directly; bypassing the ITaskService, include preventing subscribers
of the TaskService events from missing out on notifications and the opportunity to cancel tasks.
Also, the ITaskService offers a single point to provide auditing and logging etc.

Figure: Task class and interface hierarchy.

TaskBase and UndoableTaskBase are the starting point for creating new tasks which encapsulate the behavior of a task.
That is, by inheriting from either of these two classes we can encapsulate the task logic and state for an action within the subclass itself.
Alternatively, if this is too heavy, and you don't wish to go to the trouble of creating a new Task class, use the Task<T>
and UndoableTask<T> classes. Both of these two classes accept Actions,
which are performed when the task itself is performed etc.
See Using the Light Weight Task and UndoableTask Classes

Inheriting from TaskBase and UndoableTaskBase

Both TaskBase and UndoableTaskBase provide events that can be subscribed to in order to perform some custom activity.
TaskBase provides an Execute event, while UndoableTaskBase provides both an Execute event
(inherited from TaskBase) and an Undo event. I find it a good practice to favor events over virtual methods
because when a virtual method approach is used, it may cause confusion over whether the base implementation should be called,
likewise it can create a dependency on the base class implementation,
and in this case we also are able to prevent the circumvention of the TaskService, because a custom task can't
call the Execute method on the task.

An example of a custom UndoableTaskBase implementation is the MoveDesignerHostTask
from the Diagram Designer module. It is presented here in its entirety.

We can see that this task's purpose is to merely relocate a DesignerItemViewModel
by setting its Left and Top properties.

Using the Light Weight Task and UndoableTask Classes

While it is often prudent to place Task logic in a class, because this improves reusability, and helps
to decouple application logic (ala the Strategy Pattern), sometimes we may want to do things inline using a delegate.
For this purpose we can use the Task and UndoableTask, which both accept an Action parameter.
The following excerpt from the DiagramDesignerViewModel demonstrates the UndoableTask,
and how it is used to add new designer items.

Notice that in order to allow this task to be repeated, we must set its Repeatable property to true. Doing
so indicates to the TaskService that it should be placed in a repeatable Stack associated
with the DiagramDesignerViewModel (via its Id property).

Composite Tasks

Sometimes we may want to allow a set of tasks to be associated with a single user action. In these circumstances it may
be tempting to circumvent the task system by placing various logically distinct activities into a single task,
which risks violating the Single Responsibility Principle.
In order to prevent this I have created the notion of a Composite Task. Composite tasks allow you to place any number of tasks
within them, and then when the Composite Task is performed, undone, or repeated; all tasks will be performed etc.
Using a Composite Task also allows us to choose to perform the sub tasks sequentially or in parallel, and to also automatically undo tasks when an individual task raises an exception.

Just as we have Task and UndoableTask classes to represent a single activity,
we have CompositeTask and CompositeUndoableTask classes.

Executing a Composite Undoable Task

To perform the execution of a set of tasks, we instantiate a CompositeUndoableTask; passing it an IDictionary
of tasks and associated task arguments, as
shown in the following excerpt, demonstrating the alignment capability of the
diagram designer:

Chaining Tasks

Tasks themselves can leverage the ITaskService in order to execute sub-tasks.
This is useful when we have conditional execution of tasks depending on the result of some internal task activity.
The chaining capability explains a curious aspect in the implementation of the various PerformTask overloads
in the TaskService class.

We see that the task itself is performed after the task has been pushed onto the tasks Stack.
Thereby allowing any chained tasks to be undone or repeated in the correct order.

Unit Tests

The TaskServiceTest class contains the unit tests for the entire Task Model.
A behavior driven approach is invaluable when developing a component like this, as there are quite a few scenarios to test.
This class is worth looking at for understanding some of the behavior that hasn't been covered in the demo application.

Figure: Task model unit test results

Conclusion

In this article we have seen how tasks, which are application work units, are able to be performed, undone, and repeated by a task management system. We saw how composite tasks, comprising any number of sub-tasks, can be performed sequentially or in parallel,
and how tasks can be chained without compromising the undo/repeat capability. We then
touched on the practical application of the Task Model to a simple diagramming tool.
In the next part of the series, we will look at how the Task Model has been
integrated into a WPF application, and explore the example diagram designer in
greater detail. I hope you will join me then.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

Share

About the Author

Daniel Vaughan is a eight-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company dedicated to creating best-of-breed user experiences and leading-edge back-end solutions, using the Microsoft stack of technologies--in particular Xamarin, WPF, and the UWP.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Would it be possible for you to split out this functionality from the Calcium SDK, as some of us would not need all that Calcium has to offer (ie we do not use PRISM). And as this is pretty useful, I would like to use just this code outside of PRISM

Would that be achievable?

Sacha Barber

Microsoft Visual C# MVP 2008/2009

Codeproject MVP 2008/2009

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

Could you explain that a bit more to me, for example when I go to the source tab in Calcium (which is where the article is telling people to grab code from), I see that I must download entire Calcium SDK.

What I am saying is that I may not want to do that, I just want to Undo/Redo stuff, so how do I get just that code.

Sacha Barber

Microsoft Visual C# MVP 2008/2009

Codeproject MVP 2008/2009

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My base library is in the same TFS repository as Calcium, but it is separated. It is in the Core project directory (there is also a core Silverlight project). The easiest way is to download the source bundle from the Codeplex source page, and ignore the Calcium and Clog directories.

The bundle extract will look something like this:

|
- Source
|
- Calcium
- Clog
- Core

I am readily maintaining the entire Calcium repository, so that if readers download from there they are sure to get the latest code with bug fixes/enhancements etc.

I like this, I wrote a similiar thing some time back with an undoable CommandPattern stylee thing, though it did not have Parallel or Chained or composite for that matter. So this is much better.

5 all the way man. But just a word of warning, you will need to up your game if you are to stay ahead of Katka Vaughan (she has the same name as you an everything, spookie). She is churning out some really cool stuff man.

Sacha Barber

Microsoft Visual C# MVP 2008/2009

Codeproject MVP 2008/2009

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

Well Daniel, what can I say? Outstanding my friend, simply outstanding. I like this approach a lot - I will be using a slightly different approach with the batch undoables I'm working on, but this is a fantastic implementation.

I have to give you a 5, but I really wish I could award a 50.

"WPF has many lovers. It's a veritable porn star!" - Josh Smith

As Braveheart once said, "You can take our freedom but you'll never take our Hobnobs!" - Martin Hughes.