Чистый код под флагом АОП и ненавистный #ПредупреждаюНедвижимостьИзменился

    Поддавшись общей истерии на хабре,

    (а именно «Предупреждаю Недвижимость Изменился» переводит Гуглекс всеми любимый «I Notify Property Changed») по поводу уведомлений об изменении. Я решил посмотреть на сколько далеко продвинулось человечество в изобретении велосипедов.

    OnPropertyChanged(«Property»)
    OnPropertyChanged(()=>Property)
    SetProperty(ref storage, value)
    [CallerMemberName]
    [Magic]
    АОП разных мастей
    даже предлагают roslyn.codeplex.com/discussions/550266 почему бы м нет.
    Круче всех все же nameof(Property) — осторожно си шарп 6.
    Последствия истерии выражаются в следующих работах
    habrahabr.ru/post/199378
    habrahabr.ru/post/246469
    habrahabr.ru/post/95211
    Лично меня устраивает варианты OnPropertyChanged(nameof(Property)) и OnPropertyChanged(()=>Property), первый вариант работает быстрее.
    Но чаще всего я использую SetProperty(ref storage, value), коробочный вариант BindableBase.
    Хабражитель Scrooge2 запостил
    Хватит изобреать велосипеды.
    Использовать обычный INotifyPropertyChanged руки не отпадут, без всяких но.

    Полностью поддерживаю, но… НЕТ.
    Ну что же, напильник и кувалда.
    Я за чистый код! Логирование, перехваты исключений, проверка прав доступа, всевозможные уведомления и т.д. — задымляют код.Очистить код позволят древние знания шаблона Proxy, например habrahabr.ru/post/88722.
    Осложню себе жизнь и буду использовать классы а не интерфейсы.
    public class Data
        {
            public virtual int Value { get; set; }
    
            public virtual string Source { get; set; }
        }
    
    

    Уведомления, ничего личного
    class BindableObject : BindableBase
        {
            public new bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
            {
                return base.SetProperty<T>(ref storage, value, propertyName);
            }
        }
    

    И сам заместитель, взявший на себя всю грязную работу
    public class ProxyData : Data, INotifyPropertyChanged
        {
            Data _target;
            BindableObject _notifier;
    
            public ProxyData(Data target)
            {
                _target = target;
                _notifier = new BindableObject();
            }
    
            public override int Value
            {
    
                set
                {
                    int newValue = 0;
    
                    if (_notifier.SetProperty(ref newValue, value))
                        base.Value = newValue;
                }
            }
    
            public override string Source
            {
    
                set
                {
                    string newSource = null;
    
                    if (_notifier.SetProperty(ref newSource, value))
                        base.Source = newSource;
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged
            {
                add { _notifier.PropertyChanged += value; }
                remove { _notifier.PropertyChanged -= value; }
            }
        }
    

    Создам консольное приложение, где как не в консоли проверять ПредупреждаюНедвижимостьИзменился
    data = new ProxyData(new Data());
    (data as INotifyPropertyChanged).PropertyChanged += (s, e) => { Console.WriteLine(string.Format("Property {0} changed!", e.PropertyName)); };
                 
    
    data.Value = 10;
    data.Source = "List";
    

    Плюсы: чистейший класс Data, все просто и понятно, можно создать кучу разных прокси logger, access и тд, скомбинировать, безопасно удалять и добавлять разный функционал не относящийся к непосредственной работе приложения.
    Минусы: Бесполезно, это подмена понятий вьюмодель я практически превратил в модель, получив еще 1 звено шаблона Model -> ViewModel -> Proxy -> View, одно из назначений ViewModel — уведомление, конечно в vm можно оставить подготовку данных… Плюс ко всему кода стало еще больше, хотя вроде как ответственность vm снизилась, ох solid SOLID.
    Истерия! Пришло время АОП, про аоп написано довольно много и на теории я останавливаться не буду.
    я работаю с IUnityContainer, т.к. его можно считать коробочным, хорошо взаимодействует с Prism.
    А вот и универсальное поведение уведомлятора, жуть
    class NotifyPropertyChangedBehavior : IInterceptionBehavior, INotifyPropertyChanged
        {
            static readonly MethodBase _add;
            static readonly MethodBase _remove;
    
            static NotifyPropertyChangedBehavior()
            {
                var methods = typeof(INotifyPropertyChanged).GetMethods();
                _add = methods[0];
                _remove = methods[1];
            }
    
            public IEnumerable<Type> GetRequiredInterfaces()
            {
                return Type.EmptyTypes;
            }
    
            public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
            {
                IMethodReturn result = null;
                if (IsPropertyChanged(input))
                    if (SubscribeUnsubscribe(input))
                        result = input.CreateMethodReturn(null);
                    else
                    {
                        PropertyInfo property;
    
                        if (IsSetMethodCalled(out property, input))
                            result = SetValue(property, input, getNext);
                    } 
                return result ?? getNext()(input, getNext);
            }
    
            public bool WillExecute
            {
                get { return true; }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// Проверка вызова мутатора
            /// </summary>
            /// <returns></returns>
            bool IsSetMethodCalled(out PropertyInfo property, IMethodInvocation input)
            {
                string propertyName = input.MethodBase.Name.TrimStart("set_".ToArray());
                property = input.Target.GetType().GetProperty(propertyName);
    
                return property != null;
            }
    
            /// <summary>
            /// Установить 
            /// </summary>
            /// <returns></returns>
            IMethodReturn SetValue(PropertyInfo property, IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
            {
                var oldValue = property.GetValue(input.Target, new object[0]);
                var newValue = input.Arguments[0];
                IMethodReturn result = null;
                //обновление только если пришло действительно новое значение
                if (!Equals(oldValue, newValue))
                {
                    result = getNext()(input, getNext);
                    if (PropertyChanged != null)
                        PropertyChanged(input.Target, new PropertyChangedEventArgs(property.Name));
                }
                else result = input.CreateMethodReturn(null);            
    
                return result;
            }
    
            /// <summary>
            /// Проверка вызова методов INotifyPropertyChanged
            /// </summary>
            bool SubscribeUnsubscribe(IMethodInvocation input)
            {
                if (input.MethodBase == _add)
                {
                    PropertyChanged += (PropertyChangedEventHandler)input.Arguments[0];
                    return true;
                }
                else if (input.MethodBase == _remove)
                {
                    PropertyChanged -= (PropertyChangedEventHandler)input.Arguments[0];
                    return true;
                }
                return false;
            }
    
            /// <summary>
            /// Вызов на экземпляре реализующим INotifyPropertyChanged
            /// </summary>
            bool IsPropertyChanged(IMethodInvocation input) { return input.Target is INotifyPropertyChanged; }
        }
    

    ну и кусок кода, где все это связывается
    IUnityContainer container = new UnityContainer();
    
    container.AddNewExtension<Interception>();
    
    container.RegisterType<Data>(new Interceptor<VirtualMethodInterceptor>(),
                    new InterceptionBehavior<NotifyPropertyChangedBehavior>()
                    , new AdditionalInterface<INotifyPropertyChanged>());
    var data = container.Resolve<Data>();
    
    (data as INotifyPropertyChanged).PropertyChanged += (s, e) => { Console.WriteLine(string.Format("Property {0} changed!", e.PropertyName)); };
    
                 
    
    data.Value = 10;
    data.Source = "List";
    data.Value = 10;
    
    Console.ReadKey();
    

    Result ----->
    image
    плюшка для особо ленивых
    public static class Extensions
        {
            public static IUnityContainer RegisterViewModel<T>(this IUnityContainer container) where T : class
            {
                container.AddNewExtension<Interception>();
    
                return container.RegisterType<T>(new Interceptor<VirtualMethodInterceptor>(),
                    new InterceptionBehavior<NotifyPropertyChangedBehavior>()
                    , new AdditionalInterface<INotifyPropertyChanged>());
            }
        }
    

    минимизация регистрации
    container.RegisterViewModel<Data>();
    

    Нельзя оставлять иллюзий, в основе все тот же ЗАМЕСТИТЕЛЬ, только мы о нем не знаем ТсссССсссссс.
    Плюсы: теперь можно сделать VM из всего у чего есть виртуальные свойства, полная минимизация кода, чистый класс без ПредупреждаюНедвижимостьИзменился, всяких атрибутов и тому подобных техномагий.
    Минусы: совершенно не просто, нужно разбираться в контейнере и его возможностях аоп, не явность в чистом виде, воспринимаемая как магия, куча рефликсии — это не для слабонервных, просев по производительности.
    Проведя небольшой эксперимент на сотрудниках, результат плачевный, программисты испытывали боль когда я попросил разобраться как это работает.
    Но оно работает.
    В целом внедрять это не стали и чуть не придали праведному огню, аоп это круто, но все зависит от уровня команды и их стремлению к заморочкам.

    P.S. Всем кого пугает эта жесть рекомендую OnPropertyChanged(nameof(Property)) — оптимально по всем показателям, кроме того что это нужно прописывать РУКАМИ. Бу!
    Поделиться публикацией

    Комментарии 7

      +2
      1) Прекращайте истереть и используйте Fody/PropertyChanged. Минус — это AOP (например, не поставить брэкпоинт, хотя это нужно крайне редко). Плюс — автоматические вычислимые свойства.

      2) Для борьбы с чистотой кода можно вместо каждого нотифицируемого свойства использовать Prism/ObservableObject или runceel/ReactiveProperty. Биндиться придется к *.Value, c поддержкой вычислимых свойств тоже придется повозится, но зато на каждое свойство уникальное событие — не надо работать со строками.

      3) Мы в одном проекте использовали шаблоны Visual Studio T4. Можно написать шаблон, который по основному классу будет генерировать во время написания кода partial-составляющую с нужными уведомлениями и прочим. Очень гибкое решение, можно много чего так реализовать. Из минусов — привязка к конкретной студии разработки.
        0
        Истерики нет, меня вполне устраивает класические OnPropertyChanged через expression,
        например, не поставить брэкпоинт
        при написании IInterceptionBehavior это не проблема.
        У ObservableObject интересная наследственность System.Windows.FrameworkElement<-Microsoft.Practices.Prism.ObservableObject + странный биндинг, не уверен что это чистый код. Автоген это конечно круто, а чем вынос дыма в partial лучше наследования или композиции?
        0
        Я в простых случаях пользуюсь https://www.nuget.org/packages/KindOfMagic/ и забыл об этой проблеме (чуть подробнее — https://kindofmagic.codeplex.com/)
          0
          [Magic] — он самый
          1) KindOfMagic build task runs just after compilation.
          не использовал, но наслышан, а на сколько замедляет сборку приложения?
            0
            Практически незаметно. Рекомендую попробовать
          0
          В своё время, делали так:
          public static Boolean Set(this INotifyPropertyChangedEx o, ref T property, T value, [CallerMemberName] String propertyName = null)
          {
          if (Equals(property, value))
          {
          return false;
          }

          property = value;
          o.NotifyOfPropertyChange(propertyName);

          return true;
          }

          использовали:
          public bool IsLoading
          {
          get { return _isLoading; }
          set { this.Set(ref _isLoading, value); }
          }

          Вкупе со сниппетом довольно удобно. Плохой момент в том, что INotifyPropertyChangedEx из пакета Caliburn.Micro (но несложнен в реализации)
            0
            видимо аналог SetProperty(ref storage, value)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое