Updating properties and enabling commands that depend on observable model properties
Overview
As we know, RelayCommand and getter-only properties need to be notified when their dependencies change. That is usually done with NotifyCanExecuteChangedForAttribute and NotifyPropertyChangedForAttribute.
But what needs to be done in cases when we access nested object fields, and not primitive types? I know that those classes also need to implement INotifyPropertyChanged. But that is not enough here, because that event isn't propagated to cause reevaluation of CanExecute method.
Example
There is an object of Bill.Item class that implements INotifyPropertyChanged (I have tried to use both ObservableObjectand Fody.PropertyChange, but there is no difference).
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyPropertyChangedFor(nameof(NotEnoughDrinks))]
private Bill.Item billItem;
Its properties are updated through UI with XAML two-way bindings:
<Entry Placeholder="Title" Text="{Binding BillItem.Title}" />
...
<HorizontalStackLayout>
<Entry Placeholder="Quantity" Text="{Binding BillItem.Quantity}" IsReadOnly="True" />
<Stepper Value="{Binding BillItem.Quantity}" Minimum="1" Increment="1" />
</HorizontalStackLayout>
SaveCommand and NotEnoughDrinks property depend on fields inside BillItem.
public bool NotEnoughDrinks => ChosenDrink?.Quantity < BillItem.Quantity;
[RelayCommand(CanExecute = nameof(IsSaveable))]
private void Save() {
OnClose?.Invoke(BillItem);
}
private bool IsSaveable() {
if (string.IsNullOrWhiteSpace(BillItem.Title)) return false;
return true;
}
But dependent property won't be recalculated when user types in Entry fields, same thing with enabling command. I tried to find an attribute that will help for this case, but I don't think there is a solution in this library for the given case.
API breakdown
First possibility
One way I can think of to deal with this problem would be to add a parameter to the existing ObservableProperty attribute, so that changing the field of nested ObservableObject behaves the same as changing the reference itself (which is the only thing that is tracked at the moment).
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public sealed class ObservablePropertyAttribute : Attribute
{
...
public bool TrackPropertyChanges { get; init; } = false;
}
Second possibility
Second thing that comes to my mind is to somehow extend NotifyCanExecuteChangedForAttribute and NotifyPropertyChangedForAttribute.
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public sealed class NotifyPropertyChangedForAttribute : Attribute
{
...
bool string[] TrackPropertyChanges { get; init; }
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public sealed class NotifyCanExecuteChangedForAttribute : Attribute
{
...
bool string[] TrackPropertyChanges { get; init; }
}
Usage example
[ObservableProperty(TrackPropertyChanges = true)]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyPropertyChangedFor(nameof(NotEnoughDrinks))]
private Bill.Item billItem;
Breaking change?
No
Alternatives
I have found a workaround, by subscribing to PropertyChanged event and manually invoking OnPropertyChanged and NotifyCanExecuteChanged. Unfortunately, that solution produces COMException and TargetInvocationException from time to time when invoked outside main UI thread. And except from that issue, I assume that this solution is not idiomatic enough because it makes code more complex.
partial void OnBillItemChanged(Bill.Item? oldValue, Bill.Item newValue) {
void OnBillItemPropertyChanged(object? sender, PropertyChangedEventArgs args) {
try {
OnPropertyChanged(nameof(NotEnoughDrinks));
SaveCommand.NotifyCanExecuteChanged();
}
catch (Exception) {}
}
if (oldValue is not null)
oldValue.PropertyChanged -= OnBillItemPropertyChanged;
if (newValue is not null)
newValue.PropertyChanged += OnBillItemPropertyChanged;
}
Additional context
No response
Help us help you
Yes, but only if others can assist