WPF入门到跪下 第九章 MVVM-行为处理

MVVM模式下的行为处理

一、命令

命令是指特定指令、有明确的执行内容且具有一定的强制性。

命令VS事件
命令与控件的事件看起来有类似的地方,但是两者是不同的。

  • 控件的事件触发函数是在对应窗体的后台代码中进行定义的,仔细查阅窗体的后台代码,能发现这是一个部分类,也就是编译过后后台代码与XAML合成同一个类的。这样一来控件事件的整个触发、处理流程一直都是在窗体对象中进行的(也就是View层)。
  • 命令能通过控件的Command属性进行命令属性的绑定,从而让View层与业务逻辑进行分离。

命令的用途
第一个目的是将语义和调用命令的对象与执行命令的逻辑分开。这允许多个和不同源调用相同的命令逻辑,并允许针对不同的目标自定义命令逻辑。

另一个用途是统一的指示操作是否可用,即根据业务逻辑对界面进行操作、控制。例如当登录界面用户名或密码为空时,登录按钮不可用。

二、自定义命令的实现

1、创建命令类型

创建CommandBase类型并实现ICommand接口和对应成员。

  • 在MVVM模式的项目框架中,命令类型不属于MVVM中的任意一层,所以可以新建一个Base来存放。(可以是程序集或文件夹)

ICommand中有三个必须实现的成员:

  • CanExecuteChanged:事件成员,触发该事件会执行一次CanExcute方法,然后根据该方法的返回结果,来决定绑定该命令的控件对象是否可用。也就是每当关键属性变化的时候可以通过触发此事件,来刷新一下控件对象的可用状态。
    • CanExecuteChanged的订阅(CanExcute),WPF会在运行中自动完成,不需要我们再做订阅。
  • bool CanExecute(object parameter):方法成员,返回结果决定对应的控件对象是否可用,一般通过触发CanExecuteChanged事件来调用。
    • parameter:通过控件的CommandParameter属性传来的参数。
  • void Execute(object parameter):命令的执行内容。
    • parameter:通过控件的CommandParameter属性传来的参数。

CommandBase代码

public class CommandBase : ICommand
{
    //事件成员,调用该事件会执行一次CanExcute方法,然后根据该方法的返回结果,来决定绑定该命令的控件对象是否可用
    //一般在关键数据进行变换时调用
    public event EventHandler CanExecuteChanged;
    //由于外界只能使用事件的订阅或取消订阅,无法直接调用事件,因此需要做函数封装
    public void DoCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
    
    public Func<object, bool> DoCanExecute { get; set; }
    /// 
    /// 根据返回值来决定绑定了该命令的控件是否可用,一般会通过CanExecuteChanged事件来调用
    /// 
    /// 
    /// 
    public bool CanExecute(object parameter)
    {
        //直接使用对外提供的委托,由外界决定控件对象是否可用的判定逻辑
        return DoCanExecute?.Invoke(parameter) == true;
    }

    public Action<object> DoExecute { get; set; }
    /// 
    /// 命令的执行内容
    /// 
    /// 
    public void Execute(object parameter)
    {
        //直接使用对外提供的委托,由外界决定命令的执行内容
        DoExecute?.Invoke(parameter);
    }
}

CommandBase简易版

class BaseCommand : ICommand
{
    private Action<object?>? _doExecute;

    public BaseCommand(Action<object?> doExecute)
    {
        _doExecute = doExecute;
    }

    public event EventHandler? CanExecuteChanged;

    public bool CanExecute(object? parameter)
    {
        return true;
    }

    public void Execute(object? parameter)
    {
        _doExecute?.Invoke(parameter);
    }
}

2、Model层中定义命令属性

在对应的数据模型中,定义命令属性、命令的执行内容、命令所对应控件是否可用的判定条件、CanExecuteChanged事件的调用时机。

关于命令部分,其实是可以放到ViewModel层中来实现的,这里为了方便就直接在Model中实现了。

MainModel

public class MainModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private double _value1 = 0;

    public double Value1
    {
        get { return _value1; }
        set
        {
            _value1 = value;
            Command1.DoCanExecuteChanged();
        }
    }

    private double _value2 = 0;

    public double Value2
    {
        get { return _value2; }
        set
        {
            _value2 = value;
            Command2.DoCanExecuteChanged();
        }

    }

    private double _value3;

    public double Value3
    {
        get { return _value3; }
        set
        {
            _value3 = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value3"));
        }
    }

    public CommandBase Command1 { get; set; }
    public CommandBase Command2 { get; set; }
    public MainModel()
    {
        Command1 = new CommandBase
        {
            DoExecute = Execute1,
            DoCanExecute = CanExecute1
        };
        Command2 = new CommandBase
        {
            DoExecute = Execute2,
            DoCanExecute = CanExecute2
        };
    }
    
    private void Execute1(object obj) {
        //这里进行命令执行内容的定义......
        Value3 = Value1 + Value2;
        //执行完后,要调用一下命令事件,重新确认控件是否可用
        Command1.DoCanExecuteChanged();
    }

    private void Execute2(object obj) {
        //这里进行命令执行内容的定义......
        Value3 = Value1 + Value2;
        //执行完后,要调用一下命令事件,重新确认控件是否可用
        Command1.DoCanExecuteChanged();
    }

    private bool CanExecute1(object obj)
    {
        return Value1 != 0;
    }
    private bool CanExecute2(object obj)
    {
        return Value2 != 0;
    }

}

3、ViewModel层定义模型对象

public class MainViewModel
{
    public MainModel mainModel { get; set; } = new MainModel();
}

4、View层中使用命令

<Window.DataContext>
    <vm:MainViewModel/>
</Window.DataContext>
<Grid>
    <StackPanel>
        <TextBox Text="{Binding mainModel.Value1, UpdateSourceTrigger=PropertyChanged}"/>
        <Slider Value="{Binding mainModel.Value2}"/>
        <TextBlock Text="{Binding mainModel.Value3}"/>
        <Button Content="CommanTest1" Command="{Binding mainModel.Command1}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
        <Button Content="CommanTest2" Command="{Binding mainModel.Command2}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
    </StackPanel>
</Grid>

三、全局命令状态更新

在上面的自定义命令的实现示例中可以看到,命令对象的可用状态的更新操作是十分重要的(也就是CanExecuteChanged事件的触发)。但是,如果只能按照示例中的用法,通过命令对象去触发对象中的CanExecuteChanged事件的话,难免会造成层次之间的耦合,有没有什么办法可以将CanExecuteChanged事件剥离出来,不需要命令对象就能随时随地的去触发呢?此时就需要使用CommandManager类型了。

CommandManager类型用于在全局范围内对命令的可用状态更新进行管理。

CommandManager.RequerySuggested:命令对象事件成员CanExecuteChanged的挂载事件。

  • 可以在创建命令对象时,将CanExecuteChanged事件挂载在CommandManager.RequerySuggested事件上。
  • RequerySuggested事件的触发条件是 WPF 内置的,WPF 内置的触发条件会导致多次调用CanExecute
  • 此外,通过调用CommandManager.InvalidateRequerySuggested()可主动触发一次RequerySuggested事件,但必须在UI线程。
  • 自定义命令不建议使用RequerySuggested

CommandManager.InvalidateRequerySuggested():主动触发CommandManager.RequerySuggested事件。

  • 这里需要注意的是,如果RequerySuggested中挂载了多个对象的CanExecuteChanged事件,那么一旦调用InvalidateRequerySuggested()方法将会全部触发。

自定义命令实现全局命令状态更新

自定义命令的CanExecuteChanged事件挂载:

public event EventHandler CanExecuteChanged
{
		add
    {
		    CommandManager.RequerySuggested += value;
    }
    remove
    {
		    CommandManager.RequerySuggested -= value;
    }
}

在需要更新命令可用状态的地方直接调用CommandManager.InvalidateRequerySuggested()主动触发RequerySuggested事件。

private void Execute1(object obj) {
    //这里进行命令执行内容的定义......
    Value3 = Value1 + Value2;
    //执行完后,要调用一下命令事件,重新确认控件是否可用
    CommandManager.InvalidateRequerySuggested();
}

实际上,RequerySuggested事件的触发条件是 WPF 内置的,所以即使不去调用InvalidateRequerySuggested()大多数情况下也是可以的,但是WPF内置的触发条件会导致多次调用CanExecute,因此自定义命令不建议使用RequerySuggested

四、内置命令(了解)

WPF预定了一些命令及相关操作,方便我们在开发过程中快速的实现控件的行为处理。

1、常见内置命令

媒体命令(共24个)

MediaCommands.PlayMediaCommands.StopMediaCommands.Pause、…….

应用命令(共23个)

ApplicationCommands.NewApplicationCommands.OpenApplicationCommands.CopyApplicationCommands.CutApplicationCommands.Print、………

导航命令(共16个)

NavigationCommands.GoToPageNavigationCommands.LastPageNavigationCommands.Favorites、……

联合命令(共27个)

ComponentCommands.ScrollByLineComponentCommands.MoveDownComponentCommands.ExtendSelectionDown、……

编辑命令(共54个)

EditingCommands.DeleteEditingCommands.ToggleUnderlineEditingCommands.ToggleBold、……

2、内置命令的使用

内置命令的使用主要分以下几个步骤:

绑定内置命令

通过控件的Command属性绑定内置命令

<Button Command="ApplicationCommands.Open" ....../>

定义命令内容

在控件中,通过设置CommandManager的相关属性完成命令的执行内容以及命令可用判定条件的定义。

CommandManager.Executed:当前控件命令的执行内容。

CommandManager.CanExecute:当前控件命令的可用判定条件。

<Button Command="ApplicationCommands.Open"
        CommandManager.Executed="Button_Executed"
        CommandManager.CanExecute="Button_CanExecute"
        Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"/>
private bool flag = true;
private void Button_Executed(object sender, ExecutedRoutedEventArgs e)
{
		MessageBox.Show("命令被触发了");
    flag = false;
}

private void Button_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = flag;
}

在使用时候发现以下几点需要注意的地方:

  • 绑定了内置命令后可以通过Command.Text属性获得内置命令对应的文本内容,这个好处是可以根据运行环境进行语言切换的。
  • 内置命令具有快捷键,比如ApplicationCommands.Open的快捷键是Ctrl+o,快捷键会执行命令,但传给执行函数的sender参数为窗体对象。
  • 不需要显示触发状态更新事件,例如上文中自定义命令时,是需要去触发CanExecuteChanged事件来更新命令的可用状态的。但这里是通过CommandManager来对命令进行管理的,WPF会自动触发去触发命令状态更新事件。当然,我们也可以通过CommandManager.InvalidateRequerySuggested()来进行主动触发。但由于WPF对于命令可用状态更新事件的触发是很频繁的,基本上没必要。

3、内置命令业务的统一定义

在上面例子中,虽然可以在控件中通过CommandManager来对命令的执行内容、判定条件进行定义,但如果由很多个控件使用了内置命令的话,一个个去定义会显得很繁杂。WPF为此提供了解决方案,可以通过容器元素的CommandBindings属性,对同一类型的内置命令进行统一的逻辑定义,使得在作用域范围内的命令都使用都一套业务逻辑。

<Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Open" Executed="Button_Executed" CanExecute="Button_CanExecute"/>
Window.CommandBindings>
<Grid>
    <StackPanel>
        <Button Command="ApplicationCommands.Open"
                Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"
        />
        <Button Command="ApplicationCommands.Open"
                Content="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}"
        />
    StackPanel>
Grid>

上面是通过Window元素进行ApplicationCommands.Open命令的统一,如果希望缩小作用域范围也可以通过来进行统一定义。

4、内置命令的终止

内置命令实质上是路由命令,普通的路由事件的消息传递是一样的,有隧道跟冒泡机制,可以通过设置Hendled属性进行命令的终止。

private void Button_Executed(object sender, ExecutedRoutedEventArgs e)
{
    ......
    e.Handled = true;
}

5、使用内置命令自带业务

在使用过程中发现大部分的内置业务是需要自己重新做业务定义的,但也有个别内置命令自带的业务也挺好用的,比如ApplicationCommands.Copy,其自带业务是,当文本框绑定该命令时,如果用光标选中文本内容,则该命令对应控件为可用状态,执行命令则将选中内容进行复制。

<TextBox Name="tb"/>
<Button Command="ApplicationCommands.Copy" CommandTarget="{Binding ElementName=tb}"
        Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>

除了Copy外,比较好用的内置命令还有CutPaste等等。

五、路由命令

查阅内置命令的源码,可以看到两种路由命令类型:RoutedUICommandRoutedCommand

其实RoutedUICommand就是在继承了RoutedCommand的基础上新增了Text属性。

自定义路由命令

在xaml后台代码中定义对应属性:

public partial class MainWindow : Window
{
		......
		public RoutedUICommand MyRoutedCommand { get; set; }
		public MainWindow()
		{
		    InitializeComponent();
				//快捷键组合
		    InputGestureCollection inputGestureCollection = new InputGestureCollection()
		    {
		        new KeyGesture(Key.T, ModifierKeys.Alt)
		    };
		    MyRoutedCommand = new RoutedUICommand("textContent", "commandName", typeof(MainWindow),inputGestureCollection);
		}
		......
}

在xaml中使用自定义路由命令

<Button Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor},Path=MyRoutedCommand}"
        CommandManager.Executed="Button_My_Executed"
        CommandManager.CanExecute="Button_My_CanExecute"
        Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>

你可能感兴趣的:(WPF,wpf)