绑定和命令是MVVM模式的核心,也是实际MVVM工程中应用最多的技术点。
与路由事件的关系
在深入了解WPF的命令之前,有必要澄清它和路由事件的关系:
路由事件提供了一种灵活的事件传递机制,主要用于处理界面状态的变化。
命令主要用于封装业务逻辑,从而保证代码复用性。
路由事件和命令是出发点不同的两套机制,不是相互替换的关系。
在大型软件系统中,通常需要同时使用路由事件和命令来协作完成某些复杂功能。
使用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 <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 >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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()逻辑,每个命令不是相对独立的个体,与“高内聚”的软件设计思想有所违背,不是推荐用法。
自定义命令类
回归命令机制的初衷,开发者不希望在窗体的事件处理函数中写业务逻辑,更倾向于将每个业务逻辑写成普通函数,然后包装成命令供界面使用。我认为以下方式是比较好的工程做法。
(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 26 27 28 29 30 31 public class RelayCommand : ICommand { private Action<object > execute; private Func<object , bool > canExecute; public RelayCommand (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 public class ViewModel { #region 命令定义 private RelayCommand _showMessageCommand; public RelayCommand ShowMessageCommand => _showMessageCommand ?? (_showMessageCommand = new RelayCommand(ShowMessage)); private RelayCommand _modifyMessageCommand; public RelayCommand ModifyMessageCommand => _modifyMessageCommand ?? (_modifyMessageCommand = new RelayCommand(ModifyMessage)); #endregion #region 业务逻辑 public void ShowMessage (object parameter ) { MessageBox.Show(Name); } public void ModifyMessage (object parameter ) { Name = "xiaoming" ; } #endregion }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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}" /> </StackPanel > </Window >
以上代码有几个关键点:
① 给命令传参:在XAML中通过绑定将窗体作为参数传递给命令,CanExecute()和Execute()都能捕获这个参数。
② 更新按钮的可用状态:本实例在窗体宽度大于300时设置按钮可见,点击按钮后消息框显示窗体的宽度。为了更新按钮状态,必须在窗体尺寸变化事件处理器中调用RaiseCanExecuteChange()函数来间接触发CanExecute()函数;不直接调用CanExecute()函数,是因为开发者没有办法显式向其传递参数。
为控件事件绑定命令
某些场景下,我们想给控件的某一事件绑定一个命令,这时可以通过Microsoft.Xaml.Behaviors.Wpf
库实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <Window x:Class ="MVVMTest.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:MVVMTest" xmlns:i ="http://schemas.microsoft.com/xaml/behaviors" > <i:Interaction.Triggers > <i:EventTrigger EventName ="Loaded" > <i:InvokeCommandAction Command ="{Binding WindowLoadedCommand}" /> </i:EventTrigger > </i:Interaction.Triggers > </Window >