命令和路由命令

WPF中的命令是一种机制,可以将用户界面中的输入操作(如按钮点击、菜单选择等)与应用程序逻辑解耦。命令比直接事件处理器更灵活,尤其在处理多个控件共享相同逻辑或启用/禁用控件时特别有用。
WPF命令包括预定义命令和自定义命令。

  1. 命令可以将业务的需求和执行逻辑解耦,使不同来源可以调用相同的命令逻辑,并针对不同目标自定义该逻辑。
    例如,编辑操作如“复制”、“剪切”和“粘贴”可以通过按钮、菜单项或组合键调用相同的命令逻辑。
  2. 命令可指示操作是否可用。例如,“剪切”操作只有在选择内容时才可执行,命令通过CanExecute方法来确定操作是否可行,并通过CanExecuteChanged事件通知 UI 更新状态。尽管命令的语义一致,操作逻辑由目标对象定义,不同类型的对象在执行命令时有不同的处理方式

命令本身:

WPF中的命令由ICommand接口定义,该接口包括以下成员:

  • Execute:命令调用时执行什么操作
  • CanExecute:确定命令是否可以执行的方法。
  • CanExecuteChanged:当命令的执行状态发生变化时引发的事件。

使用RelayCommand,RelayCommod是我们自定义的一个ICommand的实现,里面内容也非常简单,就是常规的Command

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

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

然后,在ViewModel中使用这个命令作为成员:
(注意只能作为属性,如果作为字段会识别不到)

public class MainViewModel
{
    public ICommand MyCustomCommand { get; private set; }

    private string _textBoxContent;
    public string TextBoxContent
    {
        get { return _textBoxContent; }
        set
        {
            _textBoxContent = value;
            OnPropertyChanged();
        }
    }

    public MainViewModel()
    {
        MyCustomCommand = new RelayCommand(ExecuteMyCustomCommand, CanExecuteMyCustomCommand);
    }

    private void ExecuteMyCustomCommand(object parameter)
    {
        MessageBox.Show("Custom command executed: " + TextBoxContent);
    }

    private bool CanExecuteMyCustomCommand(object parameter)
    {
        return !string.IsNullOrEmpty(TextBoxContent);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

最后,在XAML中绑定命令:

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp"
        Title="MainWindow" Height="200" Width="300">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding TextBoxContent, UpdateSourceTrigger=PropertyChanged}" Width="200" Height="30" />
        <Button Content="Custom Command" Width="100" Height="30" Command="{Binding MyCustomCommand}" />
    </StackPanel>
</Window>

对于按钮,单击就能执行命令了,但是对于其他控件可能不一定,如ListBox里面的Item,单击是选中控件,所以需要双击才能执行

还可以将命令的参数传进来

  <Button Content="Custom Command" Width="100" Height="30" Command="{Binding MyCustomCommand}"  CommandParameter = "False"/>
    private void ExecuteMyCustomCommand(object parameter)
    {
        MessageBox.Show(parameter);
    }
    private bool CanExecuteMyCustomCommand(object parameter)
    {
        return (bool)parameter;
    }

WPF命令系统组成

  • 命令本身 即希望执行的操作
  • 命令源 调用命令的对象
  • 命令目标 执行命令的对象
  • 命令绑定 将命令绑定到具体的行动上,将命令与执行该命令的逻辑关联起来
    将某个命令绑定到特定控件(如按钮、菜单项等),使该控件在用户触发相应操作时执行绑定的命令逻辑

命令源

WPF 中的命令源通常实现 ICommandSource接口,有三个属性Command,CommandTarget, 和 CommandParameter

  • Command是在调用命令源时执行的命令。
  • CommandTarget 是要执行命令的对象。 值得注意的是,在 WPF 中,仅当 ICommand为 RoutedCommand 时,ICommandSource 上的 CommandTarget属性才适用。(如果不是路由命令的话,也就无所谓命令目标了,所以不是RoutedCommand的话哪怕设置了也会忽略)
    如果未设置 CommandTarget,则具有键盘焦点的元素将成为命令目标。
  • CommandParameter 是用于将信息传递给实现命令的处理程序的用户定义数据类型。

命令源将侦听 CanExecuteChanged事件。 结合 CanExecute方法可以查询 Command当前能否执行。 如果命令无法执行,命令源可禁用自身。 比如 MenuItem,在命令无法执行时,它自身将灰显。

实现 ICommandSource 的 WPF 类是 ButtonBaseMenuItemHyperlinkInputBinding

RoutedCommand

RoutedCommand是WPF里面ICommand的特殊实现
RoutedCommand的 Execute和 CanExecute 方法不包含该命令的应用程序逻辑,而是引发冒泡路由事件,注意不会向下传播,引发的起点是CommandTarget ,直到遇到具有 CommandBinding的对象。 CommandBinding 包含这些事件的处理程序,命令正是由这些处理程序执行。一旦命令在某个控件上成功执行,传播将停止。
可能你会觉得:前面的ICommand接口明明说的是,Execute是命令的执行逻辑,但是这里的RoutedCommand的Execute不包含执行逻辑,这样是不是不符合接口的设计
但是RoutedCommand本身不只是Command,还是Routed,也就是兼顾了路由的功能,那么作为一个路由命令,他的Execute本身的目的就是路由,找到目标命令并执行。
从代码的逻辑上讲,作为一个ICommand,RoutedCommand触发后应该调用它的Execute方法,事实上也这样,这样一想是不是还挺合理的

<StackPanel>
  <Menu>
    <MenuItem Command="ApplicationCommands.Paste" />
  </Menu>
  <TextBox />
</StackPanel>

所以这段代码只要一开始鼠标在文本框里面,就能粘贴(因为没有显示设置target,所以target是焦点)

CommandBinding

CommandBinding类包含 Command属性,及 PreviewExecuted、Executed、PreviewCanExecute和 CanExecute事件。

预定义命令

WPF提供了一组预定义命令,如剪切、复制、粘贴、撤销、重做等。这些命令定义在ApplicationCommandsNavigationCommandsMediaCommandsComponentCommands类中。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Copy" Executed="CopyCommand_Executed" CanExecute="CopyCommand_CanExecute" />
    </Window.CommandBindings>
    <StackPanel>
        <TextBox x:Name="textBox" Width="200" Height="30" />
        <Button Command="ApplicationCommands.Copy" Content="Copy" Width="100" Height="30" />
    </StackPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void CopyCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = !string.IsNullOrEmpty(textBox.Text);
    }

    private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        Clipboard.SetText(textBox.Text);
    }
}

绑定了ApplicationCommands.Copy命令到一个按钮和一个文本框。CanExecute方法确保只有在文本框中有文本时,复制按钮才是可用的。

自定义命令

有时预定义命令无法满足需求,我们需要定义自己的命令。可以使用RoutedCommand或实现ICommand接口来创建自定义命令。

示例:使用RoutedCommand定义自定义命令

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:MainWindow.MyCustomCommand}" Executed="MyCustomCommand_Executed" CanExecute="MyCustomCommand_CanExecute" />
    </Window.CommandBindings>
    <StackPanel>
        <TextBox x:Name="textBox" Width="200" Height="30" />
        <Button Command="{x:Static local:MainWindow.MyCustomCommand}" Content="Custom Command" Width="100" Height="30" />
    </StackPanel>
</Window>
public partial class MainWindow : Window
{
    public static readonly RoutedCommand MyCustomCommand = new RoutedCommand();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void MyCustomCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = !string.IsNullOrEmpty(textBox.Text);
    }

    private void MyCustomCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("Custom command executed: " + textBox.Text);
    }
}

在这个示例中,我们定义了一个名为MyCustomCommandRoutedCommand并在XAML中绑定到按钮。CanExecute方法和Executed方法分别定义了命令的执行条件和执行逻辑。(相当于直接重新赋值了RoutedCommand的CanExecute方法和Executed方法)

示例:在提交时批量验证
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;

public class UserViewModel : INotifyPropertyChanged, IDataErrorInfo
{
    private string _name;
    private int _age;

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged(nameof(Age));
        }
    }

    public ICommand SubmitCommand { get; }

    public UserViewModel()
    {
        SubmitCommand = new RelayCommand(Submit, CanSubmit);
    }

    private void Submit()
    {
        // 提交逻辑
        System.Windows.MessageBox.Show("Data Submitted!");
    }

    private bool CanSubmit()
    {
        // 检查是否可以提交的逻辑
        return !string.IsNullOrWhiteSpace(Name) && Age >= 0 && Age <= 120;
    }

    public string Error => null;

    public string this[string columnName]
    {
        get
        {
            if (columnName == nameof(Name) && string.IsNullOrWhiteSpace(Name))
                return "Name is required.";
            if (columnName == nameof(Age) && (Age < 0 || Age > 120))
                return "Age must be between 0 and 120.";
            return null;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

<Window x:Class="WpfApp.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"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Grid.BindingGroup>
            <BindingGroup Name="UserBindingGroup"/>
        </Grid.BindingGroup>

        <StackPanel>
            <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, BindingGroup=UserBindingGroup}" Width="200" Margin="10"/>
            <TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, BindingGroup=UserBindingGroup}" Width="200" Margin="10"/>
            <Button Content="Submit" Width="100" Margin="10" Command="{Binding SubmitCommand}"/>
        </StackPanel>
    </Grid>
</Window>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容