К оглавлению
Мы любим создавать wpf-приложения. И мы делаем это так часто, что просто не смогли обойтись без полной целостной инфраструктуры для поддержания паттерна mvvm. Мы пробовали использовать разные подходы: view-first, viewmodel-first. И очень важной целью было создание такого набора классов, который позволял бы с минимальными затратами решать типичные для этих подходов задачи.
Мы глубоко интегрировали работу с деревьями выражений для упрощения работы с INotifyPropertyChanged. Для навигации и управления представлением используется абстракция рабочих областей, которая является центральной при построении отображения в wpf.
Rikrop.Core.Wpf.dll
Библиотека содержит огромное количество полезных инструментов для быстрого создания отзывчивых и умных wpf-приложений.Мы любим создавать wpf-приложения. И мы делаем это так часто, что просто не смогли обойтись без полной целостной инфраструктуры для поддержания паттерна mvvm. Мы пробовали использовать разные подходы: view-first, viewmodel-first. И очень важной целью было создание такого набора классов, который позволял бы с минимальными затратами решать типичные для этих подходов задачи.
Мы глубоко интегрировали работу с деревьями выражений для упрощения работы с INotifyPropertyChanged. Для навигации и управления представлением используется абстракция рабочих областей, которая является центральной при построении отображения в wpf.
namespace Rikrop.Core.Wpf
Для удобной, типобезопасной и прозрачной работы с классами, реализующими интерфейс INotifyPropertyChanged, мы создали базовую реализацию ChangeNotifier, интерфейс которого выглядит следующим образом:
Здесь же сразу стоит указать интерфейсы ILinkedPropertyChanged и ILinkedObjectChanged:
Как и зачастую в программировании - реализация проще, чем может показаться на первый взгляд, а использование еще проще, чем реализация. Если попытаться описать назначение методов 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 "на пальцах", то этот класс позволяет информировать об изменениях свойств с помощью деревьев выражений, а так же указывать взаимосвязи между свойствами таким же образом. Для повышения производительности мы кэшируем деревья выражений и при повторном использовании применяем уже разобранные деревья.*
Но что может быть понятнее простого практического примера?! Следующий код иллюстрирует устройство с несколькими датчиками, где каждый датчик имеет значение и известное ему отклонение от среднего значения, а устройство управляет обновлением данных:
/// <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() { IValueProvider valueProvider = new RandomValueProvider(); Value = valueProvider.GetValue(this); } } /// <summary> /// Прибор с датчиками, проводящими измерения. /// </summary> public class Device : ChangeNotifier { /// <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(); BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue); sensors.Add(sensor); } _sensors = sensors; } private void UpdateDelta() { foreach (var sensor in Sensors) sensor.Delta = GetDelta(sensor); } private double GetDelta(Sensor sensor) { return Math.Abs(sensor.Value - AvgValue); } }
Интересующие нас строки кода:
SetProperty(ref _delta, value); NotifyPropertyChanged(() => AvgValue); AfterNotify(() => AvgValue).Execute(UpdateDelta); 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, 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(sensor, s => s.Value).Notify(() => 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, 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 предоставляет удобный интерфейс для изменения свойств объектов и нотификации об изменениях свойствах, который минимизирует затраты на разбор деревьев выражений.
Еще более простое объяснение.
Нет ничего проще, чем использовать ChangeNotifier в качестве базового класса для объектов, требующих реализации INotifyPropertyChanged. Мы применяем его класс для построения модели отображения и создания ViewModel. Это удобный инструмент, который экономит десятки часов разаботки на каждом нашем проекте. Короткое объяснение работы ChangeNotifier - он просто работает. И работает хорошо.
*Стоит упомянуть, что реализация извещения об изменении свойств при помощи деревьев выражений есть и в других фреймворках, например у класса NotificationObject в Microsoft prism 4, однако там разбор дерева ограничивается простейшими и базовыми случаями, что позволяет использовать нотификацию об изменении свойств только в самых типичных сценариях.
Комментариев нет:
Отправить комментарий