如何让 DelegateCommand CanExecute 使用 MVVM 更新 WPF UI?
How do I get DelegateCommand CanExecute to update WPF UI using MVVM?
在 this question 的基础上,我正在尝试制作一个带有两个按钮的 UI。按下一个按钮会禁用自身并启用另一个按钮。我正在使用 MVVM 模式和 DelegateCommand:
主窗口XAML
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<StackPanel>
<Rectangle Height="100"></Rectangle>
<Button Command="{Binding StartServer}" >Start Server</Button>
<Button Command="{Binding StopServer}" >Stop Server</Button>
</StackPanel>
</Window>
ViewModel
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
PropertyChanged += PropertyChangedHandler;
}
private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(ServerIsRunning):
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
break;
}
}
private bool m_serverIsRunning;
public bool ServerIsRunning
{
get => m_serverIsRunning;
set
{
m_serverIsRunning = value;
RaisePropertyChanged(nameof(ServerIsRunning));
}
}
public DelegateCommand<object> StartServer => new DelegateCommand<object>(
context =>
{
ServerIsRunning = true;
}, e => !ServerIsRunning);
public DelegateCommand<object> StopServer => new DelegateCommand<object>(
context =>
{
ServerIsRunning = false;
}, e => ServerIsRunning);
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
DelegateCommand
public class DelegateCommand<T> : ICommand
{
private readonly Action<object> ExecuteAction;
private readonly Func<object, bool> CanExecuteFunc;
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteFunc)
{
ExecuteAction = executeAction;
CanExecuteFunc = canExecuteFunc;
}
public bool CanExecute(object parameter)
{
return CanExecuteFunc == null || CanExecuteFunc(parameter);
}
public void Execute(object parameter)
{
ExecuteAction(parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
问题是,当我点击 Start Server
按钮时,按钮的 IsEnabled 状态在 UI 中没有改变。我是否使用不当的 DelegateCommand?
是这样的吗?
public class ViewModel : INotifyPropertyChanged
{
bool _isServerStarted;
public ViewModel()
{
StartServer = new DelegateCommand(OnStartServerExecute, OnStartServerCanExecute);
StopServer = new DelegateCommand(OnStopServerExecute, OnStopServerCanExecute);
}
void OnStartServerExecute()
{
IsServerStarted = true;
}
bool OnStartServerCanExecute()
{
return !IsServerStarted;
}
void OnStopServerExecute()
{
IsServerStarted = false;
}
bool OnStopServerCanExecute()
{
return IsServerStarted;
}
void RaiseCanExecuteChanged()
{
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
}
public bool IsServerStarted
{
get => _isServerStarted;
set
{
_isServerStarted = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsServerStarted)));
RaiseCanExecuteChanged();
}
}
public DelegateCommand StartServer { get; }
public DelegateCommand StopServer { get; }
public event PropertyChangedEventHandler PropertyChanged;
}
您只需要使用 CommandManager.InvalidateRequerySuggested() 来更新按钮状态。
您的代码的问题在于您的命令属性使用表达式主体,每次有人获得 属性 时都会执行它们,这意味着每次您请求 StartServer
命令时它都会给您一个新实例。您可以通过在 getter 上放置一个断点来自己检查这一点,您会看到它会被击中几次并始终创建委托命令的新实例。
为了使您的代码正常工作,您必须只使用每个命令的一个实例,您可以通过使用支持字段或只读 属性 并将其设置在构造函数中来实现:
public MainWindowViewModel()
{
PropertyChanged += PropertyChangedHandler;
StartServer = new DelegateCommand<object>(
context =>
{
ServerIsRunning = true;
}, e => !ServerIsRunning);
StopServer = new DelegateCommand<object>(
context => {
ServerIsRunning = false;
}, e => ServerIsRunning);
}
public DelegateCommand<object> StartServer
{ get; }
public DelegateCommand<object> StopServer
{ get; }
UI 只会监听它所绑定的命令实例引发的 CanExecuteChanged
事件(通过 Command={Binding ...}
.
您的代码也可以简化,因为您可以直接调用 RaiseCanExecuteChanged
而不是引发 PropertyChangedEvent
并自己倾听:
public bool ServerIsRunning
{
get => m_serverIsRunning;
set
{
m_serverIsRunning = value;
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
}
}
在 this question 的基础上,我正在尝试制作一个带有两个按钮的 UI。按下一个按钮会禁用自身并启用另一个按钮。我正在使用 MVVM 模式和 DelegateCommand:
主窗口XAML
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<StackPanel>
<Rectangle Height="100"></Rectangle>
<Button Command="{Binding StartServer}" >Start Server</Button>
<Button Command="{Binding StopServer}" >Stop Server</Button>
</StackPanel>
</Window>
ViewModel
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
PropertyChanged += PropertyChangedHandler;
}
private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(ServerIsRunning):
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
break;
}
}
private bool m_serverIsRunning;
public bool ServerIsRunning
{
get => m_serverIsRunning;
set
{
m_serverIsRunning = value;
RaisePropertyChanged(nameof(ServerIsRunning));
}
}
public DelegateCommand<object> StartServer => new DelegateCommand<object>(
context =>
{
ServerIsRunning = true;
}, e => !ServerIsRunning);
public DelegateCommand<object> StopServer => new DelegateCommand<object>(
context =>
{
ServerIsRunning = false;
}, e => ServerIsRunning);
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
DelegateCommand
public class DelegateCommand<T> : ICommand
{
private readonly Action<object> ExecuteAction;
private readonly Func<object, bool> CanExecuteFunc;
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteFunc)
{
ExecuteAction = executeAction;
CanExecuteFunc = canExecuteFunc;
}
public bool CanExecute(object parameter)
{
return CanExecuteFunc == null || CanExecuteFunc(parameter);
}
public void Execute(object parameter)
{
ExecuteAction(parameter);
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
问题是,当我点击 Start Server
按钮时,按钮的 IsEnabled 状态在 UI 中没有改变。我是否使用不当的 DelegateCommand?
是这样的吗?
public class ViewModel : INotifyPropertyChanged
{
bool _isServerStarted;
public ViewModel()
{
StartServer = new DelegateCommand(OnStartServerExecute, OnStartServerCanExecute);
StopServer = new DelegateCommand(OnStopServerExecute, OnStopServerCanExecute);
}
void OnStartServerExecute()
{
IsServerStarted = true;
}
bool OnStartServerCanExecute()
{
return !IsServerStarted;
}
void OnStopServerExecute()
{
IsServerStarted = false;
}
bool OnStopServerCanExecute()
{
return IsServerStarted;
}
void RaiseCanExecuteChanged()
{
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
}
public bool IsServerStarted
{
get => _isServerStarted;
set
{
_isServerStarted = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsServerStarted)));
RaiseCanExecuteChanged();
}
}
public DelegateCommand StartServer { get; }
public DelegateCommand StopServer { get; }
public event PropertyChangedEventHandler PropertyChanged;
}
您只需要使用 CommandManager.InvalidateRequerySuggested() 来更新按钮状态。
您的代码的问题在于您的命令属性使用表达式主体,每次有人获得 属性 时都会执行它们,这意味着每次您请求 StartServer
命令时它都会给您一个新实例。您可以通过在 getter 上放置一个断点来自己检查这一点,您会看到它会被击中几次并始终创建委托命令的新实例。
为了使您的代码正常工作,您必须只使用每个命令的一个实例,您可以通过使用支持字段或只读 属性 并将其设置在构造函数中来实现:
public MainWindowViewModel()
{
PropertyChanged += PropertyChangedHandler;
StartServer = new DelegateCommand<object>(
context =>
{
ServerIsRunning = true;
}, e => !ServerIsRunning);
StopServer = new DelegateCommand<object>(
context => {
ServerIsRunning = false;
}, e => ServerIsRunning);
}
public DelegateCommand<object> StartServer
{ get; }
public DelegateCommand<object> StopServer
{ get; }
UI 只会监听它所绑定的命令实例引发的 CanExecuteChanged
事件(通过 Command={Binding ...}
.
您的代码也可以简化,因为您可以直接调用 RaiseCanExecuteChanged
而不是引发 PropertyChangedEvent
并自己倾听:
public bool ServerIsRunning
{
get => m_serverIsRunning;
set
{
m_serverIsRunning = value;
StartServer.RaiseCanExecuteChanged();
StopServer.RaiseCanExecuteChanged();
}
}