MVVM ICommand.CanExecute 参数包含以前的值
MVVM ICommand.CanExecute parameter contains previous value
我很难理解为什么如果使用嵌套的 属性 而不是正常的 属性,那么 ICommand.CanExecutes 总是包含以前的值而不是新值。
问题在下面描述,除了使用某种形式的 "Facade" 模式之外,我真的想不出解决这个问题的方法,我在视图模型中创建属性并将它们挂钩到它们相应的 属性 在模型中。
或者使用该死的 CommandManager.RequerySuggested 事件。这不是最佳的原因是因为视图显示超过 30 个命令,仅计算菜单,如果每次更改时所有 CanExecute 更新,所有菜单项/按钮更新将需要几秒钟。即使使用下面的示例,仅使用一个命令和按钮以及命令管理器,按钮到 enable/disable 本身也需要大约 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 到
Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"
一切正常。 CanExecute 参数始终包含最新值而不是以前的值。
您所说的外观模式是标准 WPF 实践。你这样做的主要问题是当事件被引发时,他们订阅的事件处理程序按照他们订阅的顺序执行。您拥有的代码行:
if (value != null) value.PropertyChanged += ChildPropertyChanged;
这订阅了您的 "Project" class 的 "PropertyChanged" 事件。您的 UIElement 也通过您在 XAML 中的绑定订阅了相同的 "PropertyChanged" 事件。简而言之,您的 "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 - 它们基本上具有相同的实现,因此它们应该可以互换。
当用户输入文本然后单击按钮时,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>
视图模型:
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;
}
}
我在 prism codeplex 讨论论坛上发现了来自 swythan 的这个很棒的附件 属性,它做得很好。当然它没有回答为什么将命令参数设置为以前的值,但它以一种很好的方式解决了这个问题。
对源代码稍作修改,可以在调用 OnLoaded 事件时通过调用 HookCommandParameterChanged 在 TabItem 中的控件上使用它。
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();
}
}
我很难理解为什么如果使用嵌套的 属性 而不是正常的 属性,那么 ICommand.CanExecutes 总是包含以前的值而不是新值。
问题在下面描述,除了使用某种形式的 "Facade" 模式之外,我真的想不出解决这个问题的方法,我在视图模型中创建属性并将它们挂钩到它们相应的 属性 在模型中。
或者使用该死的 CommandManager.RequerySuggested 事件。这不是最佳的原因是因为视图显示超过 30 个命令,仅计算菜单,如果每次更改时所有 CanExecute 更新,所有菜单项/按钮更新将需要几秒钟。即使使用下面的示例,仅使用一个命令和按钮以及命令管理器,按钮到 enable/disable 本身也需要大约 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 到
Text={Binding Path=Name, UpdateSourceTrigger=PropertyChanged}"
一切正常。 CanExecute 参数始终包含最新值而不是以前的值。
您所说的外观模式是标准 WPF 实践。你这样做的主要问题是当事件被引发时,他们订阅的事件处理程序按照他们订阅的顺序执行。您拥有的代码行:
if (value != null) value.PropertyChanged += ChildPropertyChanged;
这订阅了您的 "Project" class 的 "PropertyChanged" 事件。您的 UIElement 也通过您在 XAML 中的绑定订阅了相同的 "PropertyChanged" 事件。简而言之,您的 "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 - 它们基本上具有相同的实现,因此它们应该可以互换。
当用户输入文本然后单击按钮时,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>
视图模型:
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;
}
}
我在 prism codeplex 讨论论坛上发现了来自 swythan 的这个很棒的附件 属性,它做得很好。当然它没有回答为什么将命令参数设置为以前的值,但它以一种很好的方式解决了这个问题。
对源代码稍作修改,可以在调用 OnLoaded 事件时通过调用 HookCommandParameterChanged 在 TabItem 中的控件上使用它。
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();
}
}