In WPF, a command is an object that implements the ICommand interface, which can be bound to UI controls such as buttons to trigger actions and can enable or disable these controls based on the CanExecute method. The Execute method runs the command, while the CanExecuteChanged event notifies when the command's availability changes.
Implementing WPF commands manually typically requires much boilerplate code, especially to support the CanExecuteChanged event.
The [Command] aspect generates most of the WPF command boilerplate automatically. When applied to a method, the aspect generates a Command property. It can also bind to a CanExecute
property or method and integrates with INotifyPropertyChanged.
Generating a WPF command property from a method
To generate a WPF command property from a method:
Add a reference to the
Metalama.Patterns.Wpf
package to your project.Add the [Command] attribute to the method that must be executed when the command is invoked. This method will become the implementation of the ICommand.Execute interface method. It must have one of the following signatures, where
T
is an arbitrary type:[Command] void Execute(); [Command( Background = true )] void Execute(CancellationToken); // Only for background commands. See below. [Command] void Execute(T); [Command( Background = true )] void Execute(T, CancellationToken); // Only for background commands. See below. [Command] Task ExecuteAsync(); [Command] Task ExecuteAsync(CancellationToken); [Command] Task ExecuteAsync(T); [Command] Task ExecuteAsync(T, CancellationToken);
Make the class
partial
to enable referencing the generated command properties from C# or WPF source code.
Example: Simple commands
The following example implements a window with two commands: Increment
and Decrement
. As illustrated, the [Command] aspect generates two properties, IncrementCommand
and DecrementCommand
, assigned to an instance of the DelegateCommand helper class. This class accepts a delegate to the Increment
or Decrement
method.
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.SimpleCommand;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 [Command]
11 public void Increment()
12 {
13 this.Counter++;
14 }
15
16 [Command]
17 public void Decrement()
18 {
19 this.Counter--;
20 }
21}
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.SimpleCommand;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 [Command]
11 public void Increment()
12 {
13 this.Counter++;
14 }
15
16 [Command]
17 public void Decrement()
18 {
19 this.Counter--;
20 }
21
22 public MyWindow()
23 {
24 IncrementCommand = DelegateCommandFactory.CreateDelegateCommand(Increment, null);
25 DecrementCommand = DelegateCommandFactory.CreateDelegateCommand(Decrement, null);
26 }
27
28 public DelegateCommand DecrementCommand { get; }
29
30 public DelegateCommand IncrementCommand { get; }
31}
Adding a CanExecute method or property
In addition to the Execute method, you can also supply an implementation of ICommand.CanExecute. This implementation can be either a bool
property or, when the Execute
method has a parameter, a method that accepts the same parameter type and returns bool
.
There are two ways to associate a CanExecute
implementation with the Execute
member:
- Implicitly, by respecting naming conventions. For a command named
Foo
, theCanExecute
member can be namedCanFoo
,CanExecuteFoo
, orIsFooEnabled
. See below to learn how to customize these naming conventions. - Explicitly, by setting the CanExecuteMethod or CanExecuteProperty property of the CommandAttribute.
When the CanExecute
member is a property and the declaring type implements the INotifyPropertyChanged interface, the ICommand.CanExecuteChanged event will be raised whenever the CanExecute
property changes. You can use the [Observable] aspect to implement INotifyPropertyChanged. See Metalama.Patterns.Observability for details.
Example: Commands with a CanExecute property and implicit association
The following example demonstrates two commands, Increment
and Decrement
, coupled to properties that determine if these commands are available: CanIncrement
and CanDecrement
.
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 public bool CanExecuteIncrement => this.Counter < 10;
11
12 public bool CanExecuteDecrement => this.Counter > 0;
13
14 [Command]
15 public void Increment()
16 {
17 this.Counter++;
18 }
19
20 [Command]
21 public void Decrement()
22 {
23 this.Counter--;
24 }
25}
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 public bool CanExecuteIncrement => this.Counter < 10;
11
12 public bool CanExecuteDecrement => this.Counter > 0;
13
14 [Command]
15 public void Increment()
16 {
17 this.Counter++;
18 }
19
20 [Command]
21 public void Decrement()
22 {
23 this.Counter--;
24 }
25
26 public MyWindow()
27 {
28 IncrementCommand = DelegateCommandFactory.CreateDelegateCommand(Increment, () => CanExecuteIncrement);
29 DecrementCommand = DelegateCommandFactory.CreateDelegateCommand(Decrement, () => CanExecuteDecrement);
30 }
31
32 public DelegateCommand DecrementCommand { get; }
33
34 public DelegateCommand IncrementCommand { get; }
35}
Example: Commands with a CanExecute property and explicit association
This example is identical to the one above, but it uses the CanExecuteProperty property to explicitly associate the CanExecute
property with their Execute
method.
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute_Explicit;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 public bool CanExecuteIncrement => this.Counter < 10;
11
12 public bool CanExecuteDecrement => this.Counter > 0;
13
14 [Command( CanExecuteProperty = nameof(CanExecuteIncrement) )]
15 public void Increment()
16 {
17 this.Counter++;
18 }
19
20 [Command( CanExecuteProperty = nameof(CanExecuteDecrement) )]
21 public void Decrement()
22 {
23 this.Counter--;
24 }
25}
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute_Explicit;
5
6public class MyWindow : Window
7{
8 public int Counter { get; private set; }
9
10 public bool CanExecuteIncrement => this.Counter < 10;
11
12 public bool CanExecuteDecrement => this.Counter > 0;
13
14 [Command(CanExecuteProperty = nameof(CanExecuteIncrement))]
15 public void Increment()
16 {
17 this.Counter++;
18 }
19
20 [Command(CanExecuteProperty = nameof(CanExecuteDecrement))]
21 public void Decrement()
22 {
23 this.Counter--;
24 }
25
26 public MyWindow()
27 {
28 IncrementCommand = DelegateCommandFactory.CreateDelegateCommand(Increment, () => CanExecuteIncrement);
29 DecrementCommand = DelegateCommandFactory.CreateDelegateCommand(Decrement, () => CanExecuteDecrement);
30 }
31
32 public DelegateCommand DecrementCommand { get; }
33
34 public DelegateCommand IncrementCommand { get; }
35}
Example: Commands with a CanExecute property and [Observable]
The following example demonstrates the code generated when the [Command]
and [Observable]
aspects are used together. Notice the compactness of the source code and the significant size of the generated code.
1using System.Windows;
2using Metalama.Patterns.Observability;
3using Metalama.Patterns.Wpf;
4
5namespace Doc.Command.CanExecute_Observable;
6
7[Observable]
8public class MyWindow : Window
9{
10 public int Counter { get; private set; }
11
12 [Command]
13 public void Increment()
14 {
15 this.Counter++;
16 }
17
18 public bool CanExecuteIncrement => this.Counter < 10;
19
20 [Command]
21 public void Decrement()
22 {
23 this.Counter--;
24 }
25
26 public bool CanExecuteDecrement => this.Counter > 0;
27}
1using System.ComponentModel;
2using System.Windows;
3using Metalama.Patterns.Observability;
4using Metalama.Patterns.Wpf;
5
6namespace Doc.Command.CanExecute_Observable;
7
8[Observable]
9public class MyWindow : Window, INotifyPropertyChanged
10{
11 private int _counter;
12
13 public int Counter
14 {
15 get
16 {
17 return _counter;
18 }
19
20 private set
21 {
22 if (_counter != value)
23 {
24 _counter = value;
25 OnPropertyChanged("CanExecuteDecrement");
26 OnPropertyChanged("CanExecuteIncrement");
27 OnPropertyChanged("Counter");
28 }
29 }
30 }
31
32 [Command]
33 public void Increment()
34 {
35 this.Counter++;
36 }
37
38 public bool CanExecuteIncrement => this.Counter < 10;
39
40 [Command]
41 public void Decrement()
42 {
43 this.Counter--;
44 }
45
46 public bool CanExecuteDecrement => this.Counter > 0;
47
48 public MyWindow()
49 {
50 IncrementCommand = DelegateCommandFactory.CreateDelegateCommand(Increment, () => CanExecuteIncrement, this, "CanExecuteIncrement");
51 DecrementCommand = DelegateCommandFactory.CreateDelegateCommand(Decrement, () => CanExecuteDecrement, this, "CanExecuteDecrement");
52 }
53
54 public DelegateCommand DecrementCommand { get; }
55
56 public DelegateCommand IncrementCommand { get; }
57
58 protected virtual void OnPropertyChanged(string propertyName)
59 {
60 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
61 }
62
63 public event PropertyChangedEventHandler? PropertyChanged;
64}
Async commands
When the Execute
method returns a Task
, the [Command]
aspect implements an asynchronous command, which means that the ICommand.Execute method returns immediately (i.e., after the first non-synchronous await
). The aspect generates a property of type AsyncDelegateCommand, which implements INotifyPropertyChanged and exposes the following members:
- The ExecutionTask property returns the task representing the last execution of the command.
- The Cancel method allows canceling the current task.
- The CanExecute, CanCancel, IsCancellationRequested, and IsRunning properties expose the state of the command.
By default, the CanExecute property returns false
if the previous call of the Execute method is still running. To allow for concurrent execution, set the CommandAttribute.SupportsConcurrentExecution property to true
.
To track and cancel concurrent executions of the command, subscribe to the Executed event and use the DelegateCommandExecution object.
Background commands
By default, the implementation method of the command is executed in the foreground thread. You can dispatch its execution to a background thread by setting the CommandAttribute.Background property to true
. This will work for implementation methods returning both void
or a Task
.
In both cases, the [Command]
aspect generates a property of type AsyncDelegateCommand.
Customizing naming conventions
All examples above relied on the default naming convention, which is based on the following assumptions:
- The command name is obtained by trimming the
Execute
method name (the one with the[Command]
aspect) from:- prefixes:
_
,m_
, andExecute
, - suffix:
_
,Command
, andAsync
.
- prefixes:
- Given a command name
Foo
determined by the previous step:- The command property is named
FooCommand
. - The
CanExecute
command or method can be namedCanFoo
,CanExecuteFoo
, orIsFooEnabled
.
- The command property is named
This naming convention can be modified by calling the ConfigureCommand fabric extension method, then builder.AddNamingConvention, and supply an instance of the CommandNamingConvention class.
If specified, the CommandNamingConvention.CommandNamePattern is a regular expression that matches the command name from the name of the main method. If this property is unspecified, the default matching algorithm is used. The CanExecutePatterns property is a list of patterns used to select the CanExecute
property or method, and the CommandPropertyName property is a pattern that generates the name of the generated command property. In the CanExecutePatterns and CommandPropertyName, the {CommandName}
substring is replaced by the name of the command returned by CommandNamePattern.
Naming conventions are evaluated by priority order. The default priority is the order in which the convention has been added. It can be overwritten by supplying a value to the priority
parameter.
The default naming convention is evaluated last and cannot be modified.
Example: Czech Naming Conventions
The following example illustrates a naming convention for the Czech language. There are two conventions. The first matches the Vykonat
prefix in the main method, for instance, it will match a method named VykonatBlb
and return Blb
as the command name. The second naming convention matches everything and removes the conventional prefixes _
and Execute
as described above. The default naming convention is never used in this example.
1using Metalama.Framework.Fabrics;
2using Metalama.Patterns.Wpf.Configuration;
3
4namespace Doc.Command.CanExecute_Czech;
5
6public class Fabric : ProjectFabric
7{
8 public override void AmendProject( IProjectAmender amender )
9 {
10 amender.ConfigureCommand(
11 builder =>
12 {
13 builder.AddNamingConvention(
14 new CommandNamingConvention( "czech-1" )
15 {
16 CommandNamePattern = "^Vykonat(.*)$",
17 CanExecutePatterns = ["MůžemeVykonat{CommandName}"],
18 CommandPropertyName = "{CommandName}Příkaz"
19 } );
20
21 builder.AddNamingConvention(
22 new CommandNamingConvention( "czech-2" )
23 {
24 CanExecutePatterns =
25 ["Můžeme{CommandName}"],
26 CommandPropertyName = "{CommandName}Příkaz"
27 } );
28 } );
29 }
30}
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute_Czech;
5
6public class MojeOkno : Window
7{
8 public int Počitadlo { get; private set; }
9
10 [Command]
11 public void VykonatZvýšení()
12 {
13 this.Počitadlo++;
14 }
15
16 public bool MůžemeVykonatZvýšení => this.Počitadlo < 10;
17
18 [Command]
19 public void Snížit()
20 {
21 this.Počitadlo--;
22 }
23
24 public bool MůžemeSnížit => this.Počitadlo > 0;
25}
1using System.Windows;
2using Metalama.Patterns.Wpf;
3
4namespace Doc.Command.CanExecute_Czech;
5
6public class MojeOkno : Window
7{
8 public int Počitadlo { get; private set; }
9
10 [Command]
11 public void VykonatZvýšení()
12 {
13 this.Počitadlo++;
14 }
15
16 public bool MůžemeVykonatZvýšení => this.Počitadlo < 10;
17
18 [Command]
19 public void Snížit()
20 {
21 this.Počitadlo--;
22 }
23
24 public bool MůžemeSnížit => this.Počitadlo > 0;
25
26 public MojeOkno()
27 {
28 VykonatZvýšeníPříkaz = DelegateCommandFactory.CreateDelegateCommand(VykonatZvýšení, () => MůžemeVykonatZvýšení);
29 SnížitPříkaz = DelegateCommandFactory.CreateDelegateCommand(Snížit, () => MůžemeSnížit);
30 }
31
32 public DelegateCommand SnížitPříkaz { get; }
33
34 public DelegateCommand VykonatZvýšeníPříkaz { get; }
35}