Raising PropertyChanged for a dependent property, when a prerequisite property is changed in another class?
Asked Answered
C

1

7

I have this Bank class:

public class Bank : INotifyPropertyChanged
{
    public Bank(Account account1, Account account2)
    {
        Account1 = account1;
        Account2 = account2;
    }

    public Account Account1 { get; }
    public Account Account2 { get; }

    public int Total => Account1.Balance + Account2.Balance;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Bank depends on other classes and has a property Total that is calculated from properties of these other classes. Whenever any of these Account.Balance properties is changed, PropertyChanged is raised for Account.Balance:

public class Account : INotifyPropertyChanged
{
    private int _balance;

    public int Balance
    {
        get { return _balance; }
        set
        {
            _balance = value;
            RaisePropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

I would like to raise PropertyChanged for Total, whenever any of the prerequisite properties is changed. How can I do this in a way that is easily testable?

TL;DR How do I raise PropertyChanged for a dependent property, when a prerequisite property is changed in another class?

Copywriter answered 27/4, 2017 at 9:23 Comment(0)
C
6

You can do this in many different ways. I have seen many different solutions, that invovle custom attributes or raising multiple PropertyChanged events in a single property setter. I think most of these soultions are anti-patterns, and not easily testable.

The best way a colleague (Robert Jørgensgaard Engdahl) and I have come up with is this static class:

public static class PropertyChangedPropagator
{
    public static PropertyChangedEventHandler Create(string sourcePropertyName, string dependantPropertyName, Action<string> raisePropertyChanged)
    {
        var infiniteRecursionDetected = false;
        return (sender, args) =>
        {
            try
            {
                if (args.PropertyName != sourcePropertyName) return;
                if (infiniteRecursionDetected)
                {
                    throw new InvalidOperationException("Infinite recursion detected");
                }
                infiniteRecursionDetected = true;
                raisePropertyChanged(dependantPropertyName);
            }
            finally
            {
                infiniteRecursionDetected = false;
            }
        };
    }
}

It creates a PropertyChangedEventHandler, which you can set up to listen on PropertyChanged on other classes. It handles circular dependencies with an InvalidOperationException before an StackOverflowException is thrown.

To use the static PropertyChangedPropagator in the example from above, you will have to add one line of code for each prerequisite property:

public class Bank : INotifyPropertyChanged
{
    public Bank(Account account1, Account account2)
    {
        Account1 = account1;
        Account2 = account2;
        Account1.PropertyChanged += PropertyChangedPropagator.Create(nameof(Account.Balance), nameof(Total), RaisePropertyChanged);
        Account2.PropertyChanged += PropertyChangedPropagator.Create(nameof(Account.Balance), nameof(Total), RaisePropertyChanged);
    }

    public Account Account1 { get; }
    public Account Account2 { get; }

    public int Total => Account1.Balance + Account2.Balance;


    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

This is easily testable (pseudo code):

[Test]
public void Total_PropertyChanged_Is_Raised_When_Account1_Balance_Is_Changed()
{
    var bank = new Bank(new Account(), new Account());

    bank.Account1.Balance += 10;

    Assert.PropertyChanged(bank, nameof(Bank.Total));
}
Copywriter answered 27/4, 2017 at 9:23 Comment(4)
1. PropertyChangedPropagator doesn't have a method to dispose INPC subscription. 2. Is there an easy test for "Infinite recursion detected" case?Pome
Ad 1): You don't need that. If the Account instances need to outlive the Bank class, unsubscription can be implemented in the Bank class without modifying the PropertyChangedPropagator class. If the Accounts and Bank has the same lifetime, event unsubscription is irrelevant. Ad 2): That would be a test for the PropertyChangedPropagator and not the Bank class. It is, however very simple to test (and it of course exists). Just wire it up for infinite recursion and check that it throws the expected exception.Antonioantonius
What if I have the property AnyState in class A. Which is bascally a get from two other properties, each one in a separate class. prop A get { return B.IsRunning || C.IsRunning;} Class B clas a IsRunning property and C as wellCzarevitch
@Czarevitch If I understand you correctly, this is exact the same problem. AnySate is the property Total in the Bank class, B.IsRunning is Account1.Balance and C.IsRunning is Account2.Balance in this answer.Copywriter

© 2022 - 2024 — McMap. All rights reserved.