CommunityToolkit / CommunityToolkit/dotnet

Add flag to NotifyPropertyChangedFor and NotifyCanExecuteChangedFor that allows notification on sub property changes

Open
#1,168 1 comment 1 reaction 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

I frequently have a property that depends on the sub-property of another property in my viewmodels. The most common case for me is the `IsRunning` property of `AsyncRelayCommand`, but I've run into many other examples.
I'd like some sort of way to indicate that another property or command has changed when a sub-property has changed. Ideally this could be extended to [RelayCommand] declarations as well so that when the automatically generated command starts or stops, other properties are notified.

### API breakdown

Preferably we could add a boolean property called `NotifyOnSubPropertyChanged` to `NotifyPropertyChangedForAttribute` and `NotifyCanExecuteChangedForAttribute` that opts into this behavior when set to true.

### Usage example

I would use it like this:

```csharp
public partial class TestViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FormattedProp1), NotifyOnSubPropertyChanged = true)]
[NotifyCanExecuteChangedFor(nameof(Command1Command), NotifyOnSubPropertyChanged = true)]
public partial SubViewModel SubViewModel { get; private set; } = new();

public string FormattedProp1 => "_" + SubViewModel.Prop1 + "_";

[RelayCommand(CanExecute = nameof(CanDoThing1))]
[property: NotifyCanExecuteChangedFor(nameof(DoThing2Command), NotifyOnSubPropertyChanged = true)]
private Task DoThing1() => Task.CompletedTask;
private bool CanDoThing1() => SubViewModel.Prop1 is not null;

[RelayCommand(CanExecute = nameof(CanDoThing2))]
private void DoThing2() { }
private bool CanDoThing2() => !DoThing1Command.IsRunning;
}

public partial class SubViewModel : ObservableObject
{
[ObservableProperty]
public partial string Prop1 { get; set; }
}
```

I think the generated code would look something like this for the `SubViewModel` property:

```csharp
///
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.4.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial global::WpfTests.SubViewModel SubViewModel
{
get => field;
private set
{
if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value))
{
OnSubViewModelChanging(value);
OnSubViewModelChanging(default, value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.SubViewModel);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FormattedProp1);
(field as INotifyPropertyChanged)?.PropertyChanged -= OnSubPropertyChanged;
field = value;
(field as INotifyPropertyChanged)?.PropertyChanged += OnSubPropertyChanged;
OnSubViewModelChanged(value);
OnSubViewModelChanged(default, value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.SubViewModel);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FormattedProp1);
DoThing1Command.NotifyCanExecuteChanged();
}

void OnSubPropertyChanged(object? sender, PropertyChangedEventArgs args)
{
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FormattedProp1);
DoThing1Command.NotifyCanExecuteChanged();
}
}
```

Note that the handler for detecting sub-property changes is removed and added in the parent property setter, and the handler itself is a local function of the setter that is populated based on the property names provided.

I'm not sure how it would integrate into the `RelayCommandAttribute`, but it would be really nice to have for that for asynchronous commands.

### Breaking change?

No

### Alternatives

Right now all of this has to be configured in the constructor or initializer, and greatly hinders some of the benefits of the source generators.

An alternative might be to have a new attribute, but I think this is a little cleaner.

### 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.