1

如果使用嵌套属性而不是普通属性,我很难理解为什么 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 参数始终包含最新值而不是先前值。

4

3 回答 3

0

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

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

这订阅了“项目”类的“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;
    }
}
于 2015-02-13T18:12:04.950 回答
0

我就是这样做的,它按预期工作。这里唯一的区别是我使用的是 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;
    }
}
于 2015-02-14T10:56:17.580 回答
0

我在 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

于 2015-02-14T16:02:39.787 回答