8.WPF命令

8.WPF命令

8.WPF命令

命令系统的基本元素

  • 命令:实现了ICommand的类,常见的是RoutedCommand类,也可以自定义命令
  • 命令源:命令的发送者,要实现ICommandSource接口
  • 命令目标:命令发送给谁,或者说命令作用在谁身上,必须实现IInputElement接口
  • 命令关联:把一些外围逻辑和命令关联起来,如之前判断是否可执行,以及命令执行之后还要执行哪些工作

命令的基本使用步骤

  1. 创建命令类,实现ICommand接口,如果命令与具体逻辑无关,则直接可以使用RoutedCommand类。

  2. 声明命令实例,一般一个程序某种操作只需要一个命令实例(命令目标可设置多个)。

  3. 指定命令源,指定谁发送命令,如保存命令可以通过菜单栏来发送,也可以通过快捷工具栏来发送。同时命令源会受命令的影响,如命令不能被执行时,命令源控件为不可用状态。

  4. 指定命令目标,命令目标不是命令的属性,而是命令源的属性。如果没有为命令源设置命令目标,则当前拥有焦点的对象为命令目标。

  5. 设置命令关联,通过CommandBinding在执行前帮助判断命令是否可执行,并在执行后处理其他逻辑。

    命令目标和命令关联之间的关系:当命令源和命令目标建立联系后,命令目标会不停发送可路由的PreviewCanExecute和CanExecute事件,事件会沿着UI元素树传递最后被命令关联所捕获,命令关联捕获到这些事件后,把命令能不能执行报告给命令。类似的,如果命令被发送出来并送达目标命令,命令目标会发送PreviewExecuted和Executed事件,这两个事件也会被命令关联捕获,然后命令关联去执行后续工作。其中,命令目标负责发送各种事件,命令负责跑腿,真正执行操作的是命令关联。

使用命令的好处:可以避免自己写代码判断控件是否可用以及添加快捷键

案例:

1
2
3
4
<StackPanel x:Name="stackPanel">
<Button x:Name="btn" Content="Send Command" Margin="5"/>
<TextBox x:Name="txtA" Height="100" Margin="5,0"/>
</StackPanel>

后台代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public MainWindow()
{
InitializeComponent();
InitializeCommand();
}
//声明并定义命令
private RoutedCommand clearCmd = new RoutedCommand("Clear", typeof(MainWindow));
private void InitializeCommand()
{
//把命令赋值给命令源
this.btn.Command = this.clearCmd;
this.clearCmd.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt));//给命令设置快捷键

//指定命令目标
this.btn.CommandTarget = this.txtA;//这是目标源的属性

//创建命令关联
CommandBinding cb = new CommandBinding();
cb.Command = this.clearCmd; //只关注与clearCmd相关的事件
cb.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute);
cb.Executed += new ExecutedRoutedEventHandler(cb_Executed);

//把命令关联安置在外围控件上
this.stackPanel.CommandBindings.Add(cb);

}
//命令送达目标后,此方法被调用
private void cb_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.txtA.Clear();
e.Handled = true;//避免继续传递而降低性能
}
//判断命令是否可执行
private void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.txtA.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
e.Handled = true;//避免继续传递而降低性能
}

      *案例说明:*RoutedCommand是一个与业务逻辑无关的类,只负责在程序中“跑腿”,而不会对命令目标执行任何操作。命令关联把命令能否可用告诉命令,然后会影响到命令源。命令到达命令目标后,命令目标会触发相关事件。

真正对命令目标执行操作的是命令关联。

命令目标不断的发送路由事件,CommandBinding需要安装在外围的UI树上,起到一个监听作用。

因为命令目标会不断发送CanExecute事件,为了避免降低性能,建议处理完后把e.Handled设置为True。

WPF命令库和命令参数

WPF类库中已经准备了常用的命令如打开、关闭、撤销等。这些命令库包括

  • ApplicationCommands
  • ComponentCommands
  • NavigationCommands
  • MediaCommands
  • EditingCommands

这些都是静态类,其中的命令则是类中的静态属性。

命令参数

命令库中的静态预制命令,全局仅有一个,而如果界面有两个按钮,分别需要用New命令新建不同的工程,如何实现?

命令源实现了ICommandSource接口,接口中具有CommandPrameter属性,表示命令的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="24"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="Name:" VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="0"/>
<TextBox x:Name="txtName" Margin="60,0,0,0" Grid.Row="0"/>
<Button Content="New Teacher" Command="New" CommandParameter="Teacher" Grid.Row="2"/>
<Button Content="New Student" Command="New" CommandParameter="Student" Grid.Row="4"/>
<ListBox x:Name="list" Grid.Row="6"/>
</Grid>
<Window.CommandBindings>
<CommandBinding Command="New" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (string.IsNullOrEmpty(this.txtName.Text))
{
e.CanExecute = false;
}
else
{
e.CanExecute = true;
}
e.Handled = true;
}

private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
string name = this.txtName.Text;
if (e.Parameter.ToString()=="Teacher")
{
this.list.Items.Add($"新教师{name}");
}
if (e.Parameter.ToString() == "Student")
{
this.list.Items.Add($"新学生{name}");
}
}

命令与Binding的结合

控件只有一个Command属性,而命令库有数十种命令,如何使用唯一的Command属性来调用多个命令,答案是Binding。

例如,一个button所关联的命令可能根据某些条件而改变

<Button x:Name="btn" Command={Binding Path=xxx,Source=sss} Content="Command"/>

自定义命令

RoutedCommand与业务逻辑无关,业务逻辑主要依靠CommandBinding来实现业务逻辑。现自定义命令将业务逻辑移入命令的Execute中。

案例:自定义一个名为Save的命令,命令到达命令目标时,先通过命令目标的IsChanged属性判断命令目标的内容是否改变,如果改变则命令可执行,然后命令调用命令目标的Save方法。这样,命令直接在命令目标上起作用了,而不像RoutedCommand那样现在命令目标上激发路由事件,等外围控件捕捉到事件后再对命令目标进行处理。但是这样需要使用接口来约束命令目标具有IsChanged和Save方法。

  1. 声明接口,命令目标实现该接口
1
2
3
4
5
public interface IView
{
bool IsChanged { set; get; }
void Clear();
}
  1. 自定义命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClearCommand : ICommand
{
//命令可执行状态发生改变时激发
public event EventHandler CanExecuteChanged;

//判断命令是否可执行,暂不实现
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
//命令执行,与业务相关的逻辑
public void Execute(object parameter)
{
IView view = parameter as IView;
if (view !=null)
{
view.Clear();
}
}
}
  1. 自定义命令源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyCommandSource : UserControl, ICommandSource
{
public ICommand Command { set; get; }
public object CommandParameter { set; get; }
public IInputElement CommandTarget { set; get; }

//组件被单击时连带执行命令
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
//在命令目标上执行命令
if (this.CommandTarget!=null)
{
this.Command.Execute(this.CommandTarget);
}
}
}
  1. 定义命令目标-使用WPF自定义组件
1
2
3
4
5
6
7
8
<Border CornerRadius="15" BorderBrush="LawnGreen" BorderThickness="2">
<StackPanel>
<TextBox x:Name="txt1" Margin="5"/>
<TextBox x:Name="txt2" Margin="5"/>
<TextBox x:Name="txt3" Margin="5"/>
<TextBox x:Name="txt4" Margin="5"/>
</StackPanel>
</Border>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public partial class MiniView : UserControl,IView
{
public MiniView()
{
InitializeComponent();
}

public bool IsChanged { get ; set ; }

public void Clear()
{
this.txt1.Clear();
this.txt2.Clear();
this.txt3.Clear();
this.txt4.Clear();
}
}
  1. 使用自定义命令
1
2
3
4
5
6
<StackPanel>
<local:MyCommandSource x:Name="ctlClear" Margin="10">
<TextBlock Text="清除" TextAlignment="Center" Width="80"/>
</local:MyCommandSource>
<local:MiniView x:Name="miniView"/>
</StackPanel>
1
2
3
4
5
6
7
8
9
public MainWindow()
{
InitializeComponent();

//声明命令并使命令源和目标关联
ClearCommand clrCmd = new ClearCommand();
this.ctlClear.Command = clrCmd;
this.ctlClear.CommandTarget = this.miniView;
}

该案例使用了TextBlock作为激发控件,也可以使用图片等,如果使用Button,就不要重写OnMouseLeftButtonDown方法了,因为它和Button.Click事件冲突,而是应该捕捉Button.Click事件(Mouse事件会被Button吃掉)

如果想通过Command的CanExecute方法返回值来影响命令源状态,要对ICommand和ICommandSource接口成员组成更复杂的逻辑。

作者

步步为营

发布于

2024-05-08

更新于

2025-03-15

许可协议