第一次调用 CanExecute 时,WPF CommandParameter 为 NULL
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/335849/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
WPF CommandParameter is NULL first time CanExecute is called
提问by Jonas Folles?
I have run into an issue with WPF and Commands that are bound to a Button inside the DataTemplate of an ItemsControl. The scenario is quite straight forward. The ItemsControl is bound to a list of objects, and I want to be able to remove each object in the list by clicking a Button. The Button executes a Command, and the Command takes care of the deletion. The CommandParameter is bound to the Object I want to delete. That way I know what the user clicked. A user should only be able to delete their "own" objects - so I need to do some checks in the "CanExecute" call of the Command to verify that the user has the right permissions.
我遇到了绑定到 ItemsControl 的 DataTemplate 内的按钮的 WPF 和命令的问题。这个场景非常简单。ItemsControl 绑定到一个对象列表,我希望能够通过单击一个按钮来删除列表中的每个对象。按钮执行命令,命令负责删除。CommandParameter 绑定到我要删除的对象。这样我就知道用户点击了什么。用户应该只能删除他们的“自己的”对象——所以我需要在命令的“CanExecute”调用中做一些检查,以验证用户是否具有正确的权限。
The problem is that the parameter passed to CanExecute is NULL the first time it's called - so I can't run the logic to enable/disable the command. However, if I make it allways enabled, and then click the button to execute the command, the CommandParameter is passed in correctly. So that means that the binding against the CommandParameter is working.
问题是传递给 CanExecute 的参数在第一次被调用时为 NULL - 所以我无法运行逻辑来启用/禁用命令。但是,如果我让它始终启用,然后单击按钮执行命令,则 CommandParameter 会正确传入。所以这意味着对 CommandParameter 的绑定正在起作用。
The XAML for the ItemsControl and the DataTemplate looks like this:
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>
So as you can see I have a list of Comments objects. I want the CommandParameter of the DeleteCommentCommand to be bound to the Command object.
如您所见,我有一个 Comments 对象列表。我希望将 DeleteCommentCommand 的 CommandParameter 绑定到 Command 对象。
So I guess my question is: have anyone experienced this problem before? CanExecute gets called on my Command, but the parameter is always NULL the first time - why is that?
所以我想我的问题是:以前有人遇到过这个问题吗?CanExecute 在我的命令上被调用,但参数第一次总是为 NULL - 为什么会这样?
Update:I was able to narrow the problem down a little. I added an empty Debug ValueConverter so that I could output a message when the CommandParameter is data bound. Turns out the problem is that the CanExecute method is executed before the CommandParameter is bound to the button. I have tried to set the CommandParameter before the Command (like suggested) - but it still doesn't work. Any tips on how to control it.
更新:我能够稍微缩小问题的范围。我添加了一个空的 Debug ValueConverter,以便我可以在 CommandParameter 数据绑定时输出一条消息。原来问题是在将 CommandParameter 绑定到按钮之前执行了 CanExecute 方法。我试图在命令之前设置 CommandParameter(如建议的那样) - 但它仍然不起作用。关于如何控制它的任何提示。
Update2:Is there any way to detect when the binding is "done", so that I can force re-evaluation of the command? Also - is it a problem that I have multiple Buttons (one for each item in the ItemsControl) that bind to the same instance of a Command-object?
更新 2:有什么方法可以检测绑定何时“完成”,以便我可以强制重新评估命令?另外 - 我有多个按钮(ItemsControl 中的每个项目一个)绑定到命令对象的同一实例是否有问题?
Update3:I have uploaded a reproduction of the bug to my SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip
更新 3:我已将错误的复制上传到我的 SkyDrive:http: //cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip
采纳答案by David Liersch
I stumbled upon a similar problem and solved it using my trusty TriggerConverter.
我偶然发现了一个类似的问题,并使用我值得信赖的 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.
这个值转换器接受任意数量的参数并将它们中的第一个作为转换后的值传回。在您的情况下在 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>
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.
您必须在某处添加 TriggerConverter 作为资源才能使其工作。现在,Command 属性不是在 CommandParameter 的值可用之前设置的。您甚至可以绑定到 RelativeSource.Self 和 CommandParameter 而不是 . 达到同样的效果。
回答by Travis Weber
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).
更新:我刚刚在没有使用 ElementName 的情况下遇到了这个问题,我正在绑定到我的视图模型上的命令,而我的按钮数据上下文是我的视图模型。在这种情况下,我必须简单地将 CommandParameter 属性移动到 Button 声明中的 Command 属性之前(在 XAML 中)。
CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
回答by Ed Ball
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.
我发现设置 Command 和 CommandParameter 的顺序有所不同。设置 Command 属性会导致立即调用 CanExecute,因此您希望此时已经设置了 CommandParameter。
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.
我发现在 XAML 中切换属性的顺序实际上可以产生效果,尽管我不相信它会解决您的问题。不过值得一试。
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?
您似乎暗示该按钮永远不会启用,这令人惊讶,因为我希望 CommandParameter 在您的示例中的 Command 属性之后不久设置。调用 CommandManager.InvalidateRequerySuggested() 是否会导致按钮启用?
回答by Ed Downs
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.
我想出了另一个选项来解决我想分享的这个问题。因为命令的 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);
}
}
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.
我希望这可能会帮助其他人解决这个问题。
回答by Simon Smith
This is an old thread, but since Google brought me here when I had this issue, I'll add what worked for me for a DataGridTemplateColumn with a button.
这是一个旧线程,但是由于 Google 在我遇到此问题时将我带到这里,因此我将添加对带有按钮的 DataGridTemplateColumn 有用的内容。
Change the binding from:
将绑定更改为:
CommandParameter="{Binding .}"
to
到
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"
Not sure why it works, but it did for me.
不知道为什么它有效,但它对我有用。
回答by Ralf Stauder
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.
忽略Tag
上下文菜单的特殊情况为基础的解决方法,这里的关键是要结合CommandParameter
定期,但结合了Command
与附加IsAsync=True
。这将延迟实际命令的绑定(及其CanExecute
调用),因此参数已经可用。不过,这意味着在短时间内,启用状态可能是错误的,但就我而言,这是完全可以接受的。
回答by Swythan
You may be able to use my CommandParameterBehavior
that I posted to the Prism forumsyesterday. It adds the missing behaviour where a change to the CommandParameter
cause the Command
to be re-queried.
您可以使用我昨天CommandParameterBehavior
在Prism 论坛上发布的内容。它添加了丢失的行为,其中更改CommandParameter
了Command
要重新查询的原因。
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.
这里有一些复杂性,因为我试图避免在PropertyDescriptor.AddValueChanged
没有稍后调用的情况下调用导致的内存泄漏PropertyDescriptor.RemoveValueChanged
。我尝试通过在卸载 ekement 时取消注册处理程序来解决这个问题。
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 RoutedCommand
s 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.
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();
}
}
}
}
回答by kkCosmo
After reading some good answers to similar questions I changed in your example the DelegateCommand slightly to make it work. Instead of using:
在阅读了类似问题的一些好的答案后,我在您的示例中稍微更改了 DelegateCommand 以使其工作。而不是使用:
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
这就是全部......这似乎确保在 Binding 更改时和 Execute 方法之后会调用 CanExecute
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
如果 ViewModel 发生更改,它不会自动触发,但如本线程中所述,可以通过在 GUI 线程上调用 CommandManager.InvalidateRequerySuggested
Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
回答by Joe Bako
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.
有一种相对简单的方法可以使用 DelegateCommand 来“修复”这个问题,尽管它需要更新 DelegateCommand 源并重新编译 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.
1) 下载 Prism 1.2 源代码并打开 CompositeApplicationLibrary_Desktop.sln。这里是包含 DelegateCommand 源的 Composite.Presentation.Desktop 项目。
2) Under the public event EventHandler CanExecuteChanged, modify to read as follows:
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) Under protected virtual void OnCanExecuteChanged(), modify it as follows:
3)在protected virtual void OnCanExecuteChanged()下,修改如下:
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.
4) 重新编译解决方案,然后导航到已编译的 DLL 所在的 Debug 或 Release 文件夹。将 Microsoft.Practices.Composite.Presentation.dll 和 .pdb(如果您愿意)复制到您引用外部程序集的位置,然后重新编译您的应用程序以提取新版本。
After this, CanExecute should be fired every time the UI renders elements bound to the DelegateCommand in question.
此后,每次 UI 呈现绑定到相关 DelegateCommand 的元素时,都应触发 CanExecute。
Take care, Joe
保重,乔
refereejoe at gmail
Gmail 的裁判员
回答by TravisWhidden
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().
其中一些答案是关于绑定到 DataContext 以获取 Command 本身,但问题是 CommandParameter 在不应该为 null 时为 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;
}
}