I’m designing a master/detail view-model scheme, so I created a base class that implements Add, Edit, Save, Cancel and Delete commands and also manages the CanExecute
logic for these commands. However, the Save and Delete commands need to be partially implemented in the derived class, but they must still be present in the base class to manage the CanExecute
logic.
In short, I tried the RelayCommand
inheritance this way:
// Base class
protected IRelayCommand SaveCommand { get; }
protected void Save()
{
// [...]
}
protected IRelayCommand DeleteCommand { get; }
protected void Delete()
{
// [...]
}
// Derived class
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
// [...]
base.Save();
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private void Delete()
{
// [...]
base.Delete();
}
In more detail, the base class:
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(); // [NotifyCanExecuteChangedFor(nameof(AddCommand))]
EditCommand.NotifyCanExecuteChanged(); // [NotifyCanExecuteChangedFor(nameof(EditCommand))]
SaveCommand.NotifyCanExecuteChanged(); // [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
CancelCommand.NotifyCanExecuteChanged(); // [NotifyCanExecuteChangedFor(nameof(CancelCommand))]
DeleteCommand.NotifyCanExecuteChanged(); // [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
}
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()
{
// [...]
}
//
// That's how CommunityToolkit.Mvvm implements the RelayCommand attribute:
//
//private RelayCommand? saveCommand;
//public IRelayCommand SaveCommand => saveCommand ??= new RelayCommand(new Action(Save), () => CanSave);
//
protected IRelayCommand SaveCommand { get; }
protected void Save()
{
// [...]
}
[RelayCommand(CanExecute = nameof(CanCancel))]
private void Cancel()
{
// [...]
}
protected IRelayCommand DeleteCommand { get; }
protected void Delete()
{
// [...]
}
}
And the derived class:
internal partial class CustomersViewModel : MasterDetailViewModelBase<CustomerViewModel>
{
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
// [...]
base.Save();
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private void Delete()
{
// [...]
base.Delete();
}
}
But doing it this way causes an error in the function that notifies the status of the buttons, NotifyButtonsState()
:
private void NotifyButtonsState()
{
OnPropertyChanged(nameof(CanAdd));
OnPropertyChanged(nameof(CanEdit));
OnPropertyChanged(nameof(CanSave));
OnPropertyChanged(nameof(CanCancel));
OnPropertyChanged(nameof(CanDelete));
AddCommand.NotifyCanExecuteChanged();
EditCommand.NotifyCanExecuteChanged();
SaveCommand.NotifyCanExecuteChanged(); // <-- ERROR
CancelCommand.NotifyCanExecuteChanged();
DeleteCommand.NotifyCanExecuteChanged();
}
The error:
System.NullReferenceException: ‘Object reference not set to an instance of an object.’