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接口成员组成更复杂的逻辑。