8.WPF命令
命令系统的基本元素
- 命令:实现了ICommand的类,常见的是RoutedCommand类,也可以自定义命令
- 命令源:命令的发送者,要实现ICommandSource接口
- 命令目标:命令发送给谁,或者说命令作用在谁身上,必须实现IInputElement接口
- 命令关联:把一些外围逻辑和命令关联起来,如之前判断是否可执行,以及命令执行之后还要执行哪些工作
命令的基本使用步骤
-
创建命令类,实现ICommand接口,如果命令与具体逻辑无关,则直接可以使用RoutedCommand类。
-
声明命令实例,一般一个程序某种操作只需要一个命令实例(命令目标可设置多个)。
-
指定命令源,指定谁发送命令,如保存命令可以通过菜单栏来发送,也可以通过快捷工具栏来发送。同时命令源会受命令的影响,如命令不能被执行时,命令源控件为不可用状态。
-
指定命令目标,命令目标不是命令的属性,而是命令源的属性。如果没有为命令源设置命令目标,则当前拥有焦点的对象为命令目标。
-
设置命令关联,通过CommandBinding在执行前帮助判断命令是否可执行,并在执行后处理其他逻辑。
**命令目标和命令关联之间的关系:**当命令源和命令目标建立联系后,命令目标会不停发送可路由的PreviewCanExecute和CanExecute事件,事件会沿着UI元素树传递最后被命令关联所捕获,命令关联捕获到这些事件后,把命令能不能执行报告给命令。类似的,如果命令被发送出来并送达目标命令,命令目标会发送PreviewExecuted和Executed事件,这两个事件也会被命令关联捕获,然后命令关联去执行后续工作。其中,命令目标负责发送各种事件,命令负责跑腿,真正执行操作的是命令关联。
使用命令的好处:可以避免自己写代码判断控件是否可用以及添加快捷键
案例:
<StackPanel x:Name="stackPanel">
<Button x:Name="btn" Content="Send Command" Margin="5"/>
<TextBox x:Name="txtA" Height="100" Margin="5,0"/>
</StackPanel>
后台代码
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属性,表示命令的相关信息。
<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>
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方法。
- 声明接口,命令目标实现该接口
public interface IView
{
bool IsChanged { set; get; }
void Clear();
}
- 自定义命令
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();
}
}
}
- 自定义命令源
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);
}
}
}
- 定义命令目标-使用WPF自定义组件
<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>
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();
}
}
- 使用自定义命令
<StackPanel>
<local:MyCommandSource x:Name="ctlClear" Margin="10">
<TextBlock Text="清除" TextAlignment="Center" Width="80"/>
</local:MyCommandSource>
<local:MiniView x:Name="miniView"/>
</StackPanel>
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接口成员组成更复杂的逻辑。