ViewModel中RelayCommand继承引发SaveCommand空引用异常问题
问题场景
设计主/详细视图模型架构时,创建了包含Add、Edit、Save、Cancel、Delete命令及CanExecute逻辑的基类。其中Save和Delete命令需要在派生类中扩展实现,但希望保留基类的CanExecute管理逻辑。采用示例代码实现时,NotifyButtonsState()方法中调用SaveCommand.NotifyCanExecuteChanged()和DeleteCommand.NotifyCanExecuteChanged()会触发System.NullReferenceException: 对象引用未设置到对象的实例错误。
基类代码
internal abstract partial class MasterDetailViewModelBase<TDetailViewModel> : ObservableObject { private TDetailViewModel? currentItem; public TDetailViewModel? CurrentItem { get => currentItem; set { if (EqualityComparer<TDetailViewModel?>.Default.Equals(currentItem, value)) return; SetProperty(ref currentItem, value); OnPropertyChanged(nameof(HasCurrentItem)); NotifyButtonsState(); } } public bool HasCurrentItem => CurrentItem is not null; private void NotifyButtonsState() { OnPropertyChanged(nameof(CanAdd)); OnPropertyChanged(nameof(CanEdit)); OnPropertyChanged(nameof(CanSave)); OnPropertyChanged(nameof(CanCancel)); OnPropertyChanged(nameof(CanDelete)); AddCommand.NotifyCanExecuteChanged(); EditCommand.NotifyCanExecuteChanged(); SaveCommand.NotifyCanExecuteChanged(); // <-- 错误位置 CancelCommand.NotifyCanExecuteChanged(); DeleteCommand.NotifyCanExecuteChanged(); } public bool CanAdd => HasCurrentItem ? CurrentItem.State == ItemState.Unchanged : true; public bool CanEdit => HasCurrentItem ? CurrentItem.State == ItemState.Unchanged : false; public bool CanSave => HasCurrentItem ? CurrentItem.IsChanged : false; public bool CanCancel => HasCurrentItem ? CurrentItem.State == ItemState.Adding || CurrentItem.State == ItemState.Editing : false; public bool CanDelete => HasCurrentItem ? CurrentItem.State == ItemState.Unchanged : false; [RelayCommand(CanExecute = nameof(CanAdd))] private void Add() { // [...] } [RelayCommand(CanExecute = nameof(CanEdit))] private void Edit() { // [...] } // CommunityToolkit.Mvvm实现RelayCommand特性的方式示例 protected IRelayCommand SaveCommand { get; } protected void Save() { // [...] } [RelayCommand(CanExecute = nameof(CanCancel))] private void Cancel() { // [...] } protected IRelayCommand DeleteCommand { get; } protected void Delete() { // [...] } }
派生类代码
internal partial class CustomersViewModel : MasterDetailViewModelBase<CustomerViewModel> { [RelayCommand(CanExecute = nameof(CanSave))] private void Save() { // [...] base.Save(); } [RelayCommand(CanExecute = nameof(CanDelete))] private void Delete() { // [...] base.Delete(); } }
错误原因
基类中仅声明了SaveCommand和DeleteCommand属性,但未对其进行实例化。派生类通过[RelayCommand]特性生成的命令是派生类自己的私有字段,不会赋值给基类的SaveCommand/DeleteCommand属性,导致基类中这两个属性始终为null,调用NotifyCanExecuteChanged()时触发空引用异常。
解决方案
方案1:基类定义虚方法并添加[RelayCommand],派生类重写方法
这种方式让基类负责命令的实例化,派生类仅扩展方法逻辑,保证基类的SaveCommand/DeleteCommand有有效实例:
修改后的基类代码
internal abstract partial class MasterDetailViewModelBase<TDetailViewModel> : ObservableObject { // ... 其他代码保持不变 ... [RelayCommand(CanExecute = nameof(CanSave))] protected virtual void Save() { // 基类的Save逻辑 // [...] } [RelayCommand(CanExecute = nameof(CanDelete))] protected virtual void Delete() { // 基类的Delete逻辑 // [...] } }
修改后的派生类代码
internal partial class CustomersViewModel : MasterDetailViewModelBase<CustomerViewModel> { protected override void Save() { // 派生类扩展的Save逻辑 // [...] base.Save(); } protected override void Delete() { // 派生类扩展的Delete逻辑 // [...] base.Delete(); } }
方案2:基类通过抽象属性获取派生类的命令实例
如果必须在派生类中用[RelayCommand]特性生成命令,可在基类中定义抽象属性,让派生类返回自己的命令实例:
修改后的基类代码
internal abstract partial class MasterDetailViewModelBase<TDetailViewModel> : ObservableObject { // ... 其他代码保持不变 ... // 替换原来的SaveCommand/DeleteCommand属性为抽象属性 protected abstract IRelayCommand SaveCommand { get; } protected abstract IRelayCommand DeleteCommand { get; } protected void Save() { // 基类的Save逻辑 // [...] } protected void Delete() { // 基类的Delete逻辑 // [...] } }
修改后的派生类代码
internal partial class CustomersViewModel : MasterDetailViewModelBase<CustomerViewModel> { [RelayCommand(CanExecute = nameof(CanSave))] private void Save() { // [...] base.Save(); } [RelayCommand(CanExecute = nameof(CanDelete))] private void Delete() { // [...] base.Delete(); } // 实现基类的抽象属性,返回自动生成的命令实例 protected override IRelayCommand SaveCommand => saveCommand; protected override IRelayCommand DeleteCommand => deleteCommand; }
注意:CommunityToolkit.Mvvm的
[RelayCommand]特性会自动生成以下划线开头的私有命令字段(比如saveCommand),派生类可直接引用该字段。
内容的提问来源于stack exchange,提问作者Pedro Gaspar

