学习WPF的命令机制,我认为最重要的是能够找到一种在MVVM模式中使用它的最佳工程实践方式。
绑定和命令是MVVM模式的核心,后面介绍MVVM模式基本是顺水推舟了。
与路由事件的关系
在深入了解WPF的命令之前,有必要澄清它和路由事件的关系:
(1)路由事件提供了一种灵活的事件传递机制,主要用于处理界面状态的变化;
(2)命令主要用于封装业务逻辑,从而保证代码复用性。
(3)路由事件和命令是出发点不同的两套机制,不是相互替换的关系。
(4)在大型软件系统中,通常需要同时使用路由事件和命令来协作完成某些复杂功能。
使用RoutedCommand
在WPF中,按钮控件有Command属性,设置这个属性就可以为按钮绑定命令。
一种方式是使用RoutedCommand类,这是标准库提供的实现了ICommand接口的命令类,但是它没有实现具体的CanExecute()和Executed()逻辑,需要通过命令关联的方式添加这两个逻辑。
(1)以下窗体中有一个按钮btn1和一个文本框tb1。当文本框内的内容为空时,按钮不可用;否则可用,且单击按钮时文本框里的内容会被清空:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <Window x:Class ="TestMVVM01.MainWindow" xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d ="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local ="clr-namespace:TestMVVM01" xmlns:sys ="clr-namespace:System;assembly=mscorlib" mc:Ignorable ="d" SizeToContent ="WidthAndHeight" Title ="MainWindow" > <StackPanel Orientation ="Vertical" x:Name ="sp1" > <StackPanel Orientation ="Vertical" x:Name ="sp2" > <Button x:Name ="btn1" Content ="Send Command" Margin ="5" /> <TextBox x:Name ="tb1" Margin ="5,0" Height ="100" /> </StackPanel > </StackPanel > </Window >
(2)后台代码通过CommandBinding为该命令设置CanExecute()和Executed()逻辑:
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 using System;using System.Windows;using System.Windows.Input;namespace TestMVVM01 { public partial class MainWindow : Window { public MainWindow () { InitializeComponent(); InitializeCommand(); } private RoutedCommand clearCmd = new RoutedCommand("Clear" , typeof (MainWindow)); private void InitializeCommand () { this .clearCmd.InputGestures.Add(new KeyGesture(Key.C, ModifierKeys.Alt)); this .btn1.Command = clearCmd; this .btn1.CommandTarget = this .tb1; CommandBinding binding = new CommandBinding(); binding.Command = this .clearCmd; binding.CanExecute += new CanExecuteRoutedEventHandler(cb_CanExecute); binding.Executed += new ExecutedRoutedEventHandler(cb_Executed); this .sp1.CommandBindings.Add(binding); } private void cb_Executed (object sender, ExecutedRoutedEventArgs e ) { this .tb1.Clear(); e.Handled = true ; } private void cb_CanExecute (object sender, CanExecuteRoutedEventArgs e ) { if (String.IsNullOrEmpty(this .tb1.Text)) e.CanExecute = false ; else e.CanExecute = true ; e.Handled = true ; } } }
当然,也可以在XAML中定义,以下使用WPF内置命令New,用户指定该命令的CanExecute()和Executed()逻辑。大多数内置命令是静态类的成员,需要实现个性化操作时通过传参实现:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <Window x:Class ="TestMVVM01.MainWindow" xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d ="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local ="clr-namespace:TestMVVM01" xmlns:sys ="clr-namespace:System;assembly=mscorlib" mc:Ignorable ="d" Height ="240" Width ="360" WindowStyle ="ToolWindow" Background ="LightBlue" Title ="Command Parameter" > <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 ="nameTextBox" 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 ="listBoxItems" Grid.Row ="6" /> </Grid > <Window.CommandBindings > <CommandBinding Command ="New" CanExecute ="CommandBinding_CanExecute" Executed ="CommandBinding_Executed" /> </Window.CommandBindings > </Window > /// MainWindow.xaml 的交互逻辑 /// </summary > public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { if (string.IsNullOrEmpty(this.nameTextBox.Text)) e.CanExecute = false; else e.CanExecute = true; } private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) { string name = this.nameTextBox.Text; if (e.Parameter.ToString() == "Teacher") this.listBoxItems.Items.Add($"New Teacher: {name}, 学而不厌、诲人不倦。"); if (e.Parameter.ToString() == "Student") this.listBoxItems.Items.Add($"New Student: {name}, 好好学习、天天向上。"); } }
以上方式需要在代码中集中指定命令的CanExecute()和Executed()逻辑,每个命令不是相对独立的个体,与“高内聚”的软件设计思想有所违背,不是推荐用法。
为控件事件绑定命令
某些场景下,我们希望给控件的某一事件绑定一个命令,这时可以使用System.Windows.Interactivity.WPF库实现。
较好的工程实践
回归命令机制的初衷,开发者不希望在窗体的事件处理函数中写业务逻辑,更倾向于将每个业务逻辑写成普通函数,然后包装成命令供界面使用。
那么,如何将业务处理逻辑包装成命令呢?我认为以下方式是比较好的工程做法。
(1)自定义以下命令类,通过构造函数传入实际的Execute()和CanExecute()执行逻辑,开发者可显式调用RaiseCanExecuteChange()函数,从而触发CanExecute()的执行来更新控件的可用状态(一般在窗体控件事件中触发)。
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 public class MyCommand : ICommand { private Action<object > execute; private Func<object , bool > canExecute; public MyCommand (Action<object > execute, Func<object , bool > canExecute = null ) { this .execute = execute; this .canExecute = canExecute; } public event EventHandler CanExecuteChanged; public bool CanExecute (object parameter ) { if (canExecute != null ) return CanExecute(parameter); return true ; } public void Execute (object parameter ) { execute?.Invoke(parameter); } public void RaiseCanExecuteChange () { CanExecuteChanged?.Invoke(this , EventArgs.Empty); } }
(2)在ViewModel类中定义命令,并包装成可读可写属性供界面绑定。
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 46 47 public class TestViewModel : NotifyPropertyChanged { private MyCommand _command1; public MyCommand Command1 { get { return _command1; } set { _command1 = value ; RaisePropertyChanged(); } } public TestViewModel () { _command1 = new MyCommand(ShowMessage, SetEnbale); } #region 业务处理逻辑,程序的核心 private void ShowMessage (object parameter ) { var para = parameter as Window; if (para != null ) MessageBox.Show($"窗体宽度为:{para.Width} " ); } private bool SetEnbale (object parameter ) { var para = parameter as Window; if (para != null && para.Width > 300 ) return true ; return false ; } #endregion } <Window x:Class="TestMvvm.Views.Test" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:TestMvvm.Views" mc:Ignorable="d" x:Name="myWin" SizeChanged="myWin_SizeChanged" Title="Test" SizeToContent="WidthAndHeight" > <StackPanel Orientation="Vertical" > <Button x:Name="btn1" Content="按钮1" Command="{Binding Command1}" CommandParameter="{Binding ElementName=myWin}" /> </StackPanel> </Window>
以上代码有几个关键点:
① 给命令传参:在XAML中通过绑定将窗体作为参数传递给命令,CanExecute()和Execute()都能捕获这个参数。
② 更新按钮的可用状态:本实例在窗体宽度大于300时设置按钮可见,点击按钮后消息框显示窗体的宽度。为了更新按钮状态,必须在窗体尺寸变化事件处理器中调用RaiseCanExecuteChange()函数来间接触发CanExecute()函数;不直接调用CanExecute()函数,是因为开发者没有办法显式向其传递参数。