CommunityToolkit / CommunityToolkit/dotnet
Updating properties and enabling commands that depend on observable model properties
- Dominant language
- C#
- Stars
- 3.8k
- Forks
- 400
- PR merge metrics
- No merged PRs in 30d
Description
### 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 `ObservableObject`and Fody.PropertyChange, but there is no difference).
```c#
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyPropertyChangedFor(nameof(NotEnoughDrinks))]
private Bill.Item billItem;
```
Its properties are updated through UI with XAML two-way bindings:
```xaml
...
```
`SaveCommand` and `NotEnoughDrinks` property depend on fields inside `BillItem`.
```cs
public bool NotEnoughDrinks => ChosenDrink?.Quantity < BillItem.Quantity;
```
```cs
[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).
```cs
[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`.
```cs
[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
```c#
[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.
```c#
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
Contributor guide
Assessment
This issue has not been assessed yet.