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,
                                            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)
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,
                                    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 предоставляет удобный интерфейс для изменения свойств объектов и нотификации об изменениях свойствах, который минимизирует затраты на разбор деревьев выражений. Все зависимости можно собрать в конструкторе.

Решение об использовании


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

В исходной статье есть сравнение наиболее распространенных решений поставленной задачи, но этот обзор не будет полным без понимания внутренних механизмов работы этих решений. Гораздо проще начать пользоваться кодом, которому доверяешь как своему. Надеюсь, каждый, кто добрался до этой строки, сможет сделать базовую реализацию для автоматического обновления связанных свойств, а значит сможет и разобраться в чужой реализации.

Комментарии (12)


  1. Vadimyan
    30.11.2015 10:32
    +1

    В очередном проекте на 3 вкладки понадобилось менять доступность команд при изменении свойств, не являющихся параметрами этих команд. Вспомнил о старом друге и решил заодно поделиться им. Когда речь идёт о 500 строках кода — решение о добавлении новой зависимости для меня не является однозначным.


  1. novar
    30.11.2015 12:13

    Благодарю за статью, но по моему скромному мнению, как то всё вышеописанное необоснованно сложновато. Разве зависимость одного свойства от другого не реализуется в WPF через привязку свойство1.свойство2.свойство3 (и т.д.)? Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно. Нам остаётся только грамотно продумать объектную иерархию. На худой конец, когда надо отслеживать не связанные объекты, можно использовать MultiBinding, используя простейшие конверторы.
    Да, с отображением доступности команд (действий), заморочек особенно много. Но тут проблема в отсутствии правильных библиотечных команд, которые должны содержать в себе нужную функциональность. Я для себя сам сделал такие (см. Инфраструктура команд для вызова пользователем действий в шаблоне MVVM) и практически забыл про проблему уведомления об изменении связанных свойств.
    И ещё, заголовок статьи по моему слишком абстрактный. Хорошо бы добавить в него какую то главную мысль статьи.


    1. Vadimyan
      30.11.2015 14:22
      +1

      Например, указывая в привязке Model.Property1.Property2.Property3 мы автоматически получаем отслеживание изменения в Property1, 2 и 3 одновременно.

      Чего-то я здесь не понял, наверное. Речь идёт о зависимых и вычисляемых свойствах, ссылаясь на ту же исходную статью (сейчас я думаю, что можно было взять пример из неё ради сравнения), это, например, сумма заказа. Но иногда у нас есть агрегирующая ViewModel, которая зависит от внутренних VM. Это тоже пример вычисляемых свойств. Да, MultiBinding тоже есть, но под него всегда нужен конвертер и это переносит ближе к View нашу взаимосвязь. А View должна быть глупой.
      С командами история отдельная и решений тоже достаточно. Видел даже патченный CompositeCommand из Prism, который делает RaiseCanExecuteChanged по движению мыши. Производительность — давай, до свидания.


  1. Makeman
    30.11.2015 14:06
    +3

    Существует ещё более лаконичный способ регистрации обработчиков:

    this[() => Text].PropertyChanging += (o, args) => { ... };
    this[() => Text].PropertyChanged += (o, args) => { ... };
    

    Посмотрите эвокаторы свойств и команд в библиотеке Aero Framework, сама идея похожа на вашу.

    Пример простой вьюмодели
        [DataContract]
        public class GuyViewModel : ContextObject, IExposable
        {
            [DataMember]
            public int Kisses
            {
                get { return Get(() => Kisses); }
                set { Set(() => Kisses, value); }
            }
    
            public void Expose()
            {
                var girlViewModel = Store.Get<GirlViewModel>();
    
                this[() => Kisses].PropertyChanged += (sender, args) =>
                {
                    Context.Get("KissGirl").RaiseCanExecuteChanged();
                    Context.Get("KissGuy").RaiseCanExecuteChanged();
                };
    
                this[Context.Get("KissGirl")].CanExecute += (sender, args) => 
                    args.CanExecute = Kisses > girlViewModel.Kisses - 2;
    
                this[Context.Get("KissGirl")].Executed += (sender, args) => 
                    girlViewModel.Kisses++;
            }
        }
    


    1. Makeman
      01.12.2015 07:41
      +2

      Стоит отметить, что эвокаторы свойств в Aero Framework также поддерживают достаточно удобные способы как синхронной валидации свойств (IDataErrorInfo), так и асинхронной (INotifyDataErrorInfo). Выглядит подобным образом:

      this[() => Mouse].ErrorsChanged += (sender, args) => HasErrors = !(5 < Mouse.Length && Mouse.Length < 20);
      this[() => Mouse].ValidationRules += s => 5 < Mouse.Length && Mouse.Length < 20 ? null : "Invalid Length";
      


      1. Vadimyan
        01.12.2015 10:01
        +2

        via

        verifiableObject.ForProperty(vo => vo.TotalSize).AddValidationRule(s => s > 20);
        verifiableObject.ForObject().AddAsyncValidationRule(ct => ValidateLengthAsync(ct)).AlsoValidate(vo => vo.TotalSize);
        

        Работает через тот же AfterNotify. В этой статье я специально опустил подробности о ViewModelBase, Workspaces для управления ж.ц. отображений и прочие свистелки, сделав упор на INPC.


        1. Makeman
          01.12.2015 16:48
          +1

          Рекомендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать. Собственно, исторически в аэро фремворке сначала появились эвокаторы команд, а только потом свойств :)

          this[MediaCommands.Play].CanExecute += (sender, args) => args.CanExecute = IsStopped;
          this[Context.Get("HelloCommand")].Executed += (sender, args) => MessageBox.Show("Hello!");
          

          Это крутая фича для тех, у кого страсть к простоте и лаконичности кода.


          1. Vadimyan
            01.12.2015 18:06
            +2

            Ок, раз речь зашла о командах. Вот такая реализация в Rikrop:

            MyCommand = new RelayCommandBuilder(p => DoSomeWork(p)).AddCanExecute(p => p.ReadyToWork).InvalidateOnNotify(_myDependency, md => md.SomeProperty).AddBlocker(_myServiceExecutor).CreateCommand();
            

            Можно добавлять сколько угодно много CanExecute, проверяться будут все, можно добавлять инвалидацию по изменению какого-либо свойства текущего объекта или другого объекта (а как это делается у вас?), можно добавлять блокировку по изменению состояния валидируемого объекта (см. выше), можно добавлять блокировку по состоянию IBusyItem — это такая обёртка над всем, что может блокировать что-либо (долгоиграющие операции).

            Инвалидация от произвольных свойств нужна довольно часто, по состоянию валидации модели тоже приходится изменять доступность команд. Так что рекомаендую ещё эту идею в вашей библиотеке и для команд применить — очень красиво получается, если всё грамотно сделать.

            Нет больше сил читать слово эвокатор — каждый раз перед глазами одно и то же:
            image


            1. Makeman
              01.12.2015 23:24

              Ок, забудем это слово :)

              Инвалидация контекстных команд происходит, например, следующим образом по изменению свойства:

                          this[() => Kisses].PropertyChanged += (sender, args) =>
                          {
                              Context.Get("KissGirl").RaiseCanExecuteChanged();
                              Context.Get("KissGuy").RaiseCanExecuteChanged();
                          };
              

              Преимущество такого способа в сравнении с вашим в том, что легко можно добавить условную логику в этот процесс, а не только ориентироваться на само событие изменения свойства, вот что имею в виду:

                          this[() => Kisses].PropertyChanged += (sender, args) =>
                          {
                              if (Kesses < 10) return;
                              Context.Get("KissGirl").RaiseCanExecuteChanged();
                              Context.Get("KissGuy").RaiseCanExecuteChanged();
                          };
              

              Поскольку в некоторых случаях обработчик CanExecute вызывать затратно или излишне.

              Контекстные команды очень похожи в использовании на RoutedCommands из WPF, по сути, это их упрощённая кроссплатформенная модель.

              Интересно, у вас можно сделать что-то наподобие такого?

              this[Context.Refresh].Executed += async (sender, args) =>
              {
                    try
                    {
                          IsBusy = true;
                          User = await server.GetUserData(Login, Password);
                    }
                    finally
                    {
                          IsBusy = false;
                    }
              };
              

              Имею в виду асинхронность async/await.


              1. Vadimyan
                02.12.2015 16:08

                AfterNotify(() => Kisses).Execute(() => 
                {
                    if (Kesses < 10) return;
                    _myFirstCommand.RaiseCanExecuteChanged();
                    _anotherOneCommand.RaiseCanExecuteChanged();
                });
                

                GetUserCommand = new RelayCommandBuilder<int>(async id => User  await userServiceExecutor.Execute(us => us.GetUser(id))).CreateCommand();
                

                AfterNotify(() => Scene.Dimension).Execute(async () => await OnCombine(Scene.Dimension));
                

                Предлагаю ничью пока вы не расчехлили сохранение состояния конфигурации, а я MapReduce движок.


                1. Makeman
                  02.12.2015 16:43

                  Добро, согласен на ничью )
                  Довольно интересно наблюдать, как получают развитие некоторые схожие идеи, но разными путями.
                  Можете в двух словах описать, что умеет ваш MapReduce движок?


            1. Makeman
              02.12.2015 01:02

              И ещё вопрос, допускаются ли в вашей реализации асинхронные обработчики у событий нотификации?

              this[() => Text].PropertyChanged += async (sender, args) =>
              {
                     IsSaved = await server.SaveText(Text); 
              };
              

              Иногда очень полезно иметь такую возможность.