Параметр MVVM ICommand.CanExecute содержит предыдущее значение
Мне трудно понять, почему ICommand.CanExecutes всегда содержит предыдущее значение вместо нового, если вместо обычного свойства используется вложенное свойство.
Проблема описана ниже, и я серьезно не могу найти способ исправить это, кроме использования некоторой формы шаблона "Фасад", где я создаю свойства в модели представления и подключаю их к соответствующему свойству в модели.
Или используйте чертово событие CommandManager.RequerySuggested. Причина, по которой это не является оптимальным, состоит в том, что представление представляет более 30 команд, просто считая меню, и, если все CanExecute обновляются каждый раз, когда что-то меняется, обновление всех элементов меню / кнопок займет несколько секунд. Даже используя приведенный ниже пример только с одной командой и кнопкой вместе с менеджером команд, для включения / выключения кнопки требуется около 500 мс.
Единственная причина, по которой я могу придумать, заключается в том, что привязка CommandParameter не обновляется до запуска CanExecute, и тогда, я думаю, вы ничего не можете с этим поделать.
Заранее спасибо:!
Например
Допустим, у нас есть эта базовая модель
public class BasicViewModel : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set {
this.name = value;
RaisePropertyChanged("Name");
Command.RaiseCanExecuteChanged();
}
}
private Project project;
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
if (value != null) value.PropertyChanged += ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
}
}
private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) {
Command.RaiseCanExecuteChanged();
}
public DelegateCommand<string> Command { get; set; }
public BasicViewModel()
{
this.Project = new Example.Project();
Command = new DelegateCommand<string>(this.Execute, this.CanExecute);
}
private bool CanExecute(string arg) {
return !string.IsNullOrWhiteSpace(arg);
}
private void Execute(string obj) { }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null) {
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
и эта модель
public class Project : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
RaisePropertyChanged("Text");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName = null)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Теперь, на мой взгляд, у меня есть это текстовое поле и кнопка.
<Button Content="Button" CommandParameter="{Binding Path=Project.Text}" Command="{Binding Path=Command}" />
<TextBox Text="{Binding Path=Project.Text, UpdateSourceTrigger=PropertyChanged}" />
Это работает, каждый раз, когда я набираю что-то в текстовом поле, вызывается CanExecute, НО для параметра всегда устанавливается предыдущее значение. Допустим, я пишу 'H' в текстовом поле, CanExecute запускается с параметром, установленным в NULL. Затем я пишу "E", теперь текстовое поле содержит "HE", и CanExecute запускается снова. На этот раз с параметром, установленным только на "H".
По какой-то странной причине параметр всегда устанавливается на предыдущее значение, и когда я проверяю Project.Text, он устанавливается на "HE", но параметр по-прежнему устанавливается на "H".
Если я теперь изменить параметр команды на
CommandParameter="{Binding Path=Name}"
и Textbox.Text to
Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"
все работает отлично. Параметр CanExecute всегда содержит самое последнее значение, а не предыдущее значение.
3 ответа
Я нашел это отличное свойство от swythan на дискуссионном форуме prism codeplex, который отлично справился с задачей. Конечно, это не отвечает, почему для параметра команды установлено предыдущее значение, но это хорошо решает проблему.
Код немного изменен из исходного кода, что дает возможность использовать его в элементах управления в TabItem, вызывая HookCommandParameterChanged при вызове события OnLoaded.
public static class CommandParameterBehavior
{
public static readonly DependencyProperty IsCommandRequeriedOnChangeProperty =
DependencyProperty.RegisterAttached("IsCommandRequeriedOnChange",
typeof(bool),
typeof(CommandParameterBehavior),
new UIPropertyMetadata(false, new PropertyChangedCallback(OnIsCommandRequeriedOnChangeChanged)));
public static bool GetIsCommandRequeriedOnChange(DependencyObject target)
{
return (bool)target.GetValue(IsCommandRequeriedOnChangeProperty);
}
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;
fe.Loaded -= OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded += OnUnloaded;
fce.Loaded -= OnLoaded;
}
}
private static void UnhookUnloaded(object source)
{
var fe = source as FrameworkElement;
if (fe != null)
{
fe.Unloaded -= OnUnloaded;
fe.Loaded += OnLoaded;
}
var fce = source as FrameworkContentElement;
if (fce != null)
{
fce.Unloaded -= OnUnloaded;
fce.Loaded += OnLoaded;
}
}
static void OnLoaded(object sender, RoutedEventArgs e)
{
HookCommandParameterChanged(sender);
}
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();
}
}
Источник: https://compositewpf.codeplex.com/discussions/47338
Шаблон фасада, о котором вы говорите, это стандартная практика WPF. Основная проблема в том, как вы это делаете, заключается в том, что при возникновении событий их обработчики событий с подпиской выполняются в том порядке, в котором они подписаны. Строка кода, где у вас есть:
if (value != null) value.PropertyChanged += ChildPropertyChanged;
Это подписывается на событие "PropertyChanged" вашего класса "Project". Ваши элементы UIE также подписаны на то же событие "PropertyChanged" через вашу привязку в XAML. Короче говоря, ваше событие "PropertyChanged" теперь имеет 2 подписчика.
Суть событий в том, что они запускаются в последовательности, и то, что происходит в вашем коде, заключается в том, что когда событие вызывается из вашего "Project.Text", оно выполняет ваше событие "ChildPropertyChanged", вызывая ваше событие "CanExecuteChanged", которое, наконец, запускает ваше Функция "CanExecute" (когда вы видите неправильный параметр). ПОТОМ, после этого ваши UIElements запускают свои EventHandlers этим же событием. И их значения обновляются.
Это порядок ваших подписок, вызывающих проблему. Попробуйте это и скажите мне, если это решит вашу проблему:
public Project Project
{
get { return project; }
set {
if (project != null) project.PropertyChanged -= ChildPropertyChanged;
project = value;
RaisePropertyChanged("Project");
if (project != null) project.PropertyChanged += ChildPropertyChanged;
}
}
Вот как бы я это сделал, и все работает как положено. Единственная разница здесь в том, что я использую RelayCommand вместо DelegateCommand - они в основном имеют одинаковую реализацию, поэтому они должны быть взаимозаменяемыми.
Когда пользователь вводит текст и затем нажимает кнопку, метод execute объекта RelayCommand получает ожидаемый текст - просто.
XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Grid.Column="0"
Grid.Row="0"
Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Column="0"
Grid.Row="1"
Content="Test"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Command="{Binding Path=TextCommand, Mode=OneWay}" />
</Grid>
ViewModel:
public sealed class ExampleViewModel : BaseViewModel
{
private string _text;
public ExampleViewModel()
{
TextCommand = new RelayCommand(TextExecute, CanTextExecute);
}
public string Text
{
get
{
return _text;
}
set
{
_text = value;
OnPropertyChanged("Text");
}
}
public ICommand TextCommand { get; private set; }
private void TextExecute()
{
// Do something with _text value...
}
private bool CanTextExecute()
{
return true;
}
}