WPF:如何在一个ViewModel中的ICommand中重新计算CanExecute?

lzfw57am  于 2023-04-07  发布在  其他
关注(0)|答案(2)|浏览(130)

我有一个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)的详细信息?
ttvkxqim

ttvkxqim1#

您当前的解决方案打破了MVVM模式,因此产生了您目前正在努力解决的问题。

  • View Model 不允许处理/引用控件或参与UI逻辑。这意味着,不允许显示来自 View Model 的对话框。所有与UI相关的(对话框是UI)都必须在 View 中处理。
  • View Model 不负责持久化数据。这是 Model 的责任。

下面的解决方案通过修复MVVM违规和改进类设计,优雅地解决了您的问题:
1.因为数据持久化是 Model 的责任,所以你通常会将实际的读/写操作 Package 到一个类中,以便 View Model 类可以使用,最好作为共享示例。这允许任何类(例如MainWindowViewModel)来观察数据存储库的数据更改。这现在是可能的,因为读/写操作不再分布在应用程序中。* 视图模型 *不应该知道 Model 正在使用的底层数据存储的任何细节(例如,数据是使用文件系统还是数据库持久化的)。
1.然后让命令公开一个InvalidateComand()方法,该方法显式引发ICommand.CanExecuteChanged事件。
请注意,当将CanExecuteChanged事件委托给CommandManager.RequeryREquested事件时,WPF将自动调用ICommand.CanExecute(例如在鼠标移动时)。
这样,你的类MainWindowViewModelPropertiesViewModel就完全独立了。它们都依赖于仓库:一个用于写入用户设置,另一个用于观察数据变化。

MainWindowViewModel.cs

class MainWindowViewModel
{
  public MainWindowViewModel(UserSettingsRepository userSettingsRepository)
  {
    // Use the Model to read and write data
    this.UserSettingsRepository = userSettingsRepository;

    // Because the repository is used application wide we now have a single object to observe for setting changes.
    // When the repository reports changes, we explicitly invalidate the command
    this.UserSettingsRepository.SaveCompleted += OnUserSettingsChanged;

    this.SearchDataCommand = new SearchDataCommand(ExecuteSearchDataCommand, CanExecuteSearchDataCommand);
  }

  private void OnUserSettingsChnaged(object sender, EventArgs e) => this.SearchDataCommand.InvalidateCommand();

  private void ExecuteSearchDataCommand(object? obj) => throw new NotImplementedException();
  private bool CanExecuteSearchDataCommand(object? arg) => throw new NotImplementedException();

  public SearchDataCommand SearchDataCommand { get; }
  private UserSettingsRepository UserSettingsRepository { get; }
}

SearchDataCommand.cs

class SearchDataCommand : ICommand
{
  public SearchDataCommand(Action<object?> executeSearchDataCommand, Func<object?, bool> canExecuteSearchDataCommand)
  {
    this.ExecuteSearchDataCommand = executeSearchDataCommand;
    this.CanExecuteSearchDataCommand = canExecuteSearchDataCommand;
  }

  public void InvalidateCommand() => OnCanExecuteChanged();

  public bool CanExecute(object? parameter) => this.CanExecuteSearchDataCommand?.Invoke(parameter) ?? true;
  public void Execute(object? parameter) => this.ExecuteSearchDataCommand(parameter);

  private void OnCanExecuteChanged() => this.CanExecuteChangedInternal?.Invoke(this, EventArgs.Empty);

  public event EventHandler? CanExecuteChanged
  {
    add
    {
      CommandManager.RequerySuggested += value;
      this.CanExecuteChangedInternal += value;
    }
    remove
    {
      CommandManager.RequerySuggested -= value;
      this.CanExecuteChangedInternal -= value;
    }
  }

  private event EventHandler CanExecuteChangedInternal;
  private Action<object?> ExecuteSearchDataCommand { get; }
  private Func<object?, bool> CanExecuteSearchDataCommand { get; }
}

用户设置仓库.cs

// A class of the Model (MVVM)
class UserSettingsRepository
{
  public void WriteApiKey(string apiKey)
  {
    Properties.Settings.Default.ApiKey = apiKey;
    PersistUserData();
  }

  public void WriteUserId(string userId)
  {
    Properties.Settings.Default.UserId = userId;
    PersistUserData();
  }

  public string ReadApiKey(string apiKey) => Properties.Settings.Default.ApiKey = apiKey;

  public string ReadUserId(string userId) => Properties.Settings.Default.UserId = userId;

  private PersistUserData()
  {
    Properties.Settings.Default.Save();
    OnSaveCompleted(); 
  }

  private void OnSaveCompleted => SaveCompleted?.Invoke(this, EventArgs.Empty); //Event is handled in PropertiesWindow to call Close() method    
}

MainWindow.xaml.cs

partial class MainWindow : Window
{
  private void OnShowPropertiesDialogButtonClicked(object sender, RoutedEventArgs e)
  {
    // TODO::Show PropertiesWindow dialog
  }
}

PropertyViewModel.cs

class PropertiesViewModel : INotifyPropertyChanged
{
  public PropertiesViewModel(UserSettingsRepository userSettingsRepository)
  {
    // Use the Model to read and write data
    this.UserSettingsRepository = userSettingsRepository;   
  }

  private void ExecuteSaveDataCommand(object parameter)
  {
    this.UserSettingsRepository.WriteApiKey(this.ApiKey); 
    this.UserSettingsRepository.WriteUserId(this.UserId); 
  }

  public SaveCommand SaveCommand { get; }
  private UserSettingsRepository UserSettingsRepository { get; }
}
eaf3rand

eaf3rand2#

由于SearchDataCommand依赖于MainWindowViewModel,并且视图模型可以在传递给命令后更改,因此应该有一种方法来通知命令视图模型更改的事实,因此它可以自己发出CanExecuteChanged
我将向视图模型添加一个Changed事件,在每次调用PropertyChanged时触发该事件,并在命令中订阅它。

编辑:如果PropertiesViewModel只是在PropertiesWindow中使用,并且与任何东西都没有连接(关于本文的范围),您应该考虑一些消息传递功能。

创建一个PropertiesChangedMessage并从PropertiesViewModel发布。然后订阅SearchCommand中的消息并触发CanExecuteChanged事件。
对于这样的消息传递,可以看看community toolkit

相关问题