CommunityToolkit / dotnet

.NET Community Toolkit is a collection of helpers and APIs that work for all .NET developers and are agnostic of any specific UI platform. The toolkit is maintained and published by Microsoft, and part of the .NET Foundation.

Home Page:https://docs.microsoft.com/dotnet/communitytoolkit/?WT.mc_id=dotnet-0000-bramin

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Automatically determine dependent properties with [ObservableProperty]

Tyrrrz opened this issue · comments

Overview

When using [ObservableProperty], we have the ability to apply [NotifyPropertyChangedFor(...)] and [NotifyCanExecuteChangedFor(...)] to inject signaling code for other, usually dependent properties. However, in case with many of such dependent properties, the attribute spam can get really noisy, so it would be nice if it were possible to infer these chains automatically.

API breakdown

The suggestion is to add a new attribute or, better, a property on [ObservableProperty] that would instruct the source generator to generate dependent property changed triggers automatically.

Something like this:

[ObservableProperty(NotifyDependentProperties = true)]
private string _firstName;

[ObservableProperty(NotifyDependentProperties = true)]
private string _lastName;

public string FullName => FirstName + ' ' + LastName;

With this, the source generator should produce the following code (simplified):

public string FirstName
{
    get => _firstName;
    set
    {
        _firstName = value;
        OnPropertyChanged();
        OnPropertyChanged(nameof(FullName));
    }
}

public string LastName
{
    get => _lastName;
    set
    {
        _lastName = value;
        OnPropertyChanged();
        OnPropertyChanged(nameof(FullName));
    }
}

public string FullName => FirstName + ' ' + LastName;

The way the source generator would identify dependent properties is as follows:

  1. Get all properties in the containing type, excluding auto-generated properties.
  2. Inspect the get method and find all member access expressions.
  3. If a member access expression refers, at any level of nesting, to one of the auto-generated observable properties, mark it as a dependent.
  4. If a member access expression refers, at any level of nesting, to a user-authored property on the containing type, recursively scan that property's member expressions. If those expressions refer to one of the auto-generated observable properties, mark the top-level property as a dependent.
  5. If a member access expression refers to any other member, then ignore it.

Additionally, the same can be applied to commands and the methods used as their CanExecute handlers:

[ObservableProperty(NotifyDependentCanExecuteChanged = true)]
private bool _isFooBar;

private bool CanExecuteStuff() => _isFooBar;

[RelayCommand(CanExecute = nameof(CanExecuteStuff))]
private void ExecuteStuff() { }

Would turn into this:

public bool IsFooBar
{
    get => _isFooBar;
    set
    {
        _isFooBar = value;
        OnPropertyChanged();
        ExecuteStuffCommand.NotifyCanExecuteChanged();
    }
}

// ...

Usage example

See above.

Breaking change?

No

Alternatives

The alternative is to use PropertyChanged.Fody but it could be cool to do all of this using Roslyn.

Additional context

No response

Help us help you

Yes, I'd like to be assigned to work on this item

This has been proposed before (can't find the issues right now), and it's a scenario we're intentionally not supporting. This is for two main reasons: one is that it hides what's going on and makes it not immediately clear when glancing on a property, which members it's actually notifying. The other is that the suggested implementation would be extremely expensive: we'd have to crawl the accessor bodies of all properties, find all expressions, get the semantic model, get the operation, get the target property symbol, and track that in some way. This is definitely not something we want to introduce into our incremental pipeline, as it would slow things down way too much. Additionally it's not even guaranteed it would always work, for instance, what if you have a property that calls some other method which then returns the value of that property that changed? You'd either have to crawl all method bodies of all invoked methods, or say "this isn't supported" and then fail either silently or with some diagnostics, etc. etc. and then you're just back at square one and with a super convoluted implementation that doesn't even always work anyway.

Thank you for taking the time to write the proposal though! 🙂