Skip to content

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

@bwood4

Description

@bwood4

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:

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:

/// <inheritdoc/>
[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<global::WpfTests.SubViewModel>.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature request 📬A request for new changes to improve functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions