2022-01-15 wpf Button命令模型

关联 Command 对象的 按钮的 启用和禁用状态 是由什么来决定的?

通过查看 wpf 的 源码得到以下结论:
ICommand 中定义的 CanExecuteChanged 由命令源 ButtonBase 使用(ButtonBase定义了这个事件处理方法 OnCanExecuteChanged
OnCanExecuteChanged 这个方法中,经过一系列调用 会调用 ICommand 定义的 CanExecute方法,
获取到的返回值 去设置 控件的启用还是禁用的状态。

最终结论:
要想 通过 命令 改变 命令关联的按钮的 启用禁用状态,就需要 有人 去调用 CanExecuteChanged 这个事件,谁去调用它?
在 [Windows Community Toolkit] RelayCommand 中 提供了 一个 NotifyCanExecuteChanged 方法,要自己调用这个方法,来触发按钮状态的改变

RoutedCommand 中 的 CanExecuteChanged 是 转交给 CommandManager.RequerySuggested 执行的
即 :由 CommandManager 自动调用 RoutedCommand 中 的 CanExecuteChanged 事件,来调整按钮的禁用和启用状态

// routedCommand 中的 CanExecuteChanged 事件
public event EventHandler CanExecuteChanged
{
    add
    {
        CommandManager.RequerySuggested += value;
    }
    remove
    {
        CommandManager.RequerySuggested -= value;
    }
}

详细分析:

wpf 命令模型 最重要的 一个组件 ICommand 接口,定义如下

namespace System.Windows.Input
{
    // Token: 0x02000008 RID: 8
    [NullableContext(2)]
    [TypeForwardedFrom("PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    [TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    [ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    public interface ICommand
    {
        // Token: 0x14000001 RID: 1
        // (add) Token: 0x0600001B RID: 27
        // (remove) Token: 0x0600001C RID: 28
        event EventHandler CanExecuteChanged; (在命令中定义,谁用它来关联事件程序)

        // Token: 0x0600001D RID: 29
        bool CanExecute(object parameter); (由谁调用)

        // Token: 0x0600001E RID: 30
        void Execute(object parameter);
    }
}

Button的基类 ButtonBase 类中实现的 ICommandSource 中的 Command,Command 是 一个依赖属性

[Bindable(true)]
[Category("Action")]
[Localizability(LocalizationCategory.NeverLocalize)]
public ICommand Command
{
    get
    {
        return (ICommand)base.GetValue(ButtonBase.CommandProperty);
    }
    set
    {
        base.SetValue(ButtonBase.CommandProperty, value);
    }
}

这个Command 依赖属性 注册了 一个 属性改变时的回调函数 OnCommandChanged,当设置按钮的Command属性时,会调用这个函数
ButtonBase.CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged)));

static ButtonBase()
{
    ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
    ButtonBase.CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged)));
    ButtonBase.CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(ButtonBase), new FrameworkPropertyMetadata(null));
    ButtonBase.CommandTargetProperty = DependencyProperty.Register("CommandTarget", typeof(IInputElement), typeof(ButtonBase), new FrameworkPropertyMetadata(null));
    ButtonBase.IsPressedPropertyKey = DependencyProperty.RegisterReadOnly("IsPressed", typeof(bool), typeof(ButtonBase), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox, new PropertyChangedCallback(ButtonBase.OnIsPressedChanged)));
    ButtonBase.IsPressedProperty = ButtonBase.IsPressedPropertyKey.DependencyProperty;
    ButtonBase.ClickModeProperty = DependencyProperty.Register("ClickMode", typeof(ClickMode), typeof(ButtonBase), new FrameworkPropertyMetadata(ClickMode.Release), new ValidateValueCallback(ButtonBase.IsValidClickMode));
    EventManager.RegisterClassHandler(typeof(ButtonBase), AccessKeyManager.AccessKeyPressedEvent, new AccessKeyPressedEventHandler(ButtonBase.OnAccessKeyPressed));
    KeyboardNavigation.AcceptsReturnProperty.OverrideMetadata(typeof(ButtonBase), new FrameworkPropertyMetadata(BooleanBoxes.TrueBox));
    InputMethod.IsInputMethodEnabledProperty.OverrideMetadata(typeof(ButtonBase), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox, FrameworkPropertyMetadataOptions.Inherits));
    UIElement.IsMouseOverPropertyKey.OverrideMetadata(typeof(ButtonBase), new UIPropertyMetadata(new PropertyChangedCallback(Control.OnVisualStatePropertyChanged)));
    UIElement.IsEnabledProperty.OverrideMetadata(typeof(ButtonBase), new UIPropertyMetadata(new PropertyChangedCallback(Control.OnVisualStatePropertyChanged)));
}

静态 OnCommandChanged 方法 会 调用 当前按钮对象的实列方法 OnCommandChanged

// Token: 0x060079AA RID: 31146 RVA: 0x009DAA78 File Offset: 0x009D9C78
        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ButtonBase)d).OnCommandChanged((ICommand)e.OldValue, (ICommand)e.NewValue);
        }

        // Token: 0x060079AB RID: 31147 RVA: 0x009DAAA0 File Offset: 0x009D9CA0
        private void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
        {
            if (oldCommand != null)
            {
                this.UnhookCommand(oldCommand);
            }
            if (newCommand != null)
            {
                this.HookCommand(newCommand);
            }
        }

实列的OnCommandChanged方法 会进一步 调用 UnhookCommand 和 HookCommand,从原先的Command的CanExecuteChanged事件与按钮的事件处理方法 OnCanExecuteChanged取消关联,在将新Command的CanExecuteChanged事件与按钮的事件处理方法OnCanExecuteChanged关联。
这里 按钮对象 注册了 OnCanExecuteChanged 事件函数,就是 当 调用 命令的 CanExecuteChanged 事件时,就会执行 按钮的 OnCanExecuteChanged 事件函数

// Token: 0x060079AC RID: 31148 RVA: 0x009DAAB8 File Offset: 0x009D9CB8
        private void UnhookCommand(ICommand command)
        {
            CanExecuteChangedEventManager.RemoveHandler(command, new EventHandler<EventArgs>(this.OnCanExecuteChanged));
            this.UpdateCanExecute();
        }

        // Token: 0x060079AD RID: 31149 RVA: 0x009DAAD4 File Offset: 0x009D9CD4
        private void HookCommand(ICommand command)
        {
            CanExecuteChangedEventManager.AddHandler(command, new EventHandler<EventArgs>(this.OnCanExecuteChanged));
            this.UpdateCanExecute();
        }

OnCanExecuteChanged 方法接着会 调用 UpdateCanExecute 方法,这个方法会调用 CommandHelpers.CanExecuteCommandSource(this)

// Token: 0x060079AE RID: 31150 RVA: 0x009DAAF0 File Offset: 0x009D9CF0
        private void OnCanExecuteChanged(object sender, EventArgs e)
        {
            this.UpdateCanExecute();
        }

        // Token: 0x060079AF RID: 31151 RVA: 0x009DAAF8 File Offset: 0x009D9CF8
        private void UpdateCanExecute()
        {
            if (this.Command != null)
            {
                this.CanExecute = CommandHelpers.CanExecuteCommandSource(this);
                return;
            }
            this.CanExecute = true;
        }

CanExecuteCommandSource 内部调用了 Command的 CanExecute 方法

internal static bool CanExecuteCommandSource(ICommandSource commandSource)
        {
            ICommand command = commandSource.Command;
            if (command == null)
            {
                return false;
            }
            object commandParameter = commandSource.CommandParameter;
            IInputElement inputElement = commandSource.CommandTarget;
            RoutedCommand routedCommand = command as RoutedCommand;
            if (routedCommand != null)
            {
                if (inputElement == null)
                {
                    inputElement = (commandSource as IInputElement);
                }
                return routedCommand.CanExecute(commandParameter, inputElement);
            }
            return command.CanExecute(commandParameter);
        }

从CommandHelpers.CanExecuteCommandSource(this) 方法返回的值 设置了 CanExecute 属性,设置CanExecute 属性时 设置了 按钮是 禁用 还是启用

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

推荐阅读更多精彩内容