命令
事件驱动模式有一个不太好的地方是,它的前端XAML代码和后端的C#代码建立了一种强关联的耦合关系,无法体现WPF的MVVM开发模式的优势, 于是,WPF提供了一个ICommand接口,以及一个强大的命令系统,将控件的各种事件都能转换成一个命令。这个命令依然像绑定依赖属性一样,将命令定义成一个ViewModel中的命令属性,而ViewModel又被赋值到控件或窗体的DataContext数据上下文中,于是,窗体内的所有控件的事件都可以绑定这个命令,只要控件的事件被触发,命令将会被执行。
注:所有的集合控件(item)不支持command命令
ICommandSource命令源
ICommandSource其实是一个接口,只有3个属性,分别是Command,CommandParameter 和CommandTarget 。
Command就是在调用命令源时执行的命令。
CommandParameter 表示可在执行命令时传递给该命令的用户定义的数据值。
CommandTarget 表示在其上执行该命令的对象。
所以,假如我们定义了一个叫OpenCommand的命令,并且这个OpenCommand是某个ViewModel中的属性,那么,我们的按钮就可以实现下面这样的写法。
<Grid>
<Button Content="打开"
Click="Button_Click"
Command="{Binding OpenCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}"/>
</Grid>
ICommand接口
这个接口比较关键的是CanExecute和Execute两个方法成员。前者表示当前命令是否可以执行,如果可以的话,WPF命令系统会自动帮我们去调用Execute方法成员。
ICommand的实现
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private Action action;
public RelayCommand(Action action)
{
this.action = action;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
action?.Invoke();
}
}
在上面的例子中,我们自定义了一个叫RelayCommand的Command类,非常重要的一点是,它的构造函数要求传入一个Action,这个委托传进来后,将来在Execute成员中被执行。
接下来,我们看看它的具体使用。
public class MainViewModel : ObservableObject
{
public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
{
MessageBox.Show("Hello,Command");
});
}
前端代码
<Window x:Class="HelloWorld.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:HelloWorld"
xmlns:forms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
mc:Ignorable="d" FontSize="14"
Title="WPF中文网 - 命令 - www.wpfsoft.com" Height="350" Width="500">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
</Grid>
</Window>
ICommand带参数的实现
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private Action action;
private Action<object> objectAction;
public RelayCommand(Action action)
{
this.action = action;
}
public RelayCommand(Action<object> objectAction)
{
this.objectAction = objectAction;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
action?.Invoke();
objectAction?.Invoke(parameter);
}
}
在这里,我们增加了一个带Action<object>参数的构造函数,将来定义命令时,就可以将一个带有object参数的方法传到这个RelayCommand中来。
MainViewModel代码如下
public class MainViewModel : ObservableObject
{
public RelayCommand OpenCommand { get; set; } = new RelayCommand(() =>
{
MessageBox.Show("Hello,Command");
});
public RelayCommand OpenParamCommand { get; set; } = new RelayCommand((param) =>
{
MessageBox.Show(param.ToString());
});
}
前端代码
<StackPanel VerticalAlignment="Center">
<Button Width="100" Height="30" Content="打开" Command="{Binding OpenCommand}" />
<Button Width="100" Height="30"
Content="打开"
Command="{Binding OpenParamCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}"/>
</StackPanel>
CommandBinding命令绑定:
第一步,实例化一个RoutedUICommand 命令
<Window.Resources>
<RoutedUICommand x:Key="PlayCommand" Text="Open"/>
</Window.Resources>
第二步,实例化一个CommandBinding对象
<Window.CommandBindings>
<CommandBinding Command="{StaticResource PlayCommand}"
Executed="CommandBinding_Executed"
CanExecute="CommandBinding_CanExecute"/>
</Window.CommandBindings>
这里需要定义两个回调函数。
private void CommandBinding_Executed(object sender,ExecutedRoutedEventArgs e){
MessageBox.Show("我是ALT+S");
}
private void CommandBinding_Executed(object sender,CanExecuteRoutedEventArgs e){
e.CanExecute = true;
}
第三步,调用PlayCommand命令
<StackPanel VerticalAlignment="Center">
<Button Width="100" Height="30"
Content="播放" Margin="10"
Command="{StaticResource PlayCommand}" />
</StackPanel>
除了通过控件的Command属性去绑定PlayCommand命令,还有没有别的方式呢?有的!比如我们可以通过MouseBinding或者KeyBinding去绑定一个命令。
<Window.InputBindings>
<!--鼠标+ctrl键触发command-->
<MouseBinding Gesture="Control+WheelClick" Command="{StaticResource PlayCommand}"/>
<!--快捷键触发command-->
<KeyBinding Gesture="Alt+S" Command="{StaticResource PlayCommand}"/>
</Window.InputBindings>
ApplicationCommands命令
预定义命令的使用
第一点,通过CommandBinding对象去关联一个Command
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open"
CanExecute="OpenCommandCanExecute"
Executed="OpenCommandExecuted" />
<CommandBinding Command="ApplicationCommands.Cut"
CanExecute="CutCommandCanExecute"
Executed="CutCommandExecuted" />
<CommandBinding Command="ApplicationCommands.Paste"
CanExecute="PasteCommandCanExecute"
Executed="PasteCommandExecuted" />
<CommandBinding Command="ApplicationCommands.Save"
CanExecute="SaveCommandCanExecute"
Executed="SaveCommandExecuted" />
</Window.CommandBindings>
第二点,如何编写命令的业务代码
private void OpenCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void OpenCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
var openFileDialog = new Microsoft.Win32.OpenFileDialog()
{
Filter = "文本文档 (.txt)|*.txt",
Multiselect = true
};
var result = openFileDialog.ShowDialog();
if (result.Value)
{
textbox.Text = File.ReadAllText(openFileDialog.FileName);
}
}
private void CutCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = textbox != null && textbox.SelectionLength > 0;
}
private void CutCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
textbox.Cut();
}
private void PasteCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = Clipboard.ContainsText();
}
private void PasteCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
textbox.Paste();
}
private void SaveCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = textbox != null && textbox.Text.Length > 0;
}
private void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
var saveFileDialog = new Microsoft.Win32.SaveFileDialog
{
Filter = "文本文档 (.txt)|*.txt"
};
if (saveFileDialog.ShowDialog() == true)
{
File.WriteAllText(saveFileDialog.FileName , textbox.Text);
}
}
第三点,命令如何绑定到命令源对象
<Window.InputBindings>
<KeyBinding Key="O" Modifiers="Control" Command="ApplicationCommands.Open" />
<KeyBinding Key="X" Modifiers="Control" Command="ApplicationCommands.Cut" />
<KeyBinding Key="V" Modifiers="Control" Command="ApplicationCommands.Paste" />
<KeyBinding Key="S" Modifiers="Control" Command="ApplicationCommands.Save" />
</Window.InputBindings>
这些KeyBinding可以定义快捷键,并指向某个命令。第二种方式就是创建一个前端控件,比如实例化一个按钮,利用按钮的Command绑定命令。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Content="打开" Margin="5" Command="ApplicationCommands.Open"/>
<Button Content="剪切" Margin="5" Command="ApplicationCommands.Cut"/>
<Button Content="粘贴" Margin="5" Command="ApplicationCommands.Paste"/>
<Button Content="保存" Margin="5" Command="ApplicationCommands.Save"/>
</StackPanel>
<TextBox x:Name="textbox" Grid.Row="1" Margin="5" TextWrapping="Wrap">
</TextBox>
</Grid>
WPF事件转Command命令
通过前面的学习,我们发现Button拥有Command属性,开发者可以为其设置一条命令,当用户单击按钮的时候便执行这条命令。但是,一个控件往往不止一个事件,比如UIElement基类中便定义了大量的事件,PreviewMouseDown表示鼠标按下时引发的事件。
如何在PreviewMouseDown事件触发时去执行一条命令呢?这时候就需要用到WPF提供的一个组件,它的名字叫Microsoft.Xaml.Behaviors.Wpf,我们可以在nuget管理器中找到并下载安装它。
然后,我们在window窗体中引入它的命名空间。
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
最后,我们以TextBox为例,因为TextBox也是UIElement基类的子类控件,所以,它也有PreviewMouseDown事件。
<TextBox x:Name="textbox" Grid.Row="1" Margin="5" TextWrapping="Wrap">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseDown">
<i:InvokeCommandAction Command="{Binding MouseDownCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=TextBox}}">
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
从上面的例子中,我们会发现,在TextBox 控件中增加了一个Interaction.Triggers附加属性,这个属性是一个集合,表示可以实例化多个Trigger触发器,于是,我们实例化了一个EventTrigger ,并指明它的EventName为PreviewMouseDown事件,关联的命令要写在InvokeCommandAction对象中,命令绑定的方式采用Binding即可。然后我们来看看MouseDownCommand的执行代码:
public RelayCommand<TextBox> MouseDownCommand { get; set; } = new RelayCommand<TextBox>((textbox) =>
{
textbox.Text += DateTime.Now + " 您单击了TextBox" + "\r";
});
Mvvmlight之RelayCommand
我们在前面的示例的MainViewModel中,新建一条命令。
public class MainViewModel : ObservableObject
{
public GalaSoft.MvvmLight.Command.RelayCommand<string> MvvmlightCommand { get; }
public MainViewModel()
{
MvvmlightCommand = new GalaSoft.MvvmLight.Command.RelayCommand<string>((message) =>
{
MessageBox.Show(message);
});
}
}
最后,我们在前端用一个Button来引用这个命令
<Button Content="mvvmlight"
Margin="5"
Command="{Binding MvvmlightCommand}"
CommandParameter="Hello,Mvvmlight"/>
Prism之DelegateCommand
打开nuget包管理器,搜索prism.unity关键词,下载Prism.Unity组件。
Prism框架提供了DelegateCommand、DelegateCommand<T>和CompositeCommand三种命令,分别是无参命令、有参命令和合并命令。
使用prism提供的命令分为两步,第一步定义命令,第二步调用命令。首先在C#后端的ViewModel中定义上述3种命令。
public class MainViewModel : ObservableObject
{
public DelegateCommand DelegateCommand { get; }
public DelegateCommand<string> ParamCommand { get; }
public CompositeCommand CompositeCommand { get; }
public GalaSoft.MvvmLight.Command.RelayCommand<string> MvvmlightCommand { get; }
public MainViewModel()
{
DelegateCommand = new DelegateCommand(() =>
{
MessageBox.Show("无参的DelegateCommand");
});
ParamCommand = new DelegateCommand<string>((message) =>
{
MessageBox.Show(message);
});
CompositeCommand = new CompositeCommand();
CompositeCommand.RegisterCommand(DelegateCommand);
CompositeCommand.RegisterCommand(ParamCommand);
MvvmlightCommand = new GalaSoft.MvvmLight.Command.RelayCommand<string>((message) =>
{
MessageBox.Show(message);
});
}
}
前端用三个按钮分别绑定这3个命令。
<Button Content="prism无参数"
Margin="5"
Command="{Binding DelegateCommand}"
CommandParameter="Hello,Prism"/>
<Button Content="prism有参数"
Margin="5"
Command="{Binding ParamCommand}"
CommandParameter="Hello,Prism"/>
<Button Content="prism合并命令"
Margin="5"
Command="{Binding CompositeCommand}"
CommandParameter="Hello,Prism"/>
ReactiveUI之ReactiveCommand
ReactiveUI是一个可组合的跨平台模型 - 视图 - 视图模型框架,适用于所有.NET平台,受功能性反应式编程的启发。它允许您在一个可读位置围绕功能表达想法,从用户界面抽象出可变状态,并提高应用程序的可测试性
ReactiveCommand示例:
首先,我们创建一个MainViewModel,并在其中声明一些命令。
internal class MainViewModel:ReactiveObject
{
public ICommand GeneralCommand { get; }
public ICommand ParameterCommand { get; }
public ICommand TaskCommand { get; }
public ICommand CombinedCommand { get; }
public ReactiveCommand<Unit,DateTime> ObservableCommand { get; }
public MainViewModel()
{
GeneralCommand = ReactiveCommand.Create(General);
ParameterCommand = ReactiveCommand.Create<object, bool>(Parameter);
TaskCommand = ReactiveCommand.CreateFromTask(RunAsync);
var childCommand = new List<ReactiveCommandBase<Unit,Unit>>();
childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) =>
{
MessageBox.Show("childCommand1");
return Unit.Default;
}));
childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) =>
{
MessageBox.Show("childCommand2");
return Unit.Default;
}));
childCommand.Add(ReactiveCommand.Create<Unit, Unit>((o) =>
{
MessageBox.Show("childCommand3");
return Unit.Default;
}));
CombinedCommand = ReactiveCommand.CreateCombined(childCommand);
ObservableCommand = ReactiveCommand.CreateFromObservable<Unit, DateTime>(DoObservab
ObservableCommand.Subscribe(v => ShowObservableResult(v));
}
private void RunInBackground()
{
throw new NotImplementedException();
}
private IObservable<DateTime> DoObservableCommand(Unit arg)
{
//todo 业务代码
var result = DateTime.Now;
return Observable.Return(result).Delay(TimeSpan.FromSeconds(1));
}
private void ShowObservableResult(DateTime v)
{
MessageBox.Show($"时间:{v}");
}
private async Task RunAsync()
{
await Task.Delay(3000);
}
private bool Parameter(object arg)
{
MessageBox.Show(arg.ToString());
return true;
}
private void General()
{
MessageBox.Show("ReactiveCommand!");
}
}
在这个示例中,并分演示了ReactiveCommand的普通命令、带参命令、Task命令、合并命令和观察者命令的用法。接下来创建XAML前端控件对象,将这些命令绑定到Button上面。
<Window x:Class="HelloWorld.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:HelloWorld"
mc:Ignorable="d"
Title="WPF从小白到大佬 - 命令" Height="350" Width="500">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<StackPanel>
<TextBlock Text="ReactiveUI之ReactiveCommand课程" FontSize="28" Margin="5"/>
<StackPanel Orientation="Horizontal">
<Button Margin="5" Content="普通命令" Command="{Binding GeneralCommand}"/>
<Button Margin="5" Content="参数命令" Command="{Binding ParameterCommand}"
CommandParameter="Hello,Parameter"/>
<Button Margin="5" Content="子线程命令" Command="{Binding TaskCommand}"/>
<Button Margin="5" Content="合并命令" Command="{Binding CombinedCommand}"/>
<Button Margin="5" Content="Observable命令" Command="{Binding ObservableCommand}"/>
</StackPanel>
</StackPanel>
</Window>