📜 ⬆️ ⬇️

Command infrastructure for the user to invoke actions in the MVVM pattern

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:

Application Developer Requirements:

Common MVVM design pattern requirements:

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) { //      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:

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!

Source: https://habr.com/ru/post/242073/


All Articles