четверг, 24 октября 2013 г.

Фреймворк: Rikrop.Core.Wpf.dll. Часть 0, INotifyPropertyChanged

К оглавлению

 Rikrop.Core.Wpf.dll

Библиотека содержит огромное количество полезных инструментов для быстрого создания отзывчивых и умных wpf-приложений.
Мы любим создавать wpf-приложения. И мы делаем это так часто, что просто не смогли обойтись без полной целостной инфраструктуры для поддержания паттерна mvvm. Мы пробовали использовать разные подходы: view-first, viewmodel-first. И очень важной целью было создание такого набора классов, который позволял бы с минимальными затратами решать типичные для этих подходов задачи.
Мы глубоко интегрировали работу с деревьями выражений для упрощения работы с INotifyPropertyChanged. Для навигации и управления представлением используется абстракция рабочих областей, которая является центральной при построении отображения в wpf.

namespace Rikrop.Core.Wpf

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