我有一个WPF应用程序,有两个按钮:搜索和属性。
MainWindow.xaml
<WrapPanel>
<Button Content="Search Data" Command="{Binding SearchCommand}" />
<Button Content="Properties" Command="{Binding PropertiesCommand}" />
</WrapPanel>
这两个按钮都使用在MainWindowViewModel中初始化的命令:
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
SearchCommand = new SearchDataCommand(this);
PropertiesCommand = new RelayCommand(OpenPropertiesWindow);
}
private async Task OpenPropertiesWindow()
{
PropertiesWindow propertiesWindow = new PropertiesWindow();
propertiesWindow.Owner = Application.Current.MainWindow;
propertiesWindow.ShowDialog();
}
}
单击“搜索”按钮时,将调用SearchCommand。SearchCommand的CanExecute方法检查是否设置了2个属性:
SearchDataCommand:
public override bool CanExecute(object parameter)
{
if (string.IsNullOrEmpty(Properties.Settings.Default.ApiKey))
{
// show message box that property is not set
return false;
}
if (string.IsNullOrEmpty(Properties.Settings.Default.UserId))
{
// show message box that property is not set
return false;
}
return !IsExecuting;
}
当我点击属性按钮时,我打开属性窗口,在那里我可以设置这些属性(ApiKey和UserId)。
public class PropertiesViewModel : INotifyPropertyChanged
{
//...
public PropertiesViewModel()
{
SaveCommand = new RelayCommand(SaveData, null);
ApiKey = Properties.Settings.Default.ApiKey;
UserId = Properties.Settings.Default.UserId;
}
private async Task SaveData()
{
Properties.Settings.Default.ApiKey = ApiKey;
Properties.Settings.Default.UserId = UserId;
Properties.Settings.Default.Save();
SaveCompleted?.Invoke(this, EventArgs.Empty); //Event is handled in PropertiesWindow to call Close() method
}
}
从MVVM的Angular 来看,将一个ViewModel传递给另一个ViewModel并不是一个好主意,因为它会导致视图模型之间不必要的耦合。
问题是当ApiKey和UserId属性被设置并保存在PropertiesViewModel中时,我需要在SearchDataCommand中重新计算CanExecute(以激活按钮)?如何在不违反MVVM原则的情况下正确执行?
根据@BionicCode建议更新。
主窗口:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
}
private void PropertiesBtn_OnClick(object sender, RoutedEventArgs e)
{
PropertiesWindow propertiesWindow = new PropertiesWindow
{
Owner = this
};
propertiesWindow.ShowDialog();
}
}
属性窗口:
public partial class PropertiesWindow : Window
{
private readonly PropertiesViewModel _propertiesViewModel;
public PropertiesWindow()
{
InitializeComponent();
_propertiesViewModel = new PropertiesViewModel();
DataContext = _propertiesViewModel;
_propertiesViewModel.SettingsRepository.SaveCompleted += PropertiesViewModel_SaveCompleted;
}
private void PropertiesViewModel_SaveCompleted(object sender, EventArgs e)
{
Close();
}
}
设置存储库:
public class SettingsRepository
{
public string ReadApiKey() => Properties.Settings.Default.ApiKey;
public string ReadUserId() => Properties.Settings.Default.UserId;
public void WriteApiKey(string apiKey)
{
Properties.Settings.Default.ApiKey = apiKey;
PersistData();
}
public void WriteUserId(string userId)
{
Properties.Settings.Default.UserId = userId;
PersistData();
}
private void PersistData()
{
Properties.Settings.Default.Save();
OnSaveCompleted();
}
public event EventHandler SaveCompleted;
private void OnSaveCompleted() => SaveCompleted?.Invoke(this, EventArgs.Empty);
}
搜索命令:
public class SearchCommand : ICommand
{
private bool _isExecuting;
public bool IsExecuting
{
get => _isExecuting;
set
{
_isExecuting = value;
OnCanExecuteChanged();
}
}
public SearchCommand(Action<object> executeSearchCommand, Func<object, bool> canExecuteSearchCommand)
{
ExecuteSearchCommand = executeSearchCommand;
CanExecuteSearchCommand = canExecuteSearchCommand;
}
public void InvalidateCommand() => OnCanExecuteChanged();
private Func<object, bool> CanExecuteSearchCommand { get; }
private Action<object> ExecuteSearchCommand { get; }
private event EventHandler CanExecuteChangedInternal;
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
CanExecuteChangedInternal += value;
}
remove
{
CommandManager.RequerySuggested -= value;
CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter) => CanExecuteSearchCommand?.Invoke(parameter) ?? !IsExecuting;
public void Execute(object parameter) => ExecuteSearchCommand(parameter);
private void OnCanExecuteChanged() => CanExecuteChangedInternal?.Invoke(this, EventArgs.Empty);
}
主视图模型:
public class MainViewModel : INotifyPropertyChanged
{
public SearchCommand SearchCommand { get; }
private SettingsRepository SettingsRepository { get; }
private readonly ISearchService _searchService;
#region INotifyProperties
//properties
#endregion
public MainViewModel(ISearchService searchService)
{
_searchService = searchService;
SettingsRepository = new SettingsRepository();
SettingsRepository.SaveCompleted += OnSettingsChanged;
SearchCommand = new SearchCommand(ExecuteSearchCommand, CanExecuteSearchDataCommand);
}
private bool CanExecuteSearchDataCommand(object parameter)
{
if (string.IsNullOrEmpty(SettingsRepository.ReadApiKey()))
{
MessageBox.Show(
"Set API Key in the application properties.",
"Configuration Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
return false;
}
if (string.IsNullOrEmpty(SettingsRepository.ReadUserId()))
{
MessageBox.Show(
"Set User id in the application properties.",
"Configuration Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
return false;
}
return !SearchCommand.IsExecuting;
}
private async void ExecuteSearchCommand(object parameter)
{
SearchCommand.IsExecuting = true;
await ExecuteSearchCommandAsync(parameter);
SearchCommand.IsExecuting = false;
}
private async Task ExecuteSearchCommandAsync(object parameter)
{
//Search logic with setting INotifyProperties with results
}
private void OnSettingsChanged(object sender, EventArgs e) => SearchCommand.InvalidateCommand();
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
属性视图模型:
public class PropertiesViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ICommand SaveCommand { get; }
public SettingsRepository SettingsRepository { get; }
#region INotifyProperties
//properties
#endregion
public PropertiesViewModel()
{
SettingsRepository = new SettingsRepository();
ApiKey = SettingsRepository.ReadApiKey();
UserId = SettingsRepository.ReadUserId();
SaveCommand = new RelayCommand(SaveData, null);
}
private async Task SaveData()
{
SettingsRepository.WriteApiKey(ApiKey);
SettingsRepository.WritemUserId(UserId);
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
我仍然不确定当前的类设计是否违反了MVVM原则。最后我得到了一些结果。但以下是让我困惑的事情:
- 视图模型有单独的
SettingsRepository
对象。MainViewModel
订阅SettingsRepository.SaveCompleted
来调用InvalidateCommand
,而PropertiesViewModel
订阅自己的示例只是为了关闭PropertiesWindow
,这样可以吗?看起来有点混乱。 - 由于某种原因,
CanExecuteSearchDataCommand
被调用了两次,看起来InvalidateCommand
做了太多的工作,而这里的松耦合代价是。我的意思是,它似乎对MessageBox关闭做出了React。如果我删除MessageBox并返回false,那么它就像预期的那样工作。 - 除了MSDN之外,是否有任何资源可以阅读这些机制以及如何正确使用它们(如
InvalidateCommand
)的详细信息?
2条答案
按热度按时间ttvkxqim1#
您当前的解决方案打破了MVVM模式,因此产生了您目前正在努力解决的问题。
下面的解决方案通过修复MVVM违规和改进类设计,优雅地解决了您的问题:
1.因为数据持久化是 Model 的责任,所以你通常会将实际的读/写操作 Package 到一个类中,以便 View Model 类可以使用,最好作为共享示例。这允许任何类(例如
MainWindowViewModel
)来观察数据存储库的数据更改。这现在是可能的,因为读/写操作不再分布在应用程序中。* 视图模型 *不应该知道 Model 正在使用的底层数据存储的任何细节(例如,数据是使用文件系统还是数据库持久化的)。1.然后让命令公开一个
InvalidateComand()
方法,该方法显式引发ICommand.CanExecuteChanged
事件。请注意,当将
CanExecuteChanged
事件委托给CommandManager.RequeryREquested
事件时,WPF将自动调用ICommand.CanExecute
(例如在鼠标移动时)。这样,你的类
MainWindowViewModel
和PropertiesViewModel
就完全独立了。它们都依赖于仓库:一个用于写入用户设置,另一个用于观察数据变化。MainWindowViewModel.cs
SearchDataCommand.cs
用户设置仓库.cs
MainWindow.xaml.cs
PropertyViewModel.cs
eaf3rand2#
由于
SearchDataCommand
依赖于MainWindowViewModel
,并且视图模型可以在传递给命令后更改,因此应该有一种方法来通知命令视图模型更改的事实,因此它可以自己发出CanExecuteChanged
。我将向视图模型添加一个
Changed
事件,在每次调用PropertyChanged
时触发该事件,并在命令中订阅它。编辑:如果
PropertiesViewModel
只是在PropertiesWindow
中使用,并且与任何东西都没有连接(关于本文的范围),您应该考虑一些消息传递功能。创建一个
PropertiesChangedMessage
并从PropertiesViewModel
发布。然后订阅SearchCommand
中的消息并触发CanExecuteChanged
事件。对于这样的消息传递,可以看看community toolkit。