简体   繁体   English

MVVM ICommand.CanExecute参数包含先前的值

[英]MVVM ICommand.CanExecute parameter contains previous value

I've a hard time understanding why ICommand.CanExecutes always contains the previous value instead of the new value if a nested property is used instead of a normal property. 我很难理解如果使用嵌套属性而不是常规属性,为什么ICommand.CanExecutes总是包含先前值而不是新值。

The problem is described below and I seriously can't figure out a way to fix this besides using some form of "Facade" pattern where I create properties in the viewmodel and hook them to their corresponding property in the model. 问题将在下面描述,除了使用某种形式的“门面”模式之外,我很难找到解决此问题的方法,在该模式下,我在viewmodel中创建属性并将它们连接到模型中的相应属性。

Or use the damn CommandManager.RequerySuggested event. 或使用该死的CommandManager.RequerySuggested事件。 The reason this is not optimal is because the view presents over 30 commands, just counting the menu, and if all CanExecute updates every time something changes, it will take a few seconds for all menuitems / buttons to update. 之所以不理想,是因为该视图仅显示菜单就显示了30多个命令,如果每次更改时所有CanExecute都进行更新,则所有菜单项/按钮的更新将需要几秒钟的时间。 Even using the example down below with only a single command and button together with the command manager it takes around 500ms for the button to enable/disable itself. 即使使用下面的示例,仅使用单个命令和按钮以及命令管理器,按钮也需要大约500毫秒来启用/禁用自身。

The only reason I can think of is that the CommandParameter binding is not updated before the CanExecute is fired and then I guess there is nothing you can do about it. 我能想到的唯一原因是在CanExecute触发之前CommandParameter绑定没有更新,然后我猜想您无能为力。

Thanks in advance :! 提前致谢 :!

For example 例如

Let's say we've this basic viewmodel 假设我们有这个基本的ViewModel

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));
    }
}

and this model 和这个模型

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));
    }
}

Now in my view I've this textbox and button. 现在在我看来,我有这个文本框和按钮。

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

It works, every time I type something in the textbox the CanExecute is invoked, BUT the parameter is always set to the previous value. 它的工作原理是,每当我在文本框中键入任何内容时,就会调用CanExecute,但该参数始终设置为先前的值。 Let say I write 'H' in the textbox, CanExecute is fired with parameter set to NULL. 假设我在文本框中写了“ H”,则在参数设置为NULL的情况下触发了CanExecute。 Next I write 'E', now the textbox contains "HE" and the CanExecute fires again. 接下来,我写“ E”,现在文本框包含“ HE”,并且CanExecute再次触发。 This time with the parameter set to 'H' only. 这次仅将参数设置为“ H”。

For some strange reason the parameter is always set to the previous value and when I check the Project.Text it's set to "HE" but parameter is still set to only 'H'. 由于某些奇怪的原因,该参数始终设置为先前的值,并且当我检查Project.Text时将其设置为“ HE”,但参数仍仅设置为“ H”。

If I now change the command parameter to 如果我现在将命令参数更改为

CommandParameter="{Binding Path=Name}"

and the Textbox.Text to 和Textbox.Text到

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

everything works perfectly. 一切正常。 The CanExecute parameter always contain the latest value and not the previous value. CanExecute参数始终包含最新值,而不是先前值。

The facade pattern you're talking about it standard WPF practice. 您正在谈论的外观模式是WPF的标准实践。 The main problem with the way that you're doing it is that when events are raised, their subscribed event handlers execute in the order that they are subscribed. 这样做的主要问题是,引发事件时,其订阅的事件处理程序将按照订阅的顺序执行。 The line of code where you have: 您所在的代码行:

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

This subscribes to the "PropertyChanged" Event of your "Project" class. 这订阅了“ Project”类的“ PropertyChanged”事件。 Your UIElements are also subscribed to this same "PropertyChanged" event through your binding in the XAML. 您的UIElement也通过在XAML中的绑定而订阅了相同的“ PropertyChanged”事件。 In short, your "PropertyChanged" event now has 2 subscribers. 简而言之,您的“ PropertyChanged”事件现在有2个订阅者。

The thing about events is that they fire in a sequence and what's happening in your code, is that when the event fires from your "Project.Text" it executes your "ChildPropertyChanged" event, firing your "CanExecuteChanged" event, which finally runs your "CanExecute" function(which is when you're seeing the incorrect parameter). 关于事件的事情是,它们按顺序触发,并且代码中发生的事情是,当事件从“ Project.Text”触发时,它将执行“ ChildPropertyChanged”事件,并触发“ CanExecuteChanged”事件,最终运行您的“ CanExecute”功能(当您看到错误的参数时)。 THEN, after that, your UIElements get their EventHandlers executed by that same event. 然后,在那之后,您的UIElements将获得由同一事件执行的EventHandler。 And their values get updated. 它们的值会更新。

It's the order of your subscriptions causing the problem. 这是导致问题的订阅顺序。 Try this and tell me if it fixes your problem: 试试这个,告诉我是否可以解决您的问题:

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

This is how I would have done this, and it works as expected. 这就是我本应执行的操作,并且它按预期工作。 The only difference here is I'm using RelayCommand instead of DelegateCommand - they fundamentally have the same implementation so they should be interchangeable. 唯一的区别是我使用的是RelayCommand而不是DelegateCommand-它们从根本上具有相同的实现,因此应该可以互换。

When the user enters the text and then clicks the button the execute method of the RelayCommand gets the expected text - simple. 当用户输入文本然后单击按钮时,RelayCommand的execute方法将获得期望的文本-简单。

XAML: 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>

ViewModel: 视图模型:

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;
    }
}

I found this great attached property from swythan on the prism codeplex discussion forum that did the trick very well. 我在棱镜Codeplex讨论论坛上发现了swythan的这一出色附件,效果非常好。 Of course it does not answer why the command parameter is set to the previous value but it fixes the problem in a nice way. 当然,它不能回答为什么将命令参数设置为先前的值,但是可以很好地解决该问题。

The code is slightly modified from the source, enabling the possibility to use it on controls in a TabItem by calling HookCommandParameterChanged when the OnLoaded event is invoked. 从源代码进行了稍微的修改,从而在调用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();
    }
}

Source: https://compositewpf.codeplex.com/discussions/47338 资料来源: https : //compositewpf.codeplex.com/discussions/47338

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM