CommunityToolkit / CommunityToolkit/dotnet

Updating properties and enabling commands that depend on observable model properties

Open
#906 1 comment 0 reactions 0 assignees View on GitHub
feature request :mailbox_with_mail:
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.