WPF CommandParameter имеет значение NULL при первом вызове CanExecute
Я столкнулся с проблемой с WPF и командами, которые связаны с кнопкой внутри DataTemplate ItemsControl. Сценарий довольно прямой. ItemsControl привязан к списку объектов, и я хочу иметь возможность удалить каждый объект в списке, нажав кнопку. Кнопка выполняет команду, а команда заботится об удалении. CommandParameter привязан к объекту, который я хочу удалить. Таким образом, я знаю, что пользователь нажал. Пользователь должен иметь возможность удалять только свои "собственные" объекты - поэтому мне нужно выполнить некоторые проверки в вызове "CanExecute" команды, чтобы убедиться, что у пользователя есть необходимые разрешения.
Проблема в том, что параметр, переданный в CanExecute, имеет значение NULL при первом вызове, поэтому я не могу запустить логику для включения / отключения команды. Однако, если я включу его всегда, а затем нажму кнопку, чтобы выполнить команду, CommandParameter будет передан правильно. Это означает, что связывание с CommandParameter работает.
XAML для ItemsControl и DataTemplate выглядит следующим образом:
<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>
Итак, как вы можете видеть, у меня есть список объектов комментариев. Я хочу, чтобы CommandParameter DeleteCommentCommand был привязан к объекту Command.
Поэтому я думаю, что мой вопрос: кто-нибудь сталкивался с этой проблемой раньше? CanExecute вызывается в моей Команде, но параметр всегда NULL в первый раз - почему это так?
Обновление: я смог немного сузить проблему. Я добавил пустой Debug ValueConverter, чтобы вывести сообщение, когда CommandParameter привязан к данным. Оказывается, проблема в том, что метод CanExecute выполняется до того, как CommandParameter будет привязан к кнопке. Я попытался установить CommandParameter перед командой (как предложено) - но он все еще не работает. Любые советы о том, как это контролировать.
Обновление 2: Есть ли способ определить, когда привязка "выполнена", чтобы я мог принудительно выполнить повторную оценку команды? Кроме того - это проблема, что у меня есть несколько кнопок (по одной для каждого элемента в ItemsControl), которые связаны с одним и тем же экземпляром объекта Command?
Обновление 3: я загрузил репродукцию ошибки в мой SkyDrive: http://cid-1a08c11c407c0d8e.skydrive.live.com/self.aspx/Code%20samples/CommandParameterBinding.zip
16 ответов
Я наткнулся на похожую проблему и решил ее, используя мой надежный 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 вместо. добиться того же эффекта.
У меня возникла та же проблема при попытке привязать команду к моей модели представления.
Я изменил его, чтобы использовать относительную привязку источника вместо ссылки на элемент по имени, и это помогло. Привязка параметров не изменилась.
Старый код:
Command="{Binding DataContext.MyCommand, ElementName=myWindow}"
Новый код:
Command="{Binding DataContext.MyCommand, RelativeSource={RelativeSource AncestorType=Views:MyView}}"
Обновление: я только столкнулся с этой проблемой, не используя ElementName, я привязываю команду к моей модели представления, и мой контекст данных кнопки - моя модель представления. В этом случае мне пришлось просто переместить атрибут CommandParameter перед атрибутом Command в объявлении Button (в XAML).
CommandParameter="{Binding Groups}"
Command="{Binding StartCommand}"
Я обнаружил, что порядок, в котором я устанавливаю Command и CommandParameter, имеет значение. Установка свойства Command приводит к немедленному вызову CanExecute, поэтому вы хотите, чтобы CommandParameter уже был установлен в этой точке.
Я обнаружил, что изменение порядка свойств в XAML действительно может иметь эффект, хотя я не уверен, что это решит вашу проблему. Хотя стоит попробовать.
Похоже, вы предполагаете, что кнопка никогда не станет активной, что удивительно, поскольку я ожидаю, что CommandParameter будет установлен вскоре после свойства Command в вашем примере. Вызывает ли кнопка CommandManager.InvalidateRequerySuggested() активацию кнопки?
Я знаю, что эта ветка довольно старая, но у меня есть еще одна возможность обойти эту проблему, которой я хотел бы поделиться. Поскольку метод 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}" />
Я надеюсь, что это, возможно, поможет кому-то еще с этой проблемой.
Это старая ветка, но так как Google привел меня сюда, когда у меня возникла эта проблема, я добавлю то, что у меня работает, для DataGridTemplateColumn с помощью кнопки.
Измените привязку с:
CommandParameter="{Binding .}"
в
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}"
Не уверен, почему это работает, но это сделал для меня.
Недавно я столкнулся с той же проблемой (для меня это было для пунктов меню в контекстном меню), но, хотя это может быть не подходящее решение для каждой ситуации, я нашел другой (и намного более короткий!) Способ решения этой проблемы. проблема:
<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
call) немного, поэтому параметр уже будет доступен. Однако это означает, что на короткое время состояние включения могло быть неправильным, но для моего случая это было вполне приемлемо.
Вы можете использовать мой CommandParameterBehavior
что я отправил на форумы Prism вчера. Это добавляет недостающее поведение, где изменение CommandParameter
вызвать Command
быть перепрошен.
Здесь есть некоторая сложность, вызванная моими попытками избежать утечки памяти, вызванной PropertyDescriptor.AddValueChanged
без последующего звонка PropertyDescriptor.RemoveValueChanged
, Я пытаюсь исправить это, отменив регистрацию обработчика при выгрузке ekement.
Вы, вероятно, должны будете удалить IDelegateCommand
вещи, если вы не используете Prism (и хотите внести те же изменения, что и я, в библиотеку Prism). Также обратите внимание, что мы обычно не используем RoutedCommand
здесь (мы используем призму 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();
}
}
}
}
Прочитав несколько хороших ответов на похожие вопросы, я немного изменил в вашем примере 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 изменяется, но, как упоминалось в этом потоке, это возможно, вызывая CommandManager.InvalidateRequerySuggested в потоке GUI.
Application.Current?.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)CommandManager.InvalidateRequerySuggested);
В .NET 7.0 RC1 это было исправлено.
(По крайней мере, в каком-то смысле...)
Теперь он должен автоматически пересчитываться при каждом изменении, в том числе при инициализации.
Хотя это не мешает первоначальному вызовуCanExecute()
когдаCommandParameter
все ещеnull
, многоICommand
реализации уже должны обрабатывать это, и это делает устаревшим неясный и проблематичный обходной путь / взлом упорядочения атрибутов XAML.
Как сообщил @Daniel-Svensson в комментарии GitHub :
фактическая проблема здесь заключается в том, что ICommand.CanExecute не переоценивается при изменении значения, привязанного к CommandParameter. Это, очевидно, было бы правильным поведением, поскольку параметр команды передается в CanExecute, поэтому все интуитивно ожидают такого поведения.
И это то, что исправляется.
По словам @pchaurasia14, старшего технического менеджера по WPF в Microsoft:
Это было исправлено в выпуске RC1. Вы можете попробовать это. ... Я имел в виду .NET 7 RC1.
Проблема отслеживания GitHub № 316 в проекте dotnet/wpf указана как закрытая. Изменение кода CommandParameter делает недействительным CanExecute #4217, включенный в .NET 7.0 RC1. Он был объединен 21 июля 2022 года и включен в список коммитов (прокрутите вниз) для релиза RC1 .
Существует относительно простой способ "исправить" эту проблему с 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) В защищенном виртуальном void OnCanExecuteChanged() измените его следующим образом:
protected virtual void OnCanExecuteChanged()
{
// add this line
CommandManager.InvalidateRequerySuggested();
WeakEventHandlerManager.CallWeakReferenceHandlers( this, _canExecuteChangedHandlers );
}
4) Перекомпилируйте решение, затем перейдите в папку Debug или Release, где находятся скомпилированные библиотеки DLL. Скопируйте файлы Microsoft.Practices.Composite.Presentation.dll и.pdb (если вы хотите) туда, куда вы ссылаетесь на ваши внешние сборки, а затем перекомпилируйте приложение, чтобы получить новые версии.
После этого CanExecute должен запускаться каждый раз, когда пользовательский интерфейс отображает элементы, связанные с рассматриваемым DelegateCommand.
Береги себя, Джо
refereejoe в Gmail
Некоторые из этих ответов касаются привязки к DataContext для получения самой команды, но вопрос заключался в том, что CommandParameter имеет значение null, а это не должно быть. Мы тоже это испытали. В догадках, мы нашли очень простой способ заставить это работать в нашей ViewModel. Это специально для проблемы с пустым значением CommandParameter, о которой сообщает клиент, с одной строкой кода. Обратите внимание на 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;
}
}
Я зарегистрировал это как ошибку в WPF в.Net 4.0, так как проблема все еще существует в бета-версии 2.
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=504976
Эй, Джонас, не уверен, будет ли это работать в шаблоне данных, но вот синтаксис привязки, который я использую в контекстном меню ListView, чтобы получить текущий элемент в качестве параметра команды:
CommandParameter = "{Binding RelativeSource = {RelativeSource AncestorType = ContextMenu}, Path = PlacementTarget.SelectedItem, Mode = TwoWay}"
Помимо предложения Эда Болла об установке CommandParameter до Command, убедитесь, что у вашего метода CanExecute есть параметр типа объекта.
private bool OnDeleteSelectedItemsCanExecute (объект SelectedItems)
{
// Your goes heres
}
Надеюсь, это предотвратит трату огромного количества времени, которое я потратил, чтобы выяснить, как получить SelectedItems в качестве параметра CanExecute
Команда Manager.InvalidateRequerySuggested работает и для меня. Я полагаю, что следующая ссылка говорит о подобной проблеме, и M$ dev подтвердил ограничение в текущей версии, а команда Manager.InvalidateRequerySuggested является обходным решением. http://social.expression.microsoft.com/Forums/en-US/wpf/thread/c45d2272-e8ba-4219-bb41-1e5eaed08a1f/
Что важно, так это время вызова commandManager.InvalidateRequerySuggested. Это должно быть вызвано после уведомления о соответствующем изменении значения.
Это длинный выстрел. чтобы отладить это, вы можете попробовать:
- проверка события PreviewCanExecute.
- используйте snoop/wpf mole, чтобы заглянуть внутрь и посмотреть, что такое параметр команды.
НТН,