首次调用CanExecute时,WPF CommandParameter为NULL

Jon*_*esø 81 data-binding wpf xaml command .net-3.5

我遇到了WPF和命令的问题,这些问题绑定到ItemsControl的DataTemplate中的Button.这种情况很简单.ItemsControl绑定到一个对象列表,我希望能够通过单击一个Button删除列表中的每个对象.Button执行命令,Command负责删除.CommandParameter绑定到我要删除的Object.这样我知道用户点击了什么.用户应该只能删除他们的"自己的"对象 - 所以我需要在Command的"CanExecute"调用中进行一些检查,以验证用户是否具有正确的权限.

问题是传递给CanExecute的参数在第一次被调用时是NULL - 所以我无法运行逻辑来启用/禁用命令.但是,如果我启用了allways,然后单击按钮执行命令,则会正确传入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>
Run Code Online (Sandbox Code Playgroud)

所以你可以看到我有一个评论对象列表.我希望将DeleteCommentCommand的CommandParameter绑定到Command对象.

所以我想我的问题是:以前有没有人遇到过这个问题?我的命令会调用CanExecute,但第一次参数总是为NULL - 为什么会这样?

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

Update2:有没有办法检测绑定何时"完成",以便我可以强制重新评估命令?另外 - 我有一个问题,我有多个按钮(ItemsControl中的每个项目一个)绑定到Command对象的同一个实例?

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

Tra*_*ber 53

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

我将其更改为使用相对源绑定,而不是按名称引用元素,这就是诀窍.参数绑定没有改变.

旧代码:

Command="{Binding DataContext.MyCommand, ElementName=myWindow}"
Run Code Online (Sandbox Code Playgroud)

新守则:

Command="{Binding DataContext.MyCommand, RelativeSource={RelativeSource AncestorType=Views:MyView}}"
Run Code Online (Sandbox Code Playgroud)

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

CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
Run Code Online (Sandbox Code Playgroud)

  • 在命令前面移动CommandParameter是此线程的最佳答案. (38认同)
  • 作为一个虔诚地使用扩展来自动美化XAML(跨行分割属性,修复缩进,重新排序属性)的人,改变命令'CommandParameter`和`Command`的提议让我害怕. (10认同)
  • 移动属性的顺序对我们没有帮助.如果它对执行顺序产生影响,我会感到惊讶. (5认同)
  • 我不知道为什么会这样.感觉不应该,但完全没有. (2认同)

Ed *_*all 28

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

我发现在XAML中切换属性的顺序实际上可以产生影响,尽管我不相信它会解决你的问题.不过,值得一试.

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

  • 尝试在Command之前设置CommandParameter - 仍然执行CanExecute,但仍然传入NULL ... Bummer - 但感谢提示.另外,调用CommandManager.InvalidateRequerySuggested(); 没有任何区别. (2认同)

小智 14

我偶然发现了类似的问题,并使用我可靠的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
}
Run Code Online (Sandbox Code Playgroud)

此值转换器接受任意数量的参数,并将第一个参数作为转换后的值传回.在您的情况下在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>
Run Code Online (Sandbox Code Playgroud)

您必须将TriggerConverter添加为某个地方的资源才能使其正常工作.现在,Command属性不是在CommandParameter的值可用之前设置的.您甚至可以绑定到RelativeSource.Self和CommandParameter而不是.达到同样的效果.

  • 这对我有用.我不懂为什么.谁能解释一下? (2认同)
  • 这不是解决方案。这是黑客吗?这到底是怎么回事?这曾经工作吗? (2认同)

Ed *_*wns 13

我知道这个帖子有点陈旧,但是我想出了另一种方法来解决这个我想分享的问题.因为命令的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在按钮上将命令参数绑定到...

<Button 
    Content="Press Me"
    Command="{Binding}" 
    helpers:ButtonHelper.CommandParameter="{Binding MyParameter}" />
Run Code Online (Sandbox Code Playgroud)

我希望这可能有助于其他人解决这个问题.


Sim*_*ith 6

这是一个老线程,但是当我遇到这个问题时谷歌把我带到了这里,我会用一个按钮为DataGridTemplateColumn添加对我有用的东西.

更改绑定:

CommandParameter="{Binding .}"
Run Code Online (Sandbox Code Playgroud)

CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"
Run Code Online (Sandbox Code Playgroud)

不知道为什么它有效,但它确实适合我.


小智 6

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

<MenuItem Header="Open file" Command="{Binding Tag.CommandOpenFile, IsAsync=True, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" CommandParameter="{Binding Name}" />
Run Code Online (Sandbox Code Playgroud)

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


Swy*_*han 5

您可以使用我昨天CommandParameterBehavior发布到Prism论坛的那个.它添加了缺少的行为,其中对要重新查询的CommandParameter原因进行了更改Command.

这里有一些复杂性,这是因为我试图避免在PropertyDescriptor.AddValueChanged没有以后调用的情况下调用时导致的内存泄漏PropertyDescriptor.RemoveValueChanged.我尝试通过在卸载ekement时取消注册处理程序来解决这个问题.

你可能需要删除这些IDelegateCommand东西,除非你使用的是Prism(并希望对我和Prism库进行相同的更改).另请注意,我们一般不会RoutedCommand在这里使用s(我们DelegateCommand<T>几乎都使用Prism )所以如果我的召唤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();
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)