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

Posted

技术标签:

【中文标题】WPF CommandParameter 在第一次调用 CanExecute 时为 NULL【英文标题】:WPF CommandParameter is NULL first time CanExecute is called 【发布时间】:2010-09-25 01:13:58 【问题描述】:

我遇到了 WPF 和绑定到 ItemsControl 的 DataTemplate 内的按钮的命令的问题。场景非常简单。 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 在我的命令上被调用,但参数第一次始终为 NULL - 为什么会这样?

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

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

Update3:我已将错误的复制品上传到我的 SkyDrive:http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip

【问题讨论】:

我有完全相同的问题,使用 ListBox。 目前有一个针对 WPF 的针对此问题的错误报告:github.com/dotnet/wpf/issues/316 【参考方案1】:

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

我将其更改为使用相对源绑定,而不是按名称引用元素,这样就成功了。参数绑定没有改变。

旧代码:

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

新代码:

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

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

CommandParameter="Binding Groups"
Command="Binding StartCommand"

【讨论】:

将 CommandParameter 移到 Command 前面是这个线程的最佳答案。 移动属性的顺序对我们没有帮助。如果它对执行顺序有影响,我会感到惊讶。 我不知道为什么会这样。感觉不应该,但完全可以。 我遇到了同样的问题——RelativeSource 没有帮助,改变属性的顺序有帮助。感谢您的更新! 作为一个虔诚地使用扩展来自动美化 XAML(跨行拆分属性、修复缩进、重新排序属性)的人,更改 CommandParameterCommand 顺序的提议让我害怕。跨度> 【参考方案2】:

我发现我设置 Command 和 CommandParameter 的顺序会有所不同。设置 Command 属性会导致 CanExecute 立即被调用,因此您希望在该点已经设置 CommandParameter。

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

您似乎暗示该按钮永远不会启用,这令人惊讶,因为我希望在您的示例中的 Command 属性之后不久设置 CommandParameter。调用 CommandManager.InvalidateRequerySuggested() 会导致按钮启用吗?

【讨论】:

尝试在命令之前设置 CommandParameter - 仍然执行 CanExecute,但仍然传入 NULL... 太糟糕了 - 但感谢您的提示。此外,调用 CommandManager.InvalidateRequerySuggested();没有任何区别。 CommandManager.InvalidateRequerySuggested() 为我解决了类似的问题。谢谢!【参考方案3】:

我偶然发现了一个类似的问题,并使用我可信赖的 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 作为资源添加到某处才能正常工作。现在,在 CommandParameter 的值变得可用之前,不会设置 Command 属性。您甚至可以绑定到 RelativeSource.Self 和 CommandParameter 而不是 .达到同样的效果。

【讨论】:

这对我有用。我不理解为什么。谁能解释一下? 它不起作用,因为 CommandParameter 在命令之前绑定?我怀疑您是否需要转换器... 这不是解决方案。这是一个黑客?这到底是怎么回事?这曾经有效吗? 完美,适合我!神奇之处在于 行,它会导致命令绑定在数据模板更改时更新(绑定到命令参数)【参考方案4】:

我想出了另一个选项来解决我想分享的这个问题。因为命令的 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" />

我希望这可能会帮助其他人解决这个问题。

【讨论】:

做得很好,谢谢。我不敢相信 M$ 在 8 年后还没有解决这个问题。可怕!【参考方案5】:

这是一个旧线程,但由于 Google 在我遇到此问题时将我带到这里,我将添加对我有用的 DataGridTemplateColumn 按钮。

从以下位置更改绑定:

CommandParameter="Binding ."

CommandParameter="Binding DataContext, RelativeSource=RelativeSource Self"

不知道为什么它有效,但它对我有用。

【讨论】:

我已经尝试了以上两个高分答案,但这一个只对我有用。似乎这是控制本身而不是绑定的内部问题,但仍然有很多人使用上述答案。谢谢!【参考方案6】:

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

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

对于上下文菜单的特殊情况,忽略基于Tag的解决方法,这里的关键是定期绑定CommandParameter,但将Command与附加的IsAsync=True绑定。这将稍微延迟实际命令的绑定(以及因此它的CanExecute 调用),因此参数已经可用。不过,这意味着在短时间内,启用状态可能是错误的,但就我而言,这是完全可以接受的。

【讨论】:

【参考方案7】:

您也许可以使用我昨天发布到Prism forums 的CommandParameterBehavior。它添加了缺少的行为,其中对 CommandParameter 的更改会导致重新查询 Command

这里有一些复杂性,因为我试图避免如果您调用PropertyDescriptor.AddValueChanged 而不稍后调用PropertyDescriptor.RemoveValueChanged 导致的内存泄漏。我尝试通过在卸载 ekement 时取消注册处理程序来解决此问题。

您可能需要删除 IDelegateCommand 内容,除非您使用的是 Prism(并且希望对 Prism 库进行与我相同的更改)。另请注意,我们通常不在这里使用 RoutedCommands(我们使用 Prism 的 DelegateCommand&lt;T&gt; 来处理几乎所有事情)所以如果我对 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();
            

        
    

【讨论】:

在连接时遇到了您的错误报告。你有机会用你最后的代码更新你的帖子吗?还是您后来找到了更好的解决方法? 一个更简单的解决方案可能是使用绑定而不是属性描述符来观察 CommandParameter 属性。否则一个很好的解决方案!这实际上解决了根本问题,而不仅仅是引入了尴尬的破解或解决方法。【参考方案8】:

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

1) 下载 Prism 1.2 源代码并打开 CompositeApplicationLibrary_Desktop.sln。这是一个 Composite.Presentation.Desktop 项目,其中包含 DelegateCommand 源。

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。

保重, 乔

gmail 的裁判

【讨论】:

【参考方案9】:

在阅读了类似问题的一些好的答案后,我在您的示例中稍微更改了 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 将在 Binding 更改时和 Execute 方法之后被调用

如果 ViewModel 发生更改,它不会自动触发,但正如该线程中所述,可以通过在 GUI 线程上调用 CommandManager.InvalidateRequerySuggested 来实现

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

【讨论】:

我发现DispatcherPriority.Normal 太高而无法可靠地工作(或者在我的情况下根本无法工作)。使用DispatcherPriority.Loaded 效果很好,而且似乎更合适(即明确指示在与视图模型关联的 UI 元素实际上已加载之前不会调用委托)。【参考方案10】:

嘿 Jonas,不确定这是否适用于数据模板,但这是我在 ListView 上下文菜单中使用的绑定语法,用于将当前项作为命令参数获取:

CommandParameter="Binding RelativeSource=RelativeSource AncestorType=ContextMenu, Path=PlacementTarget.SelectedItem, Mode=TwoWay"

【讨论】:

我在列表视图中做同样的事情。在这种情况下,它是一个 ItemsControl,因此没有明显的属性可以“绑定”(在可视化树中)。我想我必须找到一种方法来检测绑定何时完成,并重新评估 CanExecute(因为 CommandParameter 被绑定,只是迟到了)【参考方案11】:

我已将此记录为 .Net 4.0 中针对 WPF 的错误,因为该问题在 Beta 2 中仍然存在。

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=504976

【讨论】:

反馈链接不再有效【参考方案12】:

其中一些答案是关于绑定到 DataContext 以获取 Command 本身,但问题是 CommandParameter 在不应该为 null 时为空。我们也经历过这种情况。凭直觉,我们找到了一种非常简单的方法来让它在我们的 ViewModel 中工作。这是专门针对客户反馈的CommandParameter null问题,一行代码。注意 Dispatcher.BeginInvoke()。

public DelegateCommand<objectToBePassed> CommandShowReport
    
        get
        
            // create the command, or pass what is already created.
            var command = _commandShowReport ?? (_commandShowReport = new DelegateCommand<object>(OnCommandShowReport, OnCanCommandShowReport));

            // For the item template, the OnCanCommand will first pass in null. This will tell the command to re-pass the command param to validate if it can execute.
            Dispatcher.BeginInvoke((Action) delegate  command.RaiseCanExecuteChanged(); , DispatcherPriority.DataBind);

            return command;
        
    

【讨论】:

【参考方案13】:

这是一个长镜头。要调试它,您可以尝试: - 检查 PreviewCanExecute 事件。 - 使用 snoop/wpf mole 窥视内部,看看命令参数是什么。

HTH,

【讨论】:

尝试使用 Snoop - 但调试起来非常困难,因为它在最初加载时只是 NULL。如果我在其上运行 Snoop,则 Command 和 CommandParameter 都设置...这与在 DataTemplate 中使用命令有关。【参考方案14】:

commandManager.InvalidateRequerySuggested 也适用于我。我相信以下链接谈到了类似的问题,并且 M$ dev 确认了当前版本的限制,并且 commandManager.InvalidateRequerySuggested 是解决方法。 http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

重要的是调用 commandManager.InvalidateRequerySuggested 的时机。这应该在通知相关值更改后调用。

【讨论】:

该链接不再有效【参考方案15】:

除了Ed Ball's suggestion在Command之前设置CommandParameter,确保你的CanExecute方法有一个object参数强>类型。

private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  

    // Your goes heres

希望它可以防止有人花费大量时间来弄清楚如何接收 SelectedItems 作为 CanExecute 参数

【讨论】:

以上是关于WPF CommandParameter 在第一次调用 CanExecute 时为 NULL的主要内容,如果未能解决你的问题,请参考以下文章

在 WPF 中使用 DataContext 作为 CommandParameter

WPF CommandParameter 绑定未更新

代码中无法识别 WPF CommandParameter

理解WPF Binding CommandParameter =“{Binding}”

使用 WPF + MVVM 中的 Command + CommandParameter 删除列表视图中的行

将 WPF 按钮 CommandParameter 绑定到 DataTemplate 中的按钮本身