WPF的CommandParameter在第一次调用CanExecute时为NULL

99
我在使用WPF和绑定到ItemsControl数据模板中的按钮的命令时遇到了问题。情景非常简单,ItemsControl绑定到对象列表,我想通过点击一个按钮来删除列表中的每个对象。该按钮执行一个命令,并且该命令负责删除。CommandParameter绑定到我要删除的对象上,这样我就知道用户单击了什么。用户只能删除自己的对象,因此我需要在命令的“CanExecute”调用中进行一些检查以验证用户是否具有正确的权限。
问题是CanExecute传递的参数第一次调用时为NULL,因此我无法运行启用/禁用命令的逻辑。但是,如果我始终启用它,然后单击按钮执行命令,CommandParameter将被正确传递。这意味着与CommandParameter的绑定正在工作。
ItemsControl和DataTemplate的XAML如下:
<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    Command="{Binding Path=DataContext.DeleteCommentCommand, ElementName=commentsList}" 
                    CommandParameter="{Binding}" />
            </StackPanel>                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

可以看到我有一个Comments对象列表,我想将DeleteCommentCommand的CommandParameter绑定到Command对象。

所以我的问题是:有人之前遇到过这个问题吗?CanExecute在我的Command上被调用,但是第一次参数总是为NULL - 这是为什么?

更新:我能够稍微缩小问题的范围。我添加了一个空的Debug ValueConverter,以便在数据绑定时输出消息。结果问题是在将CommandParameter绑定到按钮之前就执行了CanExecute方法。我尝试在Command之前设置CommandParameter(如建议的那样)-但仍然不起作用。有关如何控制它的任何提示。

更新2:有没有办法检测绑定何时“完成”,以便我可以强制重新评估命令?另外,是否存在多个按钮(每个项在ItemsControl中都有一个)绑定到同一个Command对象实例的问题?

更新3:我已经将演示错误上传到我的SkyDrive:http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip


我有完全相同的问题,是关于一个ListBox。 - Hadi Eskandari
1
目前有一个针对WPF的已知问题的错误报告:https://github.com/dotnet/wpf/issues/316 - StayOnTarget
16个回答

66

我在尝试将命令绑定到我的视图模型时遇到了同样的问题。

我将绑定方式改为相对源绑定,而非按名称引用元素,这样就解决了问题。参数绑定没有改变。

原代码:

Command="{Binding DataContext.MyCommand, ElementName=myWindow}"

新代码:

Command="{Binding DataContext.MyCommand, RelativeSource={RelativeSource AncestorType=Views:MyView}}"

更新: 我刚刚遇到这个问题,没有使用ElementName,我绑定到我的视图模型上的命令,并且我的按钮的数据上下文是我的视图模型。在这种情况下,我只需要在按钮声明(在XAML中)中将CommandParameter属性移到Command属性之前即可。

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"

50
将CommandParameter移到Command之前是这个帖子中最好的解决方案。 - BSick7
6
调整属性的顺序对我们没有帮助。如果它能影响执行顺序,我会感到惊讶。 - Jack Ukleja
4
我不知道为什么这个方法有效,感觉它不应该行得通,但它完全奏效了。 - RMK
1
我遇到了同样的问题 - RelativeSource 没有帮助,但改变属性顺序解决了问题。感谢更新! - Grant Crofton
16
作为一个习惯于使用扩展程序来自动美化 XAML(将属性分成多行、修正缩进、重新排序属性)的人,更改“CommandParameter”和“Command”的顺序的提议让我感到担忧。 - Guttsy
显示剩余4条评论

34

我发现设置Command和CommandParameter的顺序会有影响。设置Command属性会立即调用CanExecute方法,因此您希望在此时已经设置好CommandParameter。

我发现在XAML中切换属性的顺序实际上可能会产生影响,但我不确定它是否能解决您的问题。不过值得一试。

您似乎在暗示按钮从未变为可用状态,这令人惊讶,因为我期望在您的示例中,Command属性后不久就会设置CommandParameter。调用CommandManager.InvalidateRequerySuggested()会使按钮变为可用吗?


3
尝试在命令之前设置CommandParameter - 仍会执行CanExecute,但仍会传入NULL... 很遗憾,但还是谢谢你的建议。此外,调用CommandManager.InvalidateRequerySuggested(); 没有任何区别。 - Jonas Follesø
CommandManager.InvalidateRequerySuggested() 对我解决了一个类似的问题。谢谢! - MJS

19

我想分享一种解决此问题的另一种方法。由于命令的CanExecute方法在设置CommandParameter属性之前执行,因此我创建了一个带有附加属性的辅助类,当绑定更改时,该属性会强制重新调用CanExecute方法。

public static class ButtonHelper
{
    public static DependencyProperty CommandParameterProperty = DependencyProperty.RegisterAttached(
        "CommandParameter",
        typeof(object),
        typeof(ButtonHelper),
        new PropertyMetadata(CommandParameter_Changed));

    private static void CommandParameter_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = d as ButtonBase;
        if (target == null)
            return;

        target.CommandParameter = e.NewValue;
        var temp = target.Command;
        // Have to set it to null first or CanExecute won't be called.
        target.Command = null;
        target.Command = temp;
    }

    public static object GetCommandParameter(ButtonBase target)
    {
        return target.GetValue(CommandParameterProperty);
    }

    public static void SetCommandParameter(ButtonBase target, object value)
    {
        target.SetValue(CommandParameterProperty, value);
    }
}

然后在按钮上,您想要绑定一个命令参数...

<Button 
    Content="Press Me"
    Command="{Binding}" 
    helpers:ButtonHelper.CommandParameter="{Binding MyParameter}" />

我希望这可以帮助其他遇到类似问题的人。

1
干得好,谢谢。我简直不敢相信微软在8年后还没有修复这个问题。太糟糕了! - McGarnagle
这个解决方案在我的情况下起作用了,不过最终我选择了Simon Smith的解决方案,因为它更简单(只需要在XAML中进行绑定格式的更改)。 - undefined

18

我也遇到了类似的问题,并且用我的可靠的TriggerConverter解决了它。

public class TriggerConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // First value is target value.
        // All others are update triggers only.
        if (values.Length < 1) return Binding.DoNothing;
        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

这个值转换器可以接受任意数量的参数,并将其中第一个作为转换后的值返回。在您的情况下,当它用于MultiBinding时,它看起来如下所示。

<ItemsControl 
    x:Name="commentsList"
    ItemsSource="{Binding Path=SharedDataItemPM.Comments}"
    Width="Auto" Height="Auto">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Button                             
                    Content="Delete"
                    FontSize="10"
                    CommandParameter="{Binding}">
                    <Button.Command>
                        <MultiBinding Converter="{StaticResource TriggerConverter}">
                            <Binding Path="DataContext.DeleteCommentCommand"
                                     ElementName="commentsList" />
                            <Binding />
                        </MultiBinding> 
                    </Button.Command>
                </Button>
            </StackPanel>                                       
         </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

要使这个工作生效,您必须将TriggerConverter添加为某个资源。现在Command属性是在CommandParameter的值可用之前设置的。您甚至可以绑定到RelativeSource.Self和CommandParameter而不是“。”以实现相同的效果。


2
这对我起作用了,但我不明白为什么。有人能解释一下吗? - Torben Junker Kjær
这是否无法工作是因为CommandParameter在Command之前绑定?我怀疑您是否需要转换器... - MBoros
2
这不是一个解决方案。这只是一个hack?到底发生了什么?这以前可是能用的啊? - Jordan
完美,适合我!魔法在于 <Binding /> 行,它会在数据模板更改时更新命令绑定(该绑定与命令参数相关)。 - Andreas Kahler

14

对于带有按钮的DataGridTemplateColumn,我会分享适用于我的解决方案。

将绑定更改为:

CommandParameter="{Binding .}"

CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"

我不确定为什么它有效,但对我来说确实有效。


我尝试了上面两个高分答案,但只有这一个对我有效。看起来这是控件本身的内部问题而不是绑定的问题,但仍然有很多人通过以上答案使其工作。谢谢! - Javidan
这对我的情况来说完美地起作用了,也是我在这里看到的最干净的解决方案(比改变Command和CommandParameter的顺序要干净得多,而且对我来说也没有起作用)。 - undefined

7

最近我遇到了同样的问题(对我而言是上下文菜单中的菜单项),虽然它可能不适用于所有情况,但我发现了一种不同(且更简短!)的解决方法:

<MenuItem Header="Open file" Command="{Binding Tag.CommandOpenFile, IsAsync=True, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" CommandParameter="{Binding Name}" />

忽略基于标签的特殊情况下上下文菜单的解决方法,关键在于正常绑定命令参数CommandParameter,但是将命令Command额外绑定为IsAsync=True。这会稍微延迟实际命令的绑定(因此其CanExecute调用也会被延迟一段时间),以便参数已经可用。这意味着在一瞬间,启用状态可能是错误的,但对于我的情况来说,这是完全可以接受的。

5
您可以使用我昨天在Prism论坛上发布的CommandParameterBehavior。它添加了缺失的行为,即更改CommandParameter会导致重新查询Command。这里有一些复杂性,由于我的尝试避免调用PropertyDescriptor.AddValueChanged而不稍后调用PropertyDescriptor.RemoveValueChanged引起的内存泄漏。我尝试通过在元素卸载时取消注册处理程序来修复它。
您可能需要删除IDelegateCommand的内容,除非您正在使用Prism(并且想要对Prism库进行相同的更改)。还请注意,我们通常不在此处使用RoutedCommand(我们几乎将Prism的DelegateCommand<T>用于所有内容),因此如果我对CommandManager.InvalidateRequerySuggested的调用引发某种量子波函数崩溃级联破坏已知宇宙或其他任何事情,请不要追究我的责任。
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Microsoft.Practices.Composite.Wpf.Commands
{
    /// <summary>
    /// This class provides an attached property that, when set to true, will cause changes to the element's CommandParameter to 
    /// trigger the CanExecute handler to be called on the Command.
    /// </summary>
    public static class CommandParameterBehavior
    {
        /// <summary>
        /// Identifies the IsCommandRequeriedOnChange attached property
        /// </summary>
        /// <remarks>
        /// When a control has the <see cref="IsCommandRequeriedOnChangeProperty" />
        /// attached property set to true, then any change to it's 
        /// <see cref="System.Windows.Controls.Primitives.ButtonBase.CommandParameter" /> property will cause the state of
        /// the command attached to it's <see cref="System.Windows.Controls.Primitives.ButtonBase.Command" /> property to 
        /// be reevaluated.
        /// </remarks>
        public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
            DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
                                                typeof(bool),
                                                typeof(CommandParameterBehavior),
                                                new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));

        /// <summary>
        /// Gets the value for the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt.</param>
        /// <returns>Whether the update on change behavior is enabled.</returns>
        public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
        {
            return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
        }

        /// <summary>
        /// Sets the <see cref="IsCommandRequeriedOnChangeProperty"/> attached property.
        /// </summary>
        /// <param name="target">The object to adapt. This is typically a <see cref="System.Windows.Controls.Primitives.ButtonBase" />, 
        /// <see cref="System.Windows.Controls.MenuItem" /> or <see cref="System.Windows.Documents.Hyperlink" /></param>
        /// <param name="value">Whether the update behaviour should be enabled.</param>
        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;
            }

            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded += OnUnloaded;
            }
        }

        private static void UnhookUnloaded(object source)
        {
            var fe = source as FrameworkElement;
            if (fe != null)
            {
                fe.Unloaded -= OnUnloaded;
            }

            var fce = source as FrameworkContentElement;
            if (fce != null)
            {
                fce.Unloaded -= OnUnloaded;
            }
        }

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

        }
    }
}

我在Connect上看到了你的错误报告。你有没有可能在这里更新一下你最后的代码?或者你是否已经找到了更好的解决方法? - Markus Hütter
一个更简单的解决方案可能是使用绑定来观察CommandParameter属性,而不是使用属性描述符。否则,这是一个很棒的解决方案!它实际上修复了潜在的问题,而不仅仅是引入一个笨拙的hack或解决方法。 - Sebastian Negraszus

1
在.NET 7.0 RC1中,这个问题已经被修复了。至少在某种程度上是这样的...现在它应该会在CommandParameter更改时自动重新评估CanExecute(),包括在初始化时。虽然这并不能防止当CommandParameter仍为null时对CanExecute()的初始调用,但是许多ICommand实现应该已经处理了这一点,并且它确实使得原来混乱且有问题的XAML属性排序解决方法变得过时了。正如@Daniel-Svenssona GitHub comment中所暗示的那样:
“这里实际的问题是当绑定到CommandParameter的值更改时,ICommand.CanExecute没有被重新评估。这样做显然是正确的行为,因为命令参数被传递给CanExecute,所以每个人都直觉地期望这种行为。”
而这就是正在被修复的问题。
根据微软WPF的高级工程经理@pchaurasia14所说:

这个问题已经在RC1版本中得到修复。你可以尝试一下……我的意思是,.NET 7 RC1。

在dotnet/wpf项目中,GitHub跟踪问题#316已被标记为关闭。CommandParameter invalidates CanExecute #4217的代码更改已包含在.NET 7.0 RC1中。它于2022年7月21日合并,并包含在提交列表(向下滚动)中,用于RC1发布

1

有一个相对简单的方法可以“修复”DelegateCommand的问题,但需要更新DelegateCommand源代码并重新编译Microsoft.Practices.Composite.Presentation.dll。

1)下载Prism 1.2源代码并打开CompositeApplicationLibrary_Desktop.sln。这里有一个包含DelegateCommand源代码的Composite.Presentation.Desktop项目。

2)在公共事件EventHandler CanExecuteChanged下,修改为以下内容:

public event EventHandler CanExecuteChanged
{
     add
     {
          WeakEventHandlerManager.AddWeakReferenceHandler( ref _canExecuteChangedHandlers, value, 2 );
          // add this line
          CommandManager.RequerySuggested += value;
     }
     remove
     {
          WeakEventHandlerManager.RemoveWeakReferenceHandler( _canExecuteChangedHandlers, value );
          // add this line
          CommandManager.RequerySuggested -= value;
     }
}

3)在 protected virtual void OnCanExecuteChanged() 下,修改如下:

protected virtual void OnCanExecuteChanged()
{
     // add this line
     CommandManager.InvalidateRequerySuggested();
     WeakEventHandlerManager.CallWeakReferenceHandlers( this, _canExecuteChangedHandlers );
}

4) 重新编译解决方案,然后导航到已编译的DLL所在的Debug或Release文件夹。将Microsoft.Practices.Composite.Presentation.dll和.pdb(如果需要)复制到引用外部程序集的位置,然后重新编译应用程序以获取新版本。

此后,每当UI呈现绑定到相关DelegateCommand的元素时,CanExecute都应该被触发。

注意事项, Joe

refereejoe at gmail


1
阅读了一些类似问题的好回答后,我稍微修改了您的示例中的DelegateCommand,使其能够正常工作。不使用:
public event EventHandler CanExecuteChanged;

我将其改为:
public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

我删除了以下两个方法,因为我太懒了,懒得去修复它们。

public void RaiseCanExecuteChanged()

并且

protected virtual void OnCanExecuteChanged()

这就是全部内容...这似乎确保在绑定更改后和执行方法后调用CanExecute。

如果ViewModel更改,它不会自动触发,但是可以通过在GUI线程上调用CommandManager.InvalidateRequerySuggested来实现,正如在此线程中提到的那样。

Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);

我发现 DispatcherPriority.Normal 太高了,无法可靠地工作(或者在我的情况下根本不起作用)。使用 DispatcherPriority.Loaded 效果很好,并且似乎更合适(即明确指示委托直到与视图模型相关联的 UI 元素实际加载完毕后才被调用)。 - Peter Duniho

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接