命令

事件驱动模式有一个不太好的地方是,它的前端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>