Sunday, April 26, 2015

Coding: Synchronizing .NET Properties

I wrote a short class that can synchronize properties for any collection of .NET objects.  The type needs to implement INotifyPropertyChanged, but other than that there is no special requirements to allow a type to be synchronized with this mechanism.

The full class definition follows.  A usage example would be give, a class Car with a writeable property MaxSpeed, a synchronizer can be created like this:
    new PropertySynchronizer(car => car.MaxSpeed)
then a collection of objects of class Car can be added using AddRange; all of these will have there MaxSpeed synchronized to the first items MaxSpeed.  Any change to one of the them will change all of them.
//////   span="""T" the type of the object to synchronize
///   span="">"PropertyType" the type of the Property to be synchronized
public class PropertySynchronizer where T : INotifyPropertyChanged
{
Note that the constraint where T : INotifyPropertyChanged can be relaxed; if INotifyPropertyChanged is not implemented then there will not be updates for the class changing.
    /// constructs a Synchronizer for object type T and PropertyType.
    /// expression provided to identify the property    /// "forMemberLambda" expression of the form 'obj => obj.Property'     public PropertySynchronizer(Expression<Func> forMemberLambda)     {         // store the member expression and compiled function         _forMemberLambda = forMemberLambda;         _forMember = _forMemberLambda.Compile();         // test to get the member info         LambdaExpression lambdaExpression = (LambdaExpression)_forMemberLambda;         MemberExpression memberExpression = (MemberExpression)lambdaExpression.Body;         _memberInfo = memberExpression.Member;     }     Expression<Func> _forMemberLambda;     Func _forMember;     MemberInfo _memberInfo; 
The constructor takes an expression representing the member property, and uses it to extract the MemberInfo as well as to create a compiled function.  Both are used to access the property.   
    /// adds a collection of objects to be synchronized
    /// "newObject" the collection of objects     public void AddRange(IEnumerable<T> newObjects)     {         // for each of the objects in the collection...         foreach (T newObject in newObjects)         {             // if it is not the first object             if (_synchronizedObjects.Count() > 0)             {                 // then set the property value                 SetPropertyValue(newObject, _forMember(_synchronizedObjects.First()));             }             // set up the property change event             INotifyPropertyChanged notifyPropertyChanged = 
                   newObject as INotifyPropertyChanged;
            if (notifyPropertyChanged != null)
            {
                notifyPropertyChanged.PropertyChanged += 
                     (s, e) => OnSyncObjectPropertyChanged(s, e);
            }
 
            // add the object to the list
            _synchronizedObjects.Add(newObject);
        }
    }
    List _synchronizedObjects = new List();
AddRange adds a collection of type T objects, setting the property values and also adding the change event handlers
    /// removes all objects from synchronization    public void RemoveAll()
    {
        // iterate over objects 
        foreach (T existingObject in _synchronizedObjects)
        {
            // remove property change event handler
            INotifyPropertyChanged notifyPropertyChanged = 
                     existingObject as INotifyPropertyChanged;
            if (notifyPropertyChanged != null)
            {
                notifyPropertyChanged.PropertyChanged -= 
                    (s, e) => OnSyncObjectPropertyChanged(s, e);
            }
        }
 
        // clear the collection
        _synchronizedObjects.Clear();
    }
RemoveAll removes all synchronized objects from the collection
    /// sets the property value for an object    /// "forObject" object of type T whose property is to be set     /// "value" the value to set     public void SetPropertyValue(T forObject, PropertyType value)     {         // standard flags for property invoke         BindingFlags flags = BindingFlags.DeclaredOnly                                    | BindingFlags.Public                                    | BindingFlags.NonPublic                                    | BindingFlags.Instance                                    | BindingFlags.SetProperty;         // set the member value         _memberInfo.DeclaringType.InvokeMember(_memberInfo.Name, flags,             null, forObject, new object[] { value });     }
SetPropertyValue sets the property value for a given T object using InvokeMember
    /// respond to property change    /// "sender" the object originating the change     /// "args" change event args     public void OnSyncObjectPropertyChanged(object sender, PropertyChangedEventArgs args)     {         // check that the property of interest is the one being triggered         if (args.PropertyName.CompareTo(_memberInfo.Name) == 0)         {             // are we already updating this property?  then skip             if (_performingUpdate)                 return;             // set flag to prevent recursion             _performingUpdate = true;             // get the value to be set             PropertyType value = _forMember((T)sender);             foreach (T otherObject in _synchronizedObjects)             {                 if (!otherObject.Equals((T)sender))                 {                     SetPropertyValue(otherObject, value);                 }             }             // done with update             _performingUpdate = false;         }     }     // flag to ensure that we don't recurse     bool _performingUpdate = false; }
OnSyncObjectPropertyChanged responds to property changes by updating the other objects in the collection to have the same property value as the sender of the change event.