Базова реалізація INotifyPropertyChanged

WPF в чомусь повторив долю js — в силу деяких невирішених на рівні платформи проблем багато намагаються стати першовідкривачами нарівні з Карлом фон Дрезем.

Проблема
У випадку з INPC в ViewModel часто існують властивості, які залежать від інших або обчислювані на їх основі. Для .net 4.0 ситуація з реалізацією ускладнюється тим, що CallerMemberNameAttribute не підтримується цією версією (насправді підтримується, якщо ви маг і чарівник).

Рішення
ПередмоваРозбираючи в черговий раз проект з десятками рядків у package-файлі, мені стає все ближче концепція UNISTACK, коли комплексне добре інтегроване рішення дозволяє реалізовувати типові задачі в типових ситуаціях і залишає місце для розширення під потреби користувача. І одночасно з цим я бачу фатальні недоліки існуючих рішень — громіздкість і ваговитість. І, іноді, немодульность.

попередній статті я обіцяв показати приклад якраз такої інтеграції — коли для будь-якої запущеної в обертке асинхронної завдання блокується UI і відображається BusyIndicator або його настроюється аналог. І я все ще обіцяю показати приклад. Так ми обертаємо всі виклики WCF, але це можна використовувати і для довгограючих обчислень, перегрупування великих колекцій і подібних операцій.

Однією з основ бібліотеки Rikrop.Core.Wpf служить базовий клас об'єкта реалізує інтерфейс 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, value)

Цей код привласнює полю, переданим у першому параметрі методу, значення другого параметра, а так само повідомляє підписувачів про зміну властивості, ім'я якого передається третім параметром. Якщо третій параметр не вказано, використовується ім'я викликає властивості.
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,
Словник<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(sensor, s => s.Value).Notify(() => AvgValue)
BeforeNotify(() => Value).Notify(() => indicator.AvgValue);

Для додавання нового зв'язаного об'єкта і дій над ним служить послідовність виклику методів AfterNotify/BeforeNotify класу ChangeNotifier і методів Notify/Execute класів-спадкоємців ILinkedPropertyChanged. В якості останніх виступають вкладені по відношенню до 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;
}
}

Для зв'язування використовуються методи RegisterBeforeLinkedPropertyListener/RegisterAfterLinkedPropertyListener класу ChangeNotifier:
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,
Словник<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
{
GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, targetPropertyName);
}

private static void RegisterLinkedPropertyListener(string linkedPropertyName,
ChangeNotifier targetObject,
Action action,
Словник<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
{
GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, action);
}

private static LinkedPropertyChangeNotifierListeners GetOrCreatePropertyListeners(string linkedPropertyName,
Словник<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 надає зручний інтерфейс для зміни властивостей об'єктів та нотифікації про зміни властивостях, який мінімізує витрати на розбір дерев виразів. Всі залежності, можна зібрати в конструкторі.

Рішення про використання
Це не зовсім стаття про бібліотеку, якою можна просто почати користуватися. Я хотів показати внутрішню реалізацію одного з варіантів вирішення типової для WPF в рамках MVVM завдання, простоту цього рішення, простоту його використання, розширюваність. Без знання реалізації набагато простіше неправильно застосувати використовуваний інструмент. Наприклад, Microsoft Prism 4 дозволяв повідомляти про зміну властивостей за допомогою передачі дерева виразів, але в розборі брав участь тільки базовий сценарій "() => PropertName". Таким чином, якщо обчислюване властивість знаходилося в іншому класі, то не було ніякої можливості повідомити про його зміну з вихідного властивості. Що логічно, але залишає простір для помилки.

вихідної статті є порівняння найбільш поширених рішень поставленої задачі, але цей огляд не буде повним без розуміння внутрішніх механізмів роботи цих рішень. Набагато простіше почати користуватися кодом, якому довіряєш як своєму. Сподіваюся, кожен, хто добрався до цього рядка, зможе зробити базову реалізацію для автоматичного оновлення зв'язаних властивостей, а значить і зможе розібратися в чужій реалізації.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.