Упрощение регистрации и работы с DependencyProperty

    При работе с WPF/Silverlight, периодически приходится создавать кастомные DependencyProperty, в основном при создании контролов. Стандартный подход объявления и работы с ними не идеальный и имеет минусы, о которых будет сказано ниже. Соответственно, появилась идея упростить запись регистрации и работы с DependencyProperty.

    Для начала приведу стандартный код объявления DependencyProperty:
    public class SomeDependecyObject : DependencyObject
    {
        public static readonly DependencyProperty IntValueProperty =
            DependencyProperty.Register("IntValue", typeof(int), typeof(SomeDependecyObject), new UIPropertyMetadata(1, OnIntValuePropertyChanged));
    
        public int IntValue
        {
            get { return (int)GetValue(IntValueProperty); }
            set { SetValue(IntValueProperty, value); }
        }
    
        private static void OnIntValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            int newPropertyValue = (int)e.NewValue;
            SomeDependecyObject instance = (SomeDependecyObject)d;
            // Perform callback action.
        }
    }
    

    Недостатки данного подхода:
    • Указание имени свойства в виде строки. При переименовании свойства нужно не забыть переименовать строковое значение.
    • Статический callback. Для доступа к членам класса нужно параметр d приводить к типу класса. Новое и старое значения свойства также не приведенные к типу свойства.
    • На уровне компиляции нет проверки типа свойства и значения по умолчанию. Параметр propertyType метода Register может принять любой Type. Параметр defaultValue является объектом.

    Код улучшенного варианта:
    public class SomeDependecyObject : DependencyObject
    {
        public static readonly DependencyProperty IntValueProperty =
            DependencyProperty<SomeDependecyObject>.Register(x => x.IntValue, 1, x => x.OnIntValuePropertyChanged);
    
        public int IntValue
        {
            get { return (int)GetValue(IntValueProperty); }
            set { SetValue(IntValueProperty, value); }
        }
    
        private void OnIntValuePropertyChanged(DependencyPropertyChangedEventArgs<int> e)
        {
        }
    }
    

    В данном варианте метод Register принимает свойство в виде выражения, значение по умолчанию конкретного типа и не статический колбек. Метод колбека принимает экземпляр дженерик класса DependencyPropertyChangedEventArgs, где старое и новое значения свойства приведены к типу свойства. Сама запись также упростилась. Далее приведу код классов позволяющих применить такую запись.

    Код кастомного дженерик класса DependecyProperty:
    public static class DependencyProperty<T> where T : DependencyObject
    {
        public static DependencyProperty Register<TProperty>(Expression<Func<T, TProperty>> propertyExpression)
        {
            return Register<TProperty>(propertyExpression, default(TProperty), null);
        }
    
        public static DependencyProperty Register<TProperty>(Expression<Func<T, TProperty>> propertyExpression, TProperty defaultValue)
        {
            return Register<TProperty>(propertyExpression, defaultValue, null);
        }
    
        public static DependencyProperty Register<TProperty>(Expression<Func<T, TProperty>> propertyExpression, Func<T, PropertyChangedCallback<TProperty>> propertyChangedCallbackFunc)
        {
            return Register<TProperty>(propertyExpression, default(TProperty), propertyChangedCallbackFunc);
        }
    
        public static DependencyProperty Register<TProperty>(Expression<Func<T, TProperty>> propertyExpression, TProperty defaultValue, Func<T, PropertyChangedCallback<TProperty>> propertyChangedCallbackFunc)
        {
            string propertyName = propertyExpression.RetrieveMemberName();
            PropertyChangedCallback callback = ConvertCallback(propertyChangedCallbackFunc);
    
            return DependencyProperty.Register(propertyName, typeof(TProperty), typeof(T), new PropertyMetadata(defaultValue, callback));
        }
    
        private static PropertyChangedCallback ConvertCallback<TProperty>(Func<T, PropertyChangedCallback<TProperty>> propertyChangedCallbackFunc)
        {
            if (propertyChangedCallbackFunc == null)
                return null;
            return new PropertyChangedCallback((d, e) =>
            {
                PropertyChangedCallback<TProperty> callback = propertyChangedCallbackFunc((T)d);
                if (callback != null)
                    callback(new DependencyPropertyChangedEventArgs<TProperty>(e));
            });
        }
    }
    
    public delegate void PropertyChangedCallback<TProperty>(DependencyPropertyChangedEventArgs<TProperty> e);
    

    Данный класс в качестве дженерик параметра принимает тип DependencyObject`а и содержит несколько перегруженных методов Register. Метод Register достает из выражения свойства его имя, конвертирует колбек и создает DependencyProperty стандартным методом.

    Код класса DependecyPropertyChangedEventArgs:
    public class DependencyPropertyChangedEventArgs<T> : EventArgs
    {
        public DependencyPropertyChangedEventArgs(DependencyPropertyChangedEventArgs e)
        {
            NewValue = (T)e.NewValue;
            OldValue = (T)e.OldValue;
            Property = e.Property;
        }
    
        public T NewValue { get; private set; }
        public T OldValue { get; private set; }
        public DependencyProperty Property { get; private set; }
    }
    

    Код дополнительного класса ExpressionExtensions, который используется для получения имени свойства по выражению:
    public static class ExpressionExtensions
    {
        public static string RetrieveMemberName<TArg, TRes>(this Expression<Func<TArg, TRes>> propertyExpression)
        {
            MemberExpression memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression == null)
            {
                UnaryExpression unaryExpression = propertyExpression.Body as UnaryExpression;
                if (unaryExpression != null)
                    memberExpression = unaryExpression.Operand as MemberExpression;
            }
            if (memberExpression != null)
            {
                ParameterExpression parameterExpression = memberExpression.Expression as ParameterExpression;
                if (parameterExpression != null && parameterExpression.Name == propertyExpression.Parameters[0].Name)
                    return memberExpression.Member.Name;
            }
            throw new ArgumentException("Invalid expression.", "propertyExpression");
        }
    }
    

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Класс! Всё очевидное — просто! Добавлю в свой список маленьких, но полезных расширений.
      • НЛО прилетело и опубликовало эту надпись здесь
          –1
          — Лябда-выражения, передача функций через параметр и прочие уменьшают скорость работы в разы.
          Какая скорость? Этот код выполняется один раз при старте программы!

          — Проверка типа свойства и значения по-умолчанию легко решаются с помощью snippet'ов и расширений типа ReSharper.
          А если, спустя время, нужно будет свойство переименовать? Да и переименовывать будет другой человек — ведь можно и забыть поменять названия ВЕЗДЕ — тут сниппеты не помогут.
            +1
            Какая скорость? Этот код выполняется один раз при старте программы!

            Т.е. скорость запуска приложения это для вас не показатель? Видал я приложение из 10 простых бизнес-сущностей с парой экранов. Приложение запускалось >1,5 минут, при том что винда сейчас запускается около 2.

            А если, спустя время, нужно будет свойство переименовать? Да и переименовывать будет другой человек — ведь можно и забыть поменять названия ВЕЗДЕ — тут сниппеты не помогут.

            Скажите, как часто вам и в каком объеме приходится переименовывать свойства? У меня такое бывает от силы 1 раз за весь проект, и то чтобы исправить синтаксическую ошибку. Если для вас норма постоянно все переименовывать — это уже ошибка дизайна. Дальше скажу больше, что обычно на DP вешается биндинг в разметке. Так вот все равно никакой решарпер и никакие сниппеты вам не помогут внести изменения в XAML, все равно придется руками искать и менять.
              –2
              Ну, если у девелоперов руки из *опы растут, то так оно и будет. Даю гарантию, что они при старте синхронно единичными запросами пол базы данных вытягивали. Если бизнес-приложение тупит, то практически в 100% случаев в этом виновато кривое обращение к БД.
                +1
                Засеките время, которое тратиться на регистрацию всех свойств для DO. Уверяю вас, это очень долго. Не стоит нагружать данные операции еще и лямбда выражениями.
                Программисты майкрасофта тоже не муд*ки, они тоже могли придумать такой сниппет, но однако этого не сделали по каким то соображениям.
                  –1
                  ну начнем с того, что засечь это время крайне сложно. не уверен, что даже профайлер сможет по-человечески его посчитать (хотя, по-идее должно получиться). но это неважно, потому что чаще всего причиной тормозов являются операции ввода/вывода и особенно, если эти операции выполняются по сети и еще особенней, если в потоке интерфейса (чтоб пользователь совсем офигел от тормозов). Даю руку на отсечение, что такие хаки с получением имени свойства сыграют ничтожно малую роль в процессе регистрации DP.
                0

                Какая скорость? Этот код выполняется один раз при старте программы!


                Т.е. скорость запуска приложения это для вас не показатель? Видал я приложение из 10 простых бизнес-сущностей с парой экранов. Приложение запускалось >1,5 минут, при том что винда сейчас запускается около 2.

                Это же сколько нужно таких свойств наклепать, чтобы задержка составила хотя-бы полсекунды?


                Скажите, как часто вам и в каком объеме приходится переименовывать свойства? У меня такое бывает от силы 1 раз за весь проект, и то чтобы исправить синтаксическую ошибку. Если для вас норма постоянно все переименовывать — это уже ошибка дизайна. Дальше скажу больше, что обычно на DP вешается биндинг в разметке. Так вот все равно никакой решарпер и никакие сниппеты вам не помогут внести изменения в XAML, все равно придется руками искать и менять.

                Не часто, но как-то сложился уже стиль программирования — если можно сделать строгую типизацию, то нужно её делать. Везде где можно. Магические числа и строки, не являющиеся текстовыми сообщениями, меня в коде немного напрягают. Для большинства бизнес-приложений микросекунды роли не играют, а вот понятность кода и возможность его статического анализа — очень даже.
                0
                Этот код выполняется один раз при старте программы!

                Даже не при старте, а при первом обращении к классу содержащему данное свойство. К примеру, при первом создании кастомного контрола. Не считаю это значимой потерей производительности.
                  0
                  Ну, я не стал заострять внимание на этом моменте — важно, что код выполняется один раз.
                  Хотя, за счёт того, что код выполняется при первом обращении к классу — эти, и без того мизерные, задержки будут ещё и размазаны во времени.
                0
                А откуда такие утверждения?

                Лябда-выражения

                Примерно 0.3 миллисекунды на единичный вызов с передачей Expression(понятно что внутри там дерево строится). Около 300 миллисекунд на миллион вызовов. Ну думаю понятно, почему разница не линейная.
                передача функций через параметр

                0,001 миллисекунда на один вызов функции с передачей функции как параметр. 0,6 миллисекунды на миллион вызовов. Если говорить о колле делегата, то при вызове чего-то типа ()=>1 и аналогичной статической функции разница будет 5 и 0.3 миллисекунды соотственно на опять же на миллион вызовов. И то я подозреваю во втором случае просто инлайн отрабатывает. Если функция будет делать что-то серьезно скорее всего разница практически исчезнет.

                Чтобы действительно уменьшить производительность в разы используя лямбда-выражения и передачу функций как параметры надо ой как постараться.
                0
                Шаблон на ReSharper'e сводит все проблемы к нажатию одной комбинации клавиш и вписывании названия DependencyProperty один раз.
                  0
                  Стандартный снипет позволяет вводить все части по 1 разу. Чуть модифицировав это снипет можно еще и сократить количество ввода.
                    0
                    Скажите, в этой строчке
                    private void OnIntValuePropertyChanged(DependencyPropertyChangedEventArgs<int> e)
                        {
                        }
                    

                    у вас ошибка и пропущено слово static?
                    А если нет, то объясните мне, как вы от статик свойств и статик callback перешли к нестатик, и в каком тогда экземпляре объекта будет выполнятся этот код?
                      0
                      Не туда, извините
                    0
                    Скажите, в этой строчке
                    private void OnIntValuePropertyChanged(DependencyPropertyChangedEventArgs<int> e)
                        {
                        }
                    
                    

                    у вас ошибка и пропущено слово static?
                    А если нет, то объясните мне, как вы от статик свойств и статик callback перешли к нестатик, и в каком тогда экземпляре объекта будет выполнятся этот код?
                      0
                      Вот в этом месте static превращается в нестатик:
                      private static PropertyChangedCallback ConvertCallback<TProperty>(Func<T, PropertyChangedCallback<TProperty>> propertyChangedCallbackFunc)
                      

                      и выполняется он в контексте того объекта в который в статик callback-е передается в первом параметре
                      public delegate object CoerceValueCallback(DependencyObject d, object baseValue);
                      


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

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