MVVM: delegating PropertyChanged notifications A -> B (readonly) -> C

428 Views Asked by At

In a .NET MAUI application I am using the Mvvm.CommunityToolkit with source generation capabiliities for INotifyPropertyChanged support in my ViewModels:

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/observableproperty

Now I am facing a design challenge. Sticking with the example:

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? firstName;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? lastName;


public string FullName => $"{FirstName} {LastName}";

This is all well and good, FullName will get updated when it is bound in the UI, etc.

Now what do I do if I have another property that needs to update whenever FullName gets updated?

Take this (admittedly contrived) example (I want to keep this very simple, the practical application is given at the end):

public int NumberOfXsInName => FullName.Count(x => char.ToLower(x) == 'x');

How can I get that notification? I do not want to add [NotifyPropertyChangedFor] to all properties that notify FullName, since that seems not very resilient, especially since I can not enforce that subclasses will do that correctly.

Do I have to write in the constructor of my ViewModel

PropertyChanged += (_, e) =>
{
    if (e?.PropertyName?.Equals(nameof(FullName)) ?? false)
    {
        OnPropertyChanged(nameof(NumberOfXsInFullName));
    }
};

This seems very verbose for just wanting to "chain up" the PropertyChanged notification through a read-only property (since I can not raise it in a setter)

(*) In practice, I want to split a FilteredCollection, that is based on a DataSource and a FilterFunction, into Groups. Here, the FilteredCollection is just a readonly property that gets notified (like FullName in the example), and I want to split this into two groups based on some criteria for consumption in a different view.

1

There are 1 best solutions below

2
ChrisBD On

new post #1

I would suggest going back to basics and using something like the following:

public class Class1: ObservableObject
{
    private string lastName;
    private string firstName;
    private string fullName;

    public string FirstName
    {
        get => firstName;
        set
        {
            SetProperty(ref firstName, value);
            UpdateFullName();
        }
    }

    public string LastName
    {
        get => lastName;
        set
        {
            SetProperty(ref lastName, value);
            UpdateFullName();
        }
    }

    private void UpdateFullName()
    {
        FullName = $"{FirstName} {LastName}";
    }

    public string FullName
    {
        get => fullName;
        private set
        {
            if (SetProperty(ref fullName, value))
            {
                OnPropertyChanged(nameof(NumberOfXsInName));
            }
        }
    }

    public int NumberOfXsInName => FullName.Count(x => char.ToLower(x) == 'x');
}

Original post with code that does not work as intended but has been retained due to comment 1 by @HotteMax

As

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? firstName;

expands to

public string? FirstName
{
    get => firstName;
    set
    {
        if (SetProperty(ref firstName, value))
        {
            OnPropertyChanged("FullName");
        }
    }
}

I guess that you could have something like the following (sticking with the tool kit)

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NotifyFullNameDependents))]
private string? firstName;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NotifyFullNameDependents))]
private string? lastName;

[RelayCommand]
private void NotifyFullNameDependents()
{
    OnPropertyChanged(nameof(FullName));
    OnPropertyChanged(nameof(NumberOfXsInName));    
}

public string FullName => $"{FirstName} {LastName}";

public int NumberOfXsInName => FullName.Count(x => char.ToLower(x) == 'x');

but this would leave a public IRelayCommand property.