📜 ⬆️ ⬇️

Basic implementation of INotifyPropertyChanged

WPF in some way reiterated the fate of js - due to some unsolved problems at the platform level, many are trying to become pioneers on a par with Carl von Drez .

Problem


In the case of INPC in ViewModel, there are often properties that are dependent on or calculated from others. For .net 4.0, the situation with the implementation is complicated by the fact that CallerMemberNameAttribute is not supported in this version (in fact, supported if you are a magician and sorcerer ).

Decision


Foreword
Understanding once again a project with dozens of lines in a package file, the UNISTACK concept is getting closer and closer to me when a comprehensive well-integrated solution allows you to implement typical tasks in typical scenarios and leaves room for expansion to the needs of the user. And at the same time, I see the fatal flaws of the existing solutions - cumbersome and heavy. And, sometimes, non-modulus.
')
In the previous article, I promised to show an example of just this kind of integration — when for any asynchronous task launched in a wrapper, the UI is blocked and BusyIndicator or its custom analog is displayed. And I still promise to show this example. So we wrap up all the calls to WCF, but this can be used for long-playing calculations, regrouping large collections, and similar operations.

One of the foundations of the Rikrop.Core.Wpf library is the base class of the object that implements the INotifyProprtyChanged interface, ChangeNotifier , which offers its heirs the following set of methods:
[DataContract(IsReference = true)] [Serializable] public abstract class ChangeNotifier : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "") protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") protected void NotifyPropertyChanged(Expression<Func<object, object>> property) protected void NotifyPropertyChanged(Expression<Func<object>> property) protected virtual void OnPropertyChanged(string propertyName) protected ILinkedPropertyChanged AfterNotify(Expression<Func<object> property) protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property) protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property) where T : INotifyPropertyChanged protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property) where T : ChangeNotifier protected ILinkedObjectChanged Notify(Expression<Func<object>> property) } 

Here you should immediately indicate the interfaces ILinkedPropertyChanged and ILinkedObjectChanged:
 public interface ILinkedPropertyChanged { ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty); ILinkedPropertyChanged Execute(Action action); } public interface ILinkedObjectChanged { ILinkedObjectChanged AfterNotify(Expression<Func<object>> sourceProperty); ILinkedObjectChanged AfterNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty) where T : INotifyPropertyChanged; ILinkedObjectChanged BeforeNotify(Expression<Func<object>> sourceProperty); ILinkedObjectChanged BeforeNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty) where T : ChangeNotifier; } 

Fictitious example of use


Where without an example, which will be called far-fetched and unrealistic? Let's see how to use ChangeNotifier in different scenarios.

We have a device with N sensors of the same type that displays the average of all the sensors. Each sensor displays the measured value and the deviation from the average. When changing the sensor value, we must first recalculate the average value, and then notify the change on the sensor itself. When changing the average value, we need to recalculate the deviations from the average for each of the sensors.
 /// <summary> /// . /// </summary> public class Sensor : ChangeNotifier { /// <summary> ///  . /// </summary> public int Value { get { return _value; } set { SetProperty(ref _value, value); } } private int _value; /// <summary> ///     . /// </summary> public double Delta { get { return _delta; } set { SetProperty(ref _delta, value); } } private double _delta; public Sensor(IAvgValueIndicator indicator) { //        BeforeNotify(() => Value).Notify(() => indicator.AvgValue); IValueProvider valueProvider = new RandomValueProvider(); Value = valueProvider.GetValue(this); } } /// <summary> ///   ,  . /// </summary> public class Device : ChangeNotifier, IAvgValueIndicator { /// <summary> ///  . /// </summary> private const int SensorsCount = 3; /// <summary> ///    . /// </summary> public IReadOnlyCollection<Sensor> Sensors { get { return _sensors; } } private IReadOnlyCollection<Sensor> _sensors; /// <summary> ///    . /// </summary> public double AvgValue { get { return (Sensors.Sum(s => s.Value)) / (double)Sensors.Count; } } public Device() { InitSensors(); AfterNotify(() => AvgValue).Execute(UpdateDelta); NotifyPropertyChanged(() => AvgValue); } private void InitSensors() { var sensors = new List<Sensor>(); for (int i = 0; i < SensorsCount; i++) { var sensor = new Sensor(this); //BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue); sensors.Add(sensor); } _sensors = sensors; } private void UpdateDelta() { foreach (var sensor in Sensors) sensor.Delta = Math.Abs(sensor.Value - AvgValue); } } 

The lines of code of interest are:
 SetProperty(ref _delta, value); NotifyPropertyChanged(() => AvgValue); AfterNotify(() => AvgValue).Execute(UpdateDelta); BeforeNotify(() => Value).Notify(() => indicator.AvgValue); BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue); 

Separately, we analyze each construction and look at the implementation of the above methods.

Implementation


SetProperty (ref _delta, value)


This code assigns a value from the second parameter to the field passed in the first parameter of the method, and also notifies subscribers about a change in the property whose name is passed to the third parameter. If the third parameter is not specified, the name of the calling property is used.
 protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "") { if (Equals(field, value)) { return; } field = value; NotifyPropertyChangedInternal(propertyName); } 

NotifyPropertyChanged (() => AvgValue)


All notification methods about changing objects, whether they accept the expression tree or the string value of the property name, eventually call the following method:
 private void NotifyPropertyChanged(PropertyChangedEventHandler handler, string propertyName) { NotifyLinkedPropertyListeners(propertyName, BeforeChangeLinkedChangeNotifierProperties); if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } OnPropertyChanged(propertyName); NotifyLinkedPropertyListeners(propertyName, AfterChangeLinkedChangeNotifierProperties); } private void NotifyLinkedPropertyListeners(string propertyName, Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedChangeNotifiers) { LinkedPropertyChangeNotifierListeners changeNotifierListeners; if (linkedChangeNotifiers.TryGetValue(propertyName, out changeNotifierListeners)) { changeNotifierListeners.NotifyAll(); } } 

Each inherited ChangeNotifier object stores bundles of “property name” -> “set of property change listeners” bundles:
 private Dictionary<string, LinkedPropertyChangeNotifierListeners> AfterChangeLinkedChangeNotifierProperties { get { ... } } private Dictionary<string, LinkedPropertyChangeNotifierListeners> BeforeChangeLinkedChangeNotifierProperties { get { ... } } 

Separately, it is necessary to consider the class LinkedPropertyChangeNotifierListeners :
 private class LinkedPropertyChangeNotifierListeners { /// <summary> ///   " " - "   " /// </summary> private readonly Dictionary<ChangeNotifier, OnNotifyExecuties> _linkedObjects = new Dictionary<ChangeNotifier, OnNotifyExecuties>(); /// <summary> ///    . /// </summary> /// <param name="linkedObject"> .</param> /// <param name="targetPropertyName">     .</param> public void Register(ChangeNotifier linkedObject, string targetPropertyName) { var executies = GetOrCreateExecuties(linkedObject); if (!executies.ProprtiesToNotify.Contains(targetPropertyName)) { executies.ProprtiesToNotify.Add(targetPropertyName); } } /// <summary> ///    . /// </summary> /// <param name="linkedObject"> .</param> /// <param name="action">  .</param> public void Register(ChangeNotifier linkedObject, Action action) { var executies = GetOrCreateExecuties(linkedObject); if (!executies.ActionsToExecute.Contains(action)) { executies.ActionsToExecute.Add(action); } } /// <summary> ///          . /// </summary> /// <param name="linkedObject"> .</param> /// <returns>      .</returns> private OnNotifyExecuties GetOrCreateExecuties(ChangeNotifier linkedObject) { OnNotifyExecuties executies; if (!_linkedObjects.TryGetValue(linkedObject, out executies)) { executies = new OnNotifyExecuties(); _linkedObjects.Add(linkedObject, executies); } return executies; } /// <summary> ///        . /// </summary> public void NotifyAll() { foreach (var linkedObject in _linkedObjects) { NotifyProperties(linkedObject.Key, linkedObject.Value.ProprtiesToNotify); ExecuteActions(linkedObject.Value.ActionsToExecute); } } /// <summary> ///        . /// </summary> /// <param name="linkedObject"> .</param> /// <param name="properties">     .</param> private void NotifyProperties(ChangeNotifier linkedObject, IEnumerable<string> properties) { foreach (var targetProperty in properties) { linkedObject.NotifyPropertyChangedInternal(targetProperty); } } /// <summary> ///  . /// </summary> /// <param name="actions"></param> private void ExecuteActions(IEnumerable<Action> actions) { foreach (var action in actions) { action(); } } private class OnNotifyExecuties { private List<string> _proprtiesToNotify; private List<Action> _actionsToExecute; public List<string> ProprtiesToNotify { get { return _proprtiesToNotify ?? (_proprtiesToNotify = new List<string>()); } } public List<Action> ActionsToExecute { get { return _actionsToExecute ?? (_actionsToExecute = new List<Action>()); } } } } 

Thus, for each property, the source object stores a collection of related objects, properties of related objects, which must be changed by notifying subscribers, and actions that must be performed before or after notification.

It is worth noting that during registration, the subscription is unique. If you have ever tried to find the reason for a two-time notification about a change in an object, then you will be very happy with this feature.

AfterNotify (() => AvgValue) .Execute (UpdateDelta)
BeforeNotify (sensor, s => s.Value) .Notify (() => AvgValue)
BeforeNotify (() => Value) .Notify (() => indicator.AvgValue);


To add a new related object and act on it, use the sequence of calling the AfterNotify / BeforeNotify methods of the ChangeNotifier class and the Notify / Execute methods of the heir classes ILinkedPropertyChanged. The latter are the classes nested with respect to the ChangeNotifier classes AfterLinkedPropertyChanged and BeforeLinkedPropertyChanged.
 /// <summary> ///         . /// </summary> private class BeforeLinkedPropertyChanged : ILinkedPropertyChanged { /// <summary> ///  . /// </summary> private readonly ChangeNotifier _sourceChangeNotifier; /// <summary> ///    . /// </summary> private readonly string _sourceProperty; /// <summary> ///  . /// </summary> private readonly ChangeNotifier _targetChangeNotifier; public BeforeLinkedPropertyChanged(ChangeNotifier sourceChangeNotifier, string sourceProperty, ChangeNotifier targetChangeNotifier) { _sourceChangeNotifier = sourceChangeNotifier; _sourceProperty = sourceProperty; _targetChangeNotifier = targetChangeNotifier; } /// <summary> ///        . /// </summary> /// <param name="targetProperty">  .</param> /// <returns>.</returns> public ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty) { _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener( _sourceProperty, _targetChangeNotifier, (string) targetProperty.GetName()); return this; } /// <summary> ///       . /// </summary> /// <param name="action">.</param> /// <returns>.</returns> public ILinkedPropertyChanged Execute(Action action) { _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener( _sourceProperty, _targetChangeNotifier, action); return this; } } 

For binding, use the RegisterBeforeLinkedPropertyListener / RegisterAfterLinkedPropertyListener methods of the ChangeNotifier class:
 public abstract class ChangeNotifier : INotifyPropertyChanged { ... private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName, ChangeNotifier targetObject, string targetPropertyName) { RegisterLinkedPropertyListener( linkedPropertyName, targetObject, targetPropertyName, BeforeChangeLinkedChangeNotifierProperties); } private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName, ChangeNotifier targetObject, Action action) { RegisterLinkedPropertyListener(linkedPropertyName, targetObject, action, BeforeChangeLinkedChangeNotifierProperties); } private static void RegisterLinkedPropertyListener(string linkedPropertyName, ChangeNotifier targetObject, string targetPropertyName, Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties) { GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, targetPropertyName); } private static void RegisterLinkedPropertyListener(string linkedPropertyName, ChangeNotifier targetObject, Action action, Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties) { GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, action); } private static LinkedPropertyChangeNotifierListeners GetOrCreatePropertyListeners(string linkedPropertyName, Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties) { LinkedPropertyChangeNotifierListeners changeNotifierListeners; if (!linkedProperties.TryGetValue(linkedPropertyName, out changeNotifierListeners)) { changeNotifierListeners = new LinkedPropertyChangeNotifierListeners(); linkedProperties.Add(linkedPropertyName, changeNotifierListeners); } return changeNotifierListeners; } ... } 

The AfterNotify / BeforeNotify methods create new instances of "binders" to provide a simple binding interface:
 protected ILinkedPropertyChanged AfterNotify(Expression<Func<object>> property) { var propertyCall = PropertyCallHelper.GetPropertyCall(property); return new AfterLinkedPropertyChanged((INotifyPropertyChanged) propertyCall.TargetObject, propertyCall.TargetPropertyName, this); } protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property) { var propertyCall = PropertyCallHelper.GetPropertyCall(property); return new BeforeLinkedPropertyChanged((ChangeNotifier) propertyCall.TargetObject, propertyCall.TargetPropertyName, this); } protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property) where T : INotifyPropertyChanged { return new AfterLinkedPropertyChanged(changeNotifier, property.GetName(), this); } protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property) where T : ChangeNotifier { return new BeforeLinkedPropertyChanged(changeNotifier, property.GetName(), this); } 

From the last listing, you can see that the current object is always the associated object, and the source object can be either an explicitly specified instance or obtained by parsing the expression tree using the auxiliary class PropertyCallHelper . Often, the source and bound objects are the same.

Please do not need any more listings.


OK. Once again on the fingers. The ChangeNotifier object contains several collections that store data about the properties of objects associated with the notification, the properties of these objects that are notified, and also about the actions that must be called before or after the notification. To provide a simple object binding interface, the AfterNotify / BeforeNotify methods return the heirs of ILinkedPropertyChanged, which allow you to easily add the necessary information to the collection. ILinkedPropertyChanged methods return the original ILinkedPropertyChanged object, which allows you to use a call chain for registration.

Upon notification of a change in a property, an object refers to collections of related objects and invokes all necessary pre-registered actions.

ChangeNotifier provides a convenient interface for changing properties of objects and notifying changes to properties, which minimizes the cost of parsing expression trees. All dependencies can be assembled in the constructor.

Decision to use


This is not really an article about the library, which you can just start using. I wanted to show the internal implementation of one of the typical solutions for WPF as part of MVVM, the simplicity of this solution, the simplicity of its use, extensibility. Without knowledge of implementation, it is much easier to misuse a used tool. For example, Microsoft Prism 4 allowed to notify on changes of properties by means of transfer of a tree of expressions, but only the basic scenario "() => PropertName" participated in analysis. Thus, if the calculated property was in a different class, then there was no way to notify about the change from the original property. Which is logical, but leaves room for error.

In the original article there is a comparison of the most common solutions to the problem, but this review will not be complete without an understanding of the internal mechanisms of these solutions. It’s much easier to start using code that you trust as your own. I hope everyone who has reached this line will be able to make a basic implementation to automatically update the associated properties, which means that they will also be able to understand someone else’s implementation.

Source: https://habr.com/ru/post/271305/


All Articles