A declarative way of handling property changed notifications

A calculated property is something that is calculated based on the results of some other properties. For example:

  • Subtotal = Unit Cost * Quantity
  • The finalize button should be enabled if the order status is Open
  • The unit cost is editable if the order isn’t finalized and the user has the appropriate access rights
  • Password change is valid if NewPassword.Length >= 6 && NewPassword == NewPasswordConfirmation

You’d go ahead and define something like:

        public decimal Subtotal
        {
            get { return UnitCost * Quantity; }
        }

This is all fine and dandy so far. But if you’re doing this in a Windows Forms application and you want to use DataBinding or an ErrorProvider, you need to implement INotifyPropertyChanged, i.e. raise an event called PropertyChanged whenever your calculated properties change.

The most straightforward way of doing this is whenever the UnitCost or Quantity changes, you raise PropertyChanged on Subtotal as well, so something like:

        private decimal _unitCost;
        public decimal UnitCost
        {
            get
            {
                return _unitCost;
            }
            set
            {
                _unitCost = value;
                PropertyChanged("UnitCost");
                PropertyChanged("Subtotal");
            }
        }

Or in other cases, you might update it right in the UI code, like this:

        public void OnOrderStatusChanged(Status newStatus)
        {
            buttonFinalize.Enabled = (newStatus == Status.Open);
        }

This is fine in a small application, but as things grow and evolve, it quickly becomes an unmaintainable mess. You might tweak the conditions for your calculated properties and forget to update the PropertyChanged notifications, introducing subtle bugs. You might have to dig through multiple places in your code just to figure out when a button should be enabled. This was one of the most painful parts of our codebase, and I was looking for a way to make this simpler. If Excel can figure out everything it needs to update whenever a value changes, why can’t Windows Forms?

Turns it, it is more or less possible, with some tricks and some boilerplate. We’ve got it working with something that looks like this:

public static PropertyInfo SubtotalProperty = For<Line>(l => l.Subtotal).Define(l => l.Qty * l.PriceEa);

We wrote some framework code to support this and ended up doing:

  • Define a View Model class for most of these calculated properties
  • Use DataBinding to link the View Model properties and the associated properties on the Form, e.g. buttonFinalize.Enabled
  • In the View Model, write the calculated property and the expression that defines it. This way, when you look at the FinalizeButtonEnabled property, you see its definition newStatus == Status.Open right above it.
  • Use System.Linq.Expressions to interpret the expression definition and track at the source properties it depends on.
  • Write some library code that automatically updates the calculated properties and raises PropertyChanged whenever the source properties change.

The end result is that we can just define these calculated properties and the framework code will automatically interpret when to update them, raising PropertyChanged notifications appropriately. It’s easy to debug when things go wrong (you just look at the declaration and see if it makes sense) and a pleasure to work with. I’d love to see this sort of feature get added to frameworks like CSLA .NET.

Update: I’ve now uploaded some sample code to demo this concept. Download sample code.

This entry was posted in Uncategorized. Bookmark the permalink.

3 Responses to A declarative way of handling property changed notifications

  1. phil says:

    are you able to and would you mind sharing the magic behind that compact syntax for the propertyinfo declaration?

  2. Stephen Fung says:

    Sure, give me some time to extract the code and I’ll post it!

Leave a Reply

Your email address will not be published. Required fields are marked *