INotifyPropertyChangedの基本的な実装

WPFはjsの運命を幾分繰り返しました-プラットフォームレベルでの未解決の問題のために、 多く Karl von Dresemと共に発見者になろうとしています。



問題



INPCの場合、ViewModelには多くの場合、他に依存するか、それらに基づいて計算されるプロパティがあります。 .net 4.0の場合、このバージョンではCallerMemberNameAttributeがサポートされていないため、実装状況は複雑ですマジシャンでウィザードの場合は実際にサポートされています)。



解決策



まえがき
パッケージファイルに数十行のプロジェクトをもう一度解析すると、UNISTACKの概念は私に近づいています。包括的な統合ソリューションにより、典型的なシナリオで典型的なタスクを実装でき、ユーザーのニーズに合わせて拡張する余地があります。 同時に、既存のソリューションの致命的な欠陥 -面倒で重いものを確認します。 そして、時には、モジュール性。



前回の記事では、まさにそのような統合の例を示すことを約束しました- ラッパーで起動された非同期タスクの場合 UIがブロックされ、BusyIndi​​catorまたはそのカスタム同等物が表示されます。 そして、私はまだこの例を設定することを約束します。 したがって、すべてのWCF呼び出しをラップしますが、長時間実行されるコンピューティング、大規模なコレクションの再グループ化、および同様の操作にも使用できます。



Rikrop.Core.Wpfライブラリの基盤の1つは、INotifyProprtyChangedインターフェイスを実装するオブジェクトの基本クラスであるChangeNotifierです。これは、次のメソッドセットを継承します。

[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) }
      
      





ここでは、すぐにILinkedPropertyChangedおよび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; }
      
      





不自然なユースケース



例のないところは、どちらが先取りされて非現実的と呼ばれますか? さまざまなシナリオでChangeNotifierを使用する方法を見てみましょう。



同じタイプのN個のセンサーを持つデバイスがあり、すべてのセンサーの平均値を表示します。 各センサーには、測定値と平均からの偏差が表示されます。 センサーの値を変更する場合、最初に平均値を再カウントしてから、センサー自体の変更について通知する必要があります。 平均値を変更する場合、各センサーの平均からの偏差を再計算する必要があります。

 /// <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); } }
      
      





興味のあるコード行は次のとおりです。

 SetProperty(ref _delta, value); NotifyPropertyChanged(() => AvgValue); AfterNotify(() => AvgValue).Execute(UpdateDelta); BeforeNotify(() => Value).Notify(() => indicator.AvgValue); BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);
      
      





各構成を個別に分析し、上記の方法の実装を見ていきます。



実装



SetProperty(ref _delta、値)



このコードは、メソッドの最初のパラメーターで渡されたフィールドを2番目のパラメーターの値に設定し、3番目のパラメーターで名前が渡されたプロパティの変更をサブスクライバーに通知します。 3番目のパラメーターが指定されていない場合、呼び出し元のプロパティの名前が使用されます。

 protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "") { if (Equals(field, value)) { return; } field = value; NotifyPropertyChangedInternal(propertyName); }
      
      





NotifyPropertyChanged(()=> AvgValue)



オブジェクトを変更するためのすべての通知メソッドは、式ツリーまたはプロパティ名の文字列値を受け入れるかどうかにかかわらず、最終的に次のメソッドを呼び出します。

 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(); } }
      
      





ChangeNotifierの各後続オブジェクトには、バインディングのコレクション「プロパティ名」->「プロパティの変更に関する通知用のリスナーのセット」が格納されます。

 private Dictionary<string, LinkedPropertyChangeNotifierListeners> AfterChangeLinkedChangeNotifierProperties { get { ... } } private Dictionary<string, LinkedPropertyChangeNotifierListeners> BeforeChangeLinkedChangeNotifierProperties { get { ... } }
      
      





別に、 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>()); } } } }
      
      





したがって、ソースオブジェクトは、プロパティごとに、関連オブジェクトのコレクション、関連オブジェクトのプロパティ、サブスクライバへの変更の通知、および通知の前後に実行する必要のあるアクションを格納します。



登録時に、サブスクリプションの一意性がチェックされることに注意してください。 オブジェクトの変更の二重通知の理由を見つけようとしたことがあれば、そのような機能に非常に満足するでしょう。



AfterNotify(()=> AvgValue).Execute(UpdateDelta)

BeforeNotify(センサー、s => s.Value).Notify(()=> AvgValue)

BeforeNotify(()=> Value).Notify(()=> indicator.AvgValue);



新しい関連オブジェクトとそれにアクションを追加するには、ChangeNotifierクラスのAfterNotify / BeforeNotifyメソッドとILinkedPropertyChanged継承クラスのNotify / Executeメソッドを呼び出すシーケンスを使用します。 後者は、ChangeNotifierに関してネストされたAfterLinkedPropertyChangedおよび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; } }
      
      





バインドするには、ChangeNotifierクラスのメソッドRegisterBeforeLinkedPropertyListener / RegisterAfterLinkedPropertyListenerを使用します。

 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; } ... }
      
      





AfterNotify / BeforeNotifyメソッドは、「バインダー」の新しいインスタンスを作成して、シンプルなバインディングインターフェイスを提供します。

 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); }
      
      





最後のリストから、現在のオブジェクトは常にリンクされたオブジェクトとして機能し、明示的に指定されたインスタンスまたはヘルパークラスPropertyCallHelperを使用して式ツリーの解析から取得したいずれかをソースオブジェクトとして使用できます。 多くの場合、ソースとリンクされたオブジェクトは同じです。



これ以上リストはありません



わかった 再び指に。 ChangeNotifierオブジェクトには、プロパティの通知に関連するオブジェクト、これらのオブジェクトの通知されたプロパティ、および通知の前後に呼び出す必要があるアクションに関するデータを格納するいくつかのコレクションが含まれます。 オブジェクトバインドのシンプルなインターフェイスを提供するために、AfterNotify / BeforeNotifyメソッドはILinkedPropertyChangedの子孫を返します。これにより、コレクションに情報を簡単に追加できます。 ILinkedPropertyChangedメソッドは元のILinkedPropertyChangedオブジェクトを返します。これにより、コールチェーンを使用して登録できます。



プロパティの変更が通知されると、オブジェクトは関連オブジェクトのコレクションにアクセスし、事前に登録されているすべての必要なアクションを呼び出します。



ChangeNotifierは、オブジェクトのプロパティを変更したり、プロパティの変更を通知したりするための便利なインターフェイスを提供します。これにより、式ツリーの解析コストが最小限に抑えられます。 すべての依存関係は、コンストラクターでアセンブルできます。



使用する決定



これは、使用を開始できるライブラリに関する記事ではありません。 MVVMタスクのフレームワーク内でのWPFに典型的なソリューションの1つの内部実装、このソリューションのシンプルさ、その使用のシンプルさ、および拡張性を示したかったのです。 実装に関する知識がなければ、使用したツールを誤用するのははるかに簡単です。 たとえば、Microsoft Prism 4では、式ツリーを渡すことでプロパティの変更を通知できましたが、分析には基本スクリプト「()=> PropertName」のみが関与していました。 したがって、計算されたプロパティが別のクラスにある場合、元のプロパティからの変更について通知する方法はありませんでした。 これは論理的ですが、エラーの余地があります。



元の記事には、問題に対する最も一般的なソリューションの比較が含まれていますが、これらのソリューションの内部メカニズムを理解しないと、このレビューは完了しません。 信頼できるコードを独自のものとして使用する方がはるかに簡単です。 この行にたどり着くすべての人が、関連するプロパティを自動的に更新するための基本的な実装を作成できることを願っています。つまり、他の誰かの実装も把握できることを意味します。




All Articles