A declarative way of handling property changed notifications

A declarative way of handling property changed notifications

Old forum URL: forums.lhotka.net/forums/t/10941.aspx


stephen.fung posted on Sunday, December 04, 2011

We have a large WinForms application based on CSLA.NET, and one of the most painful parts of it has been making sure that all the relevant properties and flags get changed depending on different conditions.

I recently worked on adding some library code that enables us to declaratively define calculated properties without having to worry about manually carefully implementing INotifyPropertyChanged whenever any of the source properties change.  It made our code base much easier to work with.  I blogged about it here:

http://www.archonsystems.com/devblog/2011/12/04/a-declarative-way-of-handling-property-changed-notifications/

I wanted to bring this up as a suggestion for a future version of CSLA.NET.  I think it's applicable to WPF as well and likely other frameworks, but my experience has only been with WinForms thus far.  I can post some more code if anybody's interested.

JonnyBee replied on Sunday, December 04, 2011

We've done some improvements for CSLA 4.2 and this should be solved by a BusinessRule.

Take for example this rule:

  public class CalcTotal : Csla.Rules.CommonRules.CommonBusinessRule
  {
    private readonly IPropertyInfo _qtyProperty;
    private readonly IPropertyInfo _priceProperty;
 
    public CalcTotal(IPropertyInfo primaryProperty, IPropertyInfo qtyProperty, IPropertyInfo priceProperty)
      : base(primaryProperty)
    {
      _qtyProperty = qtyProperty;
      _priceProperty = priceProperty;
      InputProperties = new MobileList<IPropertyInfo>() {qtyProperty, priceProperty};
    }
 
    protected override void Execute(RuleContext context)
    {
      dynamic qty = context.InputPropertyValues[_qtyProperty];
      dynamic price = context.InputPropertyValues[_priceProperty];
 
      context.AddOutValue(qty * price);
    }
  }

usage:
      BusinessRules.AddRule(new CalcTotal(SumTotalProperty, QtyProperty, PriceEaProperty));

Now this rule will automatically recalculate the SumTotal whenever Qty or PriceEa is changed
and raise the appropriate OnPropertyChanged event to notify the UI.

It is done by one of the enhancements to the rule engine by considering InputProperties as dependencies.

You should also note that the rule has no knowledge of the business object type.

As for when a user is allowed to read/edit a field should be handled (declaratively) by authorization rules.

stephen.fung replied on Tuesday, December 06, 2011

Ah that's great... it's a big improvement with only a minor conceptual change to the framework!  You might want to consider taking that a little further to offer some shorthand notation where you can add these sorts of rules right at the property definition:

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

The nice thing about this is the compact syntax, and how it's clear right from where you define the property how it's going to be used -- this works really well with Go To Definition in Visual Studio.

I've got something like that working (outside of CSLA).  The For method returns an intermediate object, then the Define method parses its expression into input properties and stores a list of these rules.  Then there's a listener whenever a property changes that goes through the list and updates any dependent calculated properties using its expression.

RockfordLhotka replied on Tuesday, December 06, 2011

Our current support for at-property-declaration rules uses DataAnnotations attributes. This has the potential to tie into other Microsoft technologies that also use those attributes (such as ASP.NET MVC), and still provides some of the readability you are looking for.

I don't know whether you could create a custom attribute that works like you describe, but it might be possible.

StefanCop replied on Thursday, December 15, 2011

I like the idea having business calculations in one BusinessRule. That helps to reuse the business calculations across "same" B.O. for different user stories.

However, I see two minor drawbacks:
1) Set calculated values when loading: You must not forget to put a  BusinessRules.CheckRules(calcedProperty);  after all other properties are loaded.
2) You cannot reuse the rule as it is in ReadOnlyBase<> (again when values are loaded). Here I think if the core calculation is factored out in a static method of the rule, we can reuse this method.

Are there other / better ideas solving these points?

 

StefanCop replied on Thursday, December 15, 2011

OOps,

stefan cop
You cannot reuse the rule as it is in ReadOnlyBase<>

I haven't recognized that there is a BusinessRules property in ReadOnlyBase. Great!

And kick the rules explicitly when loading is done makes sense.

JonnyBee replied on Thursday, December 15, 2011

One more option:

If you have a calculated value in the getter only (maybe even without using a backing field) all you need is to get a OnPropertyChanged for that field when one of the input property values are changed.

Meaning - that f you can create an empty rule with just PrimaryProperty and InputProperties:

    public class NotifyChangedWhenInputChanged : Csla.Rules.BusinessRule
    {
        public NotifyChangedWhenInputChanged(IPropertyInfo primaryProperty, params IPropertyInfo[] inputProperties) : base(primaryProperty)
        {
            InputProperties = InputProperties ?? new List<IPropertyInfo>();
            InputProperties.AddRange(inputProperties);
        }
 
        protected override void Execute(RuleContext context) { }
    }

and you have f.ex  3 properties in your BO: 
      public static readonly PropertyInfo<string> Param1Property = RegisterProperty<string>(c => c.Param1);
      public string Param1
      {
          get { return GetProperty(Param1Property); }
          set { SetProperty(Param1Property, value); }
      }
 
      public static readonly PropertyInfo<string> Param2Property = RegisterProperty<string>(c => c.Param2);
      public string Param2
      {
          get { return GetProperty(Param2Property); }
          set { SetProperty(Param2Property, value); }
      }
 
      public static readonly PropertyInfo<string> CalculatedProperty = RegisterProperty<string>(c => c.Calculated);
      public string Calculated
      {
          get { return string.Format("{0}{1}", ReadProperty(Param1Property), ReadProperty(Param2Property)); }
      }

and this rule is used: 
      BusinessRules.AddRule(new NotifyChangedWhenInputChanged(CalculatedProperty, Param1Property, Param2Property));
You will get PropertyChanged event (and UI will reread the property value) for CalculatedProperty when 
any of the input values is changed. 

StefanCop replied on Thursday, December 15, 2011

Ok, that's a good option for easy calculations.

JonnyBee
      public string Calculated       {           get { return string.Format("{0}{1}", ReadProperty(Param1Property), ReadProperty(Param2Property)); }       }

 


 

But I'm looking for good practice of reusing real business calculation and decisions across different object graphs (for different user stories).

stephen.fung replied on Monday, December 19, 2011

@Rocky:  I looked briefly into using attributes, but C# doesn't allow attributes to take lambda expressions as parameters unfortunately.

I've posted some code for the at-property compact syntax here:  http://www.archonsystems.com/devblog/2011/12/04/a-declarative-way-of-handling-property-changed-notifications/ (link at the bottom).  

I'd appreciate any feedback.  The Model.cs file shows the main usage.  The example there's not coupled with CSLA, but I'm sure the concepts could be applied to fit into the CSLA BusinessRules paradigm, perhaps by translating into a custom business rule.  I think the main benefit is that by using a lambda expression, the InputProperties can be automatically captured (except for some gotchas with function calls) and the target property value can be set.

Copyright (c) Marimer LLC