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 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 return
void
and have zero or one parameter. - 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 System.Windows.Input;
3using Metalama.Patterns.Wpf;
4using Metalama.Patterns.Wpf.Implementation;
5
6namespace Doc.Command.SimpleCommand;
7
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 [Command]
19 public void Decrement()
20 {
21 this.Counter--;
22 }
23
24 public MyWindow()
25 {
26 IncrementCommand = new DelegateCommand(_ => Increment(), null);
27 DecrementCommand = new DelegateCommand(_ => Decrement(), null);
28 }
29
30 public ICommand DecrementCommand { get; }
31
32 public ICommand IncrementCommand { get; }
33}
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 System.Windows.Input;
3using Metalama.Patterns.Wpf;
4using Metalama.Patterns.Wpf.Implementation;
5
6namespace Doc.Command.CanExecute;
7
8public class MyWindow : Window
9{
10 public int Counter { get; private set; }
11
12 public bool CanExecuteIncrement => this.Counter < 10;
13
14 public bool CanExecuteDecrement => this.Counter > 0;
15
16 [Command]
17 public void Increment()
18 {
19 this.Counter++;
20 }
21
22 [Command]
23 public void Decrement()
24 {
25 this.Counter--;
26 }
27
28 public MyWindow()
29 {
30 IncrementCommand = new DelegateCommand(_ => Increment(), _ => CanExecuteIncrement);
31 DecrementCommand = new DelegateCommand(_ => Decrement(), _ => CanExecuteDecrement);
32 }
33
34 public ICommand DecrementCommand { get; }
35
36 public ICommand IncrementCommand { get; }
37}
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 System.Windows.Input;
3using Metalama.Patterns.Wpf;
4using Metalama.Patterns.Wpf.Implementation;
5
6namespace Doc.Command.CanExecute_Explicit;
7
8public class MyWindow : Window
9{
10 public int Counter { get; private set; }
11
12 public bool CanExecuteIncrement => this.Counter < 10;
13
14 public bool CanExecuteDecrement => this.Counter > 0;
15
16 [Command(CanExecuteProperty = nameof(CanExecuteIncrement))]
17 public void Increment()
18 {
19 this.Counter++;
20 }
21
22 [Command(CanExecuteProperty = nameof(CanExecuteDecrement))]
23 public void Decrement()
24 {
25 this.Counter--;
26 }
27
28 public MyWindow()
29 {
30 IncrementCommand = new DelegateCommand(_ => Increment(), _ => CanExecuteIncrement);
31 DecrementCommand = new DelegateCommand(_ => Decrement(), _ => CanExecuteDecrement);
32 }
33
34 public ICommand DecrementCommand { get; }
35
36 public ICommand IncrementCommand { get; }
37}
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 System.Windows.Input;
4using Metalama.Patterns.Observability;
5using Metalama.Patterns.Wpf;
6using Metalama.Patterns.Wpf.Implementation;
7
8namespace Doc.Command.CanExecute_Observable;
9
10[Observable]
11public class MyWindow : Window, INotifyPropertyChanged
12{
13 private int _counter;
14
15 public int Counter
16 {
17 get
18 {
19 return _counter;
20 }
21
22 private set
23 {
24 if (_counter != value)
25 {
26 _counter = value;
27 OnPropertyChanged("CanExecuteDecrement");
28 OnPropertyChanged("CanExecuteIncrement");
29 OnPropertyChanged("Counter");
30 }
31 }
32 }
33
34 [Command]
35 public void Increment()
36 {
37 this.Counter++;
38 }
39
40 public bool CanExecuteIncrement => this.Counter < 10;
41
42 [Command]
43 public void Decrement()
44 {
45 this.Counter--;
46 }
47
48 public bool CanExecuteDecrement => this.Counter > 0;
49
50 public MyWindow()
51 {
52 IncrementCommand = new DelegateCommand(_ => Increment(), _ => CanExecuteIncrement, this, "CanExecuteIncrement");
53 DecrementCommand = new DelegateCommand(_ => Decrement(), _ => CanExecuteDecrement, this, "CanExecuteDecrement");
54 }
55
56 public ICommand DecrementCommand { get; }
57
58 public ICommand IncrementCommand { get; }
59
60 protected virtual void OnPropertyChanged(string propertyName)
61 {
62 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
63 }
64
65 public event PropertyChangedEventHandler? PropertyChanged;
66}
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:
_
andCommand
.
- 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 one 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 System.Windows;
2using Metalama.Framework.Fabrics;
3using Metalama.Patterns.Wpf;
4using Metalama.Patterns.Wpf.Configuration;
5
6namespace Doc.Command.CanExecute_Czech;
7
8public class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 {
12 amender.ConfigureCommand(
13 builder =>
14 {
15 builder.AddNamingConvention(
16 new CommandNamingConvention( "czech-1" )
17 {
18 CommandNamePattern = "^Vykonat(.*)$",
19 CanExecutePatterns = ["MůžemeVykonat{CommandName}"],
20 CommandPropertyName = "{CommandName}Příkaz"
21 } );
22
23 builder.AddNamingConvention(
24 new CommandNamingConvention( "czech-2" )
25 {
26 CanExecutePatterns =
27 ["Můžeme{CommandName}"],
28 CommandPropertyName = "{CommandName}Příkaz"
29 } );
30 } );
31 }
32}
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 System.Windows.Input;
3using Metalama.Patterns.Wpf;
4using Metalama.Patterns.Wpf.Implementation;
5
6namespace Doc.Command.CanExecute_Czech;
7
8public class MojeOkno : Window
9{
10 public int Počitadlo { get; private set; }
11
12 [Command]
13 public void VykonatZvýšení()
14 {
15 this.Počitadlo++;
16 }
17
18 public bool MůžemeVykonatZvýšení => this.Počitadlo < 10;
19
20 [Command]
21 public void Snížit()
22 {
23 this.Počitadlo--;
24 }
25
26 public bool MůžemeSnížit => this.Počitadlo > 0;
27
28 public MojeOkno()
29 {
30 VykonatZvýšeníPříkaz = new DelegateCommand(_ => VykonatZvýšení(), _ => MůžemeVykonatZvýšení);
31 SnížitPříkaz = new DelegateCommand(_ => Snížit(), _ => MůžemeSnížit);
32 }
33
34 public ICommand SnížitPříkaz { get; }
35
36 public ICommand VykonatZvýšeníPříkaz { get; }
37}