Imagine a typical user interface. There are several controls that run some repeated (during the life of the application) actions of varying complexity. In order for complex actions, such as accessing various media, accessing the network, or complex computation, to reduce the responsiveness of the interface, they must be asynchronous. Additionally, there may be controls that cancel an asynchronously started action. The action has a state property (inactive, started, completed successfully, completed with an error, canceled), which is somehow displayed to the user. The
MVVM design pattern adopted in WPF, Silverlight and WinPhone dictates that such an “action” be part of the presentation model, making it possible to invoke model services from the user interface without creating a hard link between them. Unfortunately, this "action" in the base class library is not implemented. The closest entities in the library, such as
System.Threading.Tasks.Task tasks,
System.Windows.Input.ICommand commands and
System.Delegate delegates, are not suitable: tasks are always one-time and cannot represent a repeatable action, delegates and commands do not support cancellation and do not contain state properties, and commands in general cannot be asynchronous. Next, I propose a solution in the form of a small class library, making it possible to easily use the described "actions" in your applications.
To begin with, we summarize our requirements.
User Requirements:
- No running action (as well as canceling an action) blocks the user interface.
- Long-running action can be undone.
- Actions can be mutually exclusive (for example: loading and saving). For such actions, only one of the group can be started.
- For a group of actions, a single user interface element can be used to cancel them, which will cancel everything that is running from the group.
- User interface elements for starting and canceling actions display the availability of the corresponding operations, preventing the restart of the running or canceling of the not running action.
Application Developer Requirements:
- Starting and canceling actions should be represented by ICommand commands for linking to user interface elements.
- The context action must allow binding to many user interface elements (for example, one context menu is called for many list items). For this action must take a context parameter that is specified in relation to the elements of the user interface.
- Actions should receive a Cancel request token parameter ( CancellationToken ) that allows them to respond to cancellation requests.
Common MVVM design pattern requirements:
- A view consists of XAML only and does not contain code.
- In the view model, there are no references to the view and the classes that are specific to the view.
None of the solutions found on the Internet satisfies all the listed requirements. View
Commands in MVVM ,
RelayCommand Yes Another One! ,
Asynchronous WPF Command ,
WPF Commands and Async Commands and the closest one for the requirements.
Templates for asynchronous MVVM applications: commands . Interestingly, all the solutions found have the same illogicality: they are centered around commands, adding a completely alien asynchrony to the
ICommand interface. From my point of view, it is much more logical to push off from tasks (Task), making them repeatable and adding to them the necessary commands, such as “start”, “cancel” or “pause”.
The following entities are implemented in my library. The
RepeatableTask class is a repeatable task that is self-contained and in no way associated with the MVVM template, or in general with the user interface. The RepeatableTask constructor specifies a synchronous or asynchronous method that will be executed when the task is started. The purpose of the Start () and Cancel () methods is obvious. The TaskStarting, TaskStarted, and TaskEnded events allow you to set event-life event handlers for the task to be repeated.
public class RepeatableTask : IDisposable { // RepeatableTask // . public RepeatableTask (Func<object, CancellationToken, Task> taskFactory); // RepeatableTask // . public RepeatableTask (Action<object, CancellationToken> taskAction, TaskScheduler taskScheduler = null); // true , false. public bool IsRunning { get; } // . // . public void Start (object state); // . public void Cancel (); // . public void Dispose (); // . public event EventHandler<DataEventArgs<CompletedTaskData>> TaskEnded; // . public event EventHandler<DataEventArgs<object>> TaskStarted; // , . public event EventHandler<TaskStartingEventArgs> TaskStarting; }
In the
CommandedRepeatableTask class inherited from RepeatableTask, start and cancel commands are added that can be used to bind to the user interface.
public class CommandedRepeatableTask : RepeatableTask, INotifyPropertyChanged { // CommandedRepeatableTask // . public CommandedRepeatableTask (Func<object, CancellationToken, Task> taskFactory); // CommandedRepeatableTask // . public CommandedRepeatableTask (Action<object, CancellationToken> taskAction, TaskScheduler taskScheduler); // . public ChainedRelayCommand<object> StartCommand { get; } // . public ChainedRelayCommand StopCommand { get; } // . public event PropertyChangedEventHandler PropertyChanged; // . public CommandedRepeatableTask CreateLinked (Func<object, CancellationToken, Task> taskFactory); // . public CommandedRepeatableTask CreateLinked (Action<object, CancellationToken> taskAction, TaskScheduler taskScheduler); }
ChainedRelayCommand commands
are based on the widespread
RelayCommand and are supplemented by support for
chaining to form groups of interrelated (in terms of user interface) actions.
public class ChainedRelayCommand : ICommand { // ChainedRelayCommand // // . // . public ChainedRelayCommand (Action execute, Func<bool> canExecute = null); // ChainedRelayCommand // , // // . public ChainedRelayCommand (CommandChain commandChain, Action execute, Func<bool> canExecute = null); // , . public CommandChain Chain { get; } // . public void Execute (object parameter); // public bool CanExecute (object parameter); // CanExecuteChanged . public void RaiseCanExecuteChanged (); // , . public event EventHandler CanExecuteChanged; }
The
CommandChain class contains a list of combined commands and parameters for their joint execution.
public class CommandChain { // RelayCommandChain // . public CommandChain (bool executionChained, ExecutionAbilityChainBehavior canExecuteChainBehavior); // // . public ExecutionAbilityChainBehavior ExecutionAbilityChainBehavior { get; } // . public bool ExecutionChained { get; } // . public SingleLinkedListNode<ChainedCommandBase> FirstCommand { get; } // . public void Add (ChainedCommandBase command); // . public void Clear (); }
It is easy to use the described entities, especially for those who work with the MVVM pattern and are already familiar with the RelayCommand. To create simple actions that do not need to be performed asynchronously (for example, sorting a list by clicking on its header), use the ChainedRelayCommand as well as the RelayCommand. Create a command by specifying the method of the model that performs the sort. You expose the command as a property of the view model.
public class AppViewModel { private readonly ChainedRelayCommand<object> _sortListCommand; public ChainedRelayCommand<object> SortListCommand { get { return _sortListCommand; } } public AppViewModel () { _sortListCommand = new ChainedRelayCommand<object> (arg => SortList ((string)arg)); } } private void SortList (string column) {
In the view for the desired control for the Command property, specify a binding to the created property of the view model. For the mentioned case of sorting by clicking on the header of the list, it makes sense for each column to specify the same command, but apart from the Command, specify the property CommandParameter with the value by which to do the sorting.
<ListView> <ListView.View> <GridView> <GridView.Columns> <GridViewColumn> <GridViewColumnHeader Command="{Binding SortListCommand}" CommandParameter="ID" Content="ID" /> </GridViewColumn> <GridViewColumn> <GridViewColumnHeader Command="{Binding SortListCommand}" CommandParameter="Name" Content="" /> </GridViewColumn> </GridView.Columns> </GridView> </ListView.View> </ListView>
For an action that requires asynchronous execution, create a RepeatatableTask repeatable task, specifying the model method. The task is set as a property of the view model. If actions form a mutually exclusive group, after creating the first action, the second and subsequent actions are not created through the constructor, but through a call to the CreateLinked method of the first action. Actions in a group (chain) use as well as single. At the same time, the availability of a start command (StartCommand) of each action will depend on the status of other actions of the group. And the cancellation command (StopCommand) of any of them will behave in the same way (cancel any of the running actions of the group).
')
public class AppViewModel { private readonly CommandedRepeatableTask _workTask1; private readonly CommandedRepeatableTask _workTask2; private readonly CommandedRepeatableTask _workTask3; private readonly BusinessLogicService _businessLogicService; public CommandedRepeatableTask WorkTask1 { get { return _workTask1; } } public CommandedRepeatableTask WorkTask2 { get { return _workTask2; } } public CommandedRepeatableTask WorkTask3 { get { return _workTask3; } } public AppViewModel (BusinessLogicService businessLogicService) { _businessLogicService = businessLogicService; _workTask1 = new CommandedRepeatableTask (_businessLogicService.Work1, TaskScheduler.Default); _workTask2 = _workTask1.CreateLinked (_businessLogicService.Work2, TaskScheduler.Default); _workTask3 = _workTask1.CreateLinked (_businessLogicService.Work3, TaskScheduler.Default); } } public class BusinessLogicService { public void Work1 (object state, CancellationToken cancellationToken); public void Work2 (object state, CancellationToken cancellationToken); public void Work3 (object state, CancellationToken cancellationToken); }
In the view, you bind the properties of the StartCommand and StopCommand tasks to the corresponding controls.
<Button Command="{Binding WorkTask1.StartCommand}">Work1</Button> <Button Command="{Binding WorkTask2.StartCommand}">Work2</Button> <Button Command="{Binding WorkTask3.StartCommand}">Work3</Button> <Button Command="{Binding WorkTask1.StopCommand}">Cancel</Button>
Summarize. The created CommandedRepeatableTask class satisfies all the listed requirements for “actions” and additionally provides the following amenities:
- It is constructed on the basis of methods in both simple synchronous syntax and in async / await syntax.
- When running synchronous methods asynchronously, they can be executed in the desired execution thread or synchronization context. For example, actions with a shell (Shell) or clipboard (Clipboard) usually require execution in a stream with ApartmentState.STA.
- Cancellation request tokens are created automatically for each launch of each action.
- The provided cancel command initiates a cancel request for the token with which the action is started.
- The provided start and cancel commands reflect the status of the action in the ICommand method. CanExecute () and ICommand events are automatically generated. CanExecuteChanged when the state of the action changes.
- Allows you to assign preventive measures that may cancel the launch of the action or provide an additional parameter for it (for example, ask for confirmation before the responsible action or request the name of the file with which the action will work). In order to be able to create new views, a precautionary event is performed in the same thread (context) from which the launch of the action is initiated.
- Allows you to assign preparatory activities that will be launched before the start of the action. In order to be able to create new views (for example, the progress window), the preparatory event is performed in the same thread (context) from which the launch of the action was initiated.
- Allows you to assign logging events that will run after the action is completed, even if it is canceled or caused an exception. The logging event receives the status of the action and an exception with the argument. To be able to create new views (for example, an action error window), the recording event is performed in the same thread (context) from which the launch of the action was initiated.
The library is created as a Portable Class Library Profile259 (.NET Framework 4.5, Windows 8, Windows Phone 8.1, Windows Phone Silverlight 8). For building under the .NET Framework 4 a separate project has been added, which consists only of references to the source code of the main one. The example project shows various ways to use the library in a WPF application: in the toolbar buttons, in the main menu items, in the context menu items of the list entries, and in the list headers (to sort it). The solution with all the projects listed is uploaded to the
public repository on github . The assembly ready for use is laid out in the form of a
nuget-package . Happy programming!