К оглавлению
Мы любим создавать 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, однако там разбор дерева ограничивается простейшими и базовыми случаями, что позволяет использовать нотификацию об изменении свойств только в самых типичных сценариях.
Комментариев нет:
Отправить комментарий