【问题标题】:MVVM ICommand.CanExecute parameter contains previous valueMVVM ICommand.CanExecute 参数包含以前的值
【发布时间】:2015-04-14 15:48:30
【问题描述】:

如果使用嵌套属性而不是普通属性,我很难理解为什么 ICommand.CanExecutes 总是包含以前的值而不是新值。

问题在下面描述,除了使用某种形式的“外观”模式之外,我真的无法找到解决此问题的方法,我在视图模型中创建属性并将它们挂钩到模型中的相应属性。

或者使用该死的 CommandManager.RequerySuggested 事件。这不是最佳的原因是因为视图显示了 30 多个命令,仅计算菜单,如果每次更改时所有 CanExecute 都更新,则所有菜单项/按钮都需要几秒钟才能更新。即使使用下面的示例,只有一个命令和按钮以及命令管理器,按钮也需要大约 500 毫秒来启用/禁用自身。

我能想到的唯一原因是在 CanExecute 被触发之前 CommandParameter 绑定没有更新,然后我想你对此无能为力。

提前致谢:!

例如

假设我们有这个基本的视图模型

public class BasicViewModel : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get { return name; }
        set {
            this.name = value;
            RaisePropertyChanged("Name");
            Command.RaiseCanExecuteChanged();
        }
    }

    private Project project;

    public Project Project
    {
        get { return project; }
        set {
            if (project != null) project.PropertyChanged -= ChildPropertyChanged;
            if (value != null) value.PropertyChanged += ChildPropertyChanged;

            project = value;
            RaisePropertyChanged("Project");
        }
    }

    private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) {
        Command.RaiseCanExecuteChanged();
    }

    public DelegateCommand<string> Command { get; set; }

    public BasicViewModel()
    {
        this.Project = new Example.Project();
        Command = new DelegateCommand<string>(this.Execute, this.CanExecute);
    }

    private bool CanExecute(string arg) {
        return !string.IsNullOrWhiteSpace(arg);
    }

    private void Execute(string obj) { }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName = null) {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

还有这个模型

public class Project : INotifyPropertyChanged
{
    private string text;

    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaisePropertyChanged("Text");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName = null)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

现在在我看来,我有这个文本框和按钮。

<Button Content="Button" CommandParameter="{Binding Path=Project.Text}" Command="{Binding Path=Command}" />
<TextBox Text="{Binding Path=Project.Text, UpdateSourceTrigger=PropertyChanged}" />

它有效,每次我在文本框中键入内容时都会调用 CanExecute,但参数始终设置为以前的值。假设我在文本框中写了“H”,CanExecute 被触发,参数设置为 NULL。接下来我写“E”,现在文本框包含“HE”,CanExecute 再次触发。这次仅将参数设置为“H”。

由于某种奇怪的原因,参数总是设置为以前的值,当我检查 Project.Text 时,它设置为“HE”,但参数仍设置为仅“H”。

如果我现在将命令参数更改为

CommandParameter="{Binding Path=Name}"

和 Textbox.Text 到

Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"

一切都很完美。 CanExecute 参数始终包含最新的值,而不是之前的值。

【问题讨论】:

  • 您应该将VM的所有示例代码都放在问题中,而不是引用pastebin等外部站点
  • @AwkwardCoder 更新了问题中的所有代码,我这样做的原因是为了使问题尽可能简短,但我知道很难理解这个问题。
  • 首先,您不需要在 ViewModel 中调用 Command.RaiseCanExecuteChanged(),绑定命令的重新评估应该会自动发生。
  • 其次,如果您将 TextBox 绑定到 ViewModel 中的属性,则不必使用 XAML 中的 CommandParameter 将参数传回,这是多余的,我要么不传递命令参数并绑定文本框或不绑定文本框并将文本值作为命令参数传递。
  • @AwkwardCoder 嗯,如果我直接将参数绑定到textbox.Text,那么我需要为TextChanged事件创建一个事件处理程序,然后每次文本更改时调用CanExecute,感觉太多了代码在后端,但我会尝试在不走 CommandManager 路线的情况下找出解决方案。

标签: c# wpf mvvm icommand delegatecommand


【解决方案1】:

您所说的外观模式是标准的 WPF 实践。您这样做的主要问题是,当引发事件时,它们订阅的事件处理程序会按照它们被订阅的顺序执行。您拥有的代码行:

        if (value != null) value.PropertyChanged += ChildPropertyChanged;

这订阅了“Project”类的“PropertyChanged”事件。您的 UIElements 也通过您在 XAML 中的绑定订阅了相同的“PropertyChanged”事件。简而言之,您的“PropertyChanged”事件现在有 2 个订阅者。

关于事件的事情是它们按顺序触发,而您的代码中发生的事情是,当事件从您的“Project.Text”触发时,它会执行您的“ChildPropertyChanged”事件,触发您的“CanExecuteChanged”事件,这最后运行你的“CanExecute”函数(当你看到不正确的参数时)。 然后,在那之后,你的 UIElements 让他们的 EventHandlers 被同一个事件执行。它们的值也会更新。

这是导致问题的订阅顺序。试试这个,告诉我它是否能解决你的问题:

public Project Project
{
    get { return project; }
    set {
        if (project != null) project.PropertyChanged -= ChildPropertyChanged;
        project = value;
        RaisePropertyChanged("Project");
        if (project != null) project.PropertyChanged += ChildPropertyChanged;
    }
}

【讨论】:

  • 没有区别,因为我们正在更改 Project.Text 而不是 Project。 Project 的 set 方法只设置一次,在创建 Project 实例的构建过程中。不管怎样,我试过了,没有任何区别。
【解决方案2】:

这就是我应该这样做的方式,并且它按预期工作。这里唯一的区别是我使用的是 RelayCommand 而不是 DelegateCommand - 它们基本上具有相同的实现,因此它们应该可以互换。

当用户输入文本然后单击按钮时,RelayCommand 的执行方法会获取预期的文本 - 很简单。

XAML:

<Grid>

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <TextBox Grid.Column="0"
             Grid.Row="0"
             Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

    <Button Grid.Column="0"
            Grid.Row="1"
            Content="Test"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Center"
            Command="{Binding Path=TextCommand, Mode=OneWay}" />

</Grid>

视图模型:

public sealed class ExampleViewModel : BaseViewModel
{
    private string _text;

    public ExampleViewModel()
    {
       TextCommand = new RelayCommand(TextExecute, CanTextExecute);
    }

    public string Text
    {
        get
        {
            return _text;
        }
        set
        {
            _text = value;
            OnPropertyChanged("Text");
        }
    }

    public ICommand TextCommand { get; private set; }

    private void TextExecute()
    {
        // Do something with _text value...
    }

    private bool CanTextExecute()
    {
        return true;
    }
}

【讨论】:

    【解决方案3】:

    我在 prism codeplex 讨论论坛上从 swythan 找到了这个很棒的附加属性,它做得很好。当然它没有回答为什么命令参数设置为以前的值,但它以一种很好的方式解决了问题。

    代码在源代码上稍作修改,可以通过在调用 OnLoaded 事件时调用 HookCommandParameterChanged 在 TabItem 中的控件上使用它。

    public static class CommandParameterBehavior
    {
        public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
            DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
                                                typeof(bool),
                                                typeof(CommandParameterBehavior),
                                                new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));
    
        public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
        {
            return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
        }
    
        public static void SetIsCommandRequeriedOnChange(DependencyObject target, bool value)
        {
            target.SetValue(IsCommandRequeriedOnChangeProperty, value);
        }
    
        private static void OnIsCommandRequeriedOnChangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is ICommandSource))
                return;
    
            if (!(d is FrameworkElement || d is FrameworkContentElement))
                return;
    
            if ((bool)e.NewValue)
                HookCommandParameterChanged(d);
            else
                UnhookCommandParameterChanged(d);
    
            UpdateCommandState(d);
        }
    
        private static PropertyDescriptor GetCommandParameterPropertyDescriptor(object source)
        {
            return TypeDescriptor.GetProperties(source.GetType())["CommandParameter"];
        }
    
        private static void HookCommandParameterChanged(object source)
        {
            var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
            propertyDescriptor.AddValueChanged(source, OnCommandParameterChanged);
    
            // N.B. Using PropertyDescriptor.AddValueChanged will cause "source" to never be garbage collected,
            // so we need to hook the Unloaded event and call RemoveValueChanged there.
            HookUnloaded(source);
        }
    
        private static void UnhookCommandParameterChanged(object source)
        {
            var propertyDescriptor = GetCommandParameterPropertyDescriptor(source);
            propertyDescriptor.RemoveValueChanged(source, OnCommandParameterChanged);
    
            UnhookUnloaded(source);
        }
    
        private static void HookUnloaded(object source)
        {
            var fe = source as FrameworkElement;
            if (fe != null)
            {
                fe.Unloaded += OnUnloaded;
                fe.Loaded -= OnLoaded;
            }
    
            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded += OnUnloaded;
                fce.Loaded -= OnLoaded;
            }
        }
    
        private static void UnhookUnloaded(object source)
        {
            var fe = source as FrameworkElement;
            if (fe != null)
            {
                fe.Unloaded -= OnUnloaded;
                fe.Loaded += OnLoaded;
            }
    
            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded -= OnUnloaded;
                fce.Loaded += OnLoaded;
            }
        }
    
        static void OnLoaded(object sender, RoutedEventArgs e)
        {
            HookCommandParameterChanged(sender);
        }
    
        static void OnUnloaded(object sender, RoutedEventArgs e)
        {
            UnhookCommandParameterChanged(sender);
        }
    
        static void OnCommandParameterChanged(object sender, EventArgs ea)
        {
            UpdateCommandState(sender);
        }
    
        private static void UpdateCommandState(object target)
        {
            var commandSource = target as ICommandSource;
    
            if (commandSource == null)
                return;
    
            var rc = commandSource.Command as RoutedCommand;
            if (rc != null)
                CommandManager.InvalidateRequerySuggested();
    
            var dc = commandSource.Command as IDelegateCommand;
            if (dc != null)
                dc.RaiseCanExecuteChanged();
        }
    }
    

    来源:https://compositewpf.codeplex.com/discussions/47338

    【讨论】:

      猜你喜欢
      • 2010-10-11
      • 2021-08-28
      • 1970-01-01
      • 2011-04-01
      • 1970-01-01
      • 2021-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多