第一次调用 CanExecute 时,WPFCommandParameter 为 NULL

我遇到了 WPF 和命令的问题,这些命令绑定到 ItemsControl 的 DataTemplate 中的 Button。情况很简单。ItemsControl 绑定到一个对象列表,我希望能够通过单击 Button 删除列表中的每个对象。Button 执行一个 Command,Command 负责删除。CommandParameter 绑定到要删除的对象。这样我就知道用户点击了什么。用户应该只能删除他们的“自己的”对象——所以我需要在 Command 的“ 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>

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

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

更新: 我能够把问题缩小一点。我添加了一个空的 Debug ValueConverter,以便在 CommandParameter 数据绑定时输出消息。问题在于 CanExecute 方法是在 CommandParameter 绑定到按钮之前执行的。我曾试图在 Command 之前设置 CommandParameter (像建议的那样)-但它仍然不起作用。有什么控制方法吗。

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

更新3: 我已经上传了一个 bug 的复制品到我的 SkyDrive: < a href = “ http://cid-1a08c11c407c0d8e.SkyDrive. live.com/self.aspx/code% 20sample/CommandParameter terBinding.zip”rel = “ noReferrer”> http://cid-1a08c11c407c0d8e.SkyDrive.live.com/self.aspx/code%20samples/commandparameterbinding.zip

44355 次浏览

I have found that the order in which I set Command and CommandParameter makes a difference. Setting the Command property causes CanExecute to be called immediately, so you want CommandParameter to already be set at that point.

I have found that switching the order of the properties in the XAML can actually have an effect, though I'm not confident that it will solve your problem. It's worth a try, though.

You seem to be suggesting that the button never becomes enabled, which is surprising, since I would expect the CommandParameter to be set shortly after the Command property in your example. Does calling CommandManager.InvalidateRequerySuggested() cause the button to become enabled?

Its a long shot. to debug this you can try:
- checking the PreviewCanExecute event.
- use snoop/wpf mole to peek inside and see what the commandparameter is.

HTH,

not sure if this will work in a data template, but here is the binding syntax I use in a ListView Context menu to grab the current item as a command parameter:

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

The commandManager.InvalidateRequerySuggested works for me as well. I believe the following link talks about similar problem, and M$ dev confirmed the limitation in the current version, and the commandManager.InvalidateRequerySuggested is the workaround. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/

What important is the timing of invoking the commandManager.InvalidateRequerySuggested. This should be invoked after the relevant value change is notified.

I stumbled upon a similar problem and solved it using my trusty 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
}

This value converter takes any number of parameters and passes the first of them back as the converted value. When used in a MultiBinding in your case it looks like the following.

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

You will have to add TriggerConverter as a resource somewhere for this to work. Now the Command property is set not before the value for the CommandParameter has become available. You could even bind to RelativeSource.Self and CommandParameter instead of . to achieve the same effect.

You may be able to use my CommandParameterBehavior that I posted to the Prism forums yesterday. It adds the missing behaviour where a change to the CommandParameter cause the Command to be re-queried.

There's some complexity here caused by my attempts to avoid the memory leak caused if you call PropertyDescriptor.AddValueChanged without later calling PropertyDescriptor.RemoveValueChanged. I try and fix that by unregistering the handler when the ekement is unloaded.

You'll probably need to remove the IDelegateCommand stuff unless you're using Prism (and want to make the same changes as me to the Prism library). Also note that we don't generally use RoutedCommands here (we use Prism's DelegateCommand<T> for pretty much everything) so please don't hold me responsible if my call to CommandManager.InvalidateRequerySuggested sets off some sort of quantum wavefuntion collapse cascade that destroys the known universe or anything.

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


}
}
}

I've logged this as a bug against WPF in .Net 4.0, as the problem still exists in Beta 2.

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

There's a relatively simple way to "fix" this problem with DelegateCommand, though it requires updating the DelegateCommand source and re-compiling the Microsoft.Practices.Composite.Presentation.dll.

1) Download the Prism 1.2 source code and open the CompositeApplicationLibrary_Desktop.sln. In here is a Composite.Presentation.Desktop project that contains the DelegateCommand source.

2) Under the public event EventHandler CanExecuteChanged, modify to read as follows:

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) Under protected virtual void OnCanExecuteChanged(), modify it as follows:

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

4) Recompile the solution, then navigate to either the Debug or Release folder where the compiled DLLs live. Copy the Microsoft.Practices.Composite.Presentation.dll and .pdb (if you wish) to where you references your external assemblies, and then recompile your application to pull the new versions.

After this, CanExecute should be fired every time the UI renders elements bound to the DelegateCommand in question.

Take care, Joe

refereejoe at gmail

I was having this same issue while trying to bind to a command on my view model.

I changed it to use a relative source binding rather than referring to the element by name and that did the trick. Parameter binding didn't change.

Old Code:

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

New Code:

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

Update: I just came across this issue without using ElementName, I'm binding to a command on my view model and my data context of the button is my view model. In this case I had to simply move the CommandParameter attribute before the Command attribute in the Button declaration (in XAML).

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

I've come up with another option to work around this issue that I wanted to share. Because the CanExecute method of the command gets executed before the CommandParameter property is set, I created a helper class with an attached property that forces the CanExecute method to be called again when the binding changes.

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

And then on the button you want to bind a command parameter to...

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

I hope this perhaps helps someone else with the issue.

Beside Ed Ball's suggestion on setting CommandParameter prior to Command, make sure your CanExecute method has a parameter of object type.

private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)
{
// Your goes heres
}

Hope it prevents someone spending the huge amount of time I did to figure out how to receive SelectedItems as CanExecute parameter

After reading some good answers to similar questions I changed in your example the DelegateCommand slightly to make it work. Instead of using:

public event EventHandler CanExecuteChanged;

I changed it to:

public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}

I removed the following two methods because I was too lazy to fix them

public void RaiseCanExecuteChanged()

and

protected virtual void OnCanExecuteChanged()

And that's all... this seems to ensure that CanExecute will be called when the Binding changes and after the Execute method

It will not automatically trigger if the ViewModel is changed but as mentioned in this thread possible by calling the CommandManager.InvalidateRequerySuggested on the GUI thread

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

Some of these answers are about binding to the DataContext to get the Command itself, but the question was about the CommandParameter being null when it shouldn't be. We also experienced this. On on a hunch, we found a very simple way to get this to work in our ViewModel. This is specifically for the CommandParameter null problem reported by the customer, with one line of code. Note the 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;
}
}

I'll add what worked for me for a DataGridTemplateColumn with a button.

Change the binding from:

CommandParameter="{Binding .}"

to

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

Not sure why it works, but it did for me.

I recently came across the same problem (for me it was for the menu items in a context menu), nad while it may not be a suitable solution for every situation, I found a different (and a lot shorter!) way of solving this problem:

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

Ignoring the Tag-based workaround for the special case of context menu, the key here is to bind the CommandParameter regularly, but bind the Command with the additional IsAsync=True. This will delay the binding of the actual command (and therefore its CanExecute call) a bit, so the parameter will already be available. This means, though, that for a brief moment, the enabled-state might be wrong, but for my case, that was perfectly acceptable.

In .NET 7.0 RC1 this has been fixed.

(At least in a sense...)

It should now automatically reevaluate CanExecute() whenever CommandParameter changes, including upon initialization.

Although that does not prevent the initial call to CanExecute() when CommandParameter is still null, many ICommand implementations should already be handling that and it does render obsolete the obscuure and problematic XAML attribute ordering workaround / hack.

As intimated by @Daniel-Svensson in a GitHub comment:

the actual issue here is that ICommand.CanExecute is not reevaluated when the value bound to CommandParameter changes. Doing so would obviously be the correct behavior, since the command parameter is passed to CanExecute, so everyone expects this behavior intuitively.

And that is what is being fixed.


According to @pchaurasia14, a Sr. Engineering Manager for WPF at Microsoft:

This has been fixed in RC1 release. You may try it out. ... I meant, .NET 7 RC1.

At the moment the GitHub tracking issue #316 in the dotnet/wpf project is still listed as open. Maybe that is because it's scope was broader. But the code change CommandParameter invalidates CanExecute #4217 has been included in .NET 7.0 RC1. It was merged on July 21, 2022 and is included in the list of commits (scroll way down) for the RC1 release.