При работе с WPF/Silverlight, периодически приходится создавать кастомные DependencyProperty, в основном при создании контролов. Стандартный подход объявления и работы с ними не идеальный и имеет минусы, о которых будет сказано ниже. Соответственно, появилась идея упростить запись регистрации и работы с DependencyProperty.
Для начала приведу стандартный код объявления DependencyProperty:
Недостатки данного подхода:
Код улучшенного варианта:
В данном варианте метод Register принимает свойство в виде выражения, значение по умолчанию конкретного типа и не статический колбек. Метод колбека принимает экземпляр дженерик класса DependencyPropertyChangedEventArgs, где старое и новое значения свойства приведены к типу свойства. Сама запись также упростилась. Далее приведу код классов позволяющих применить такую запись.
Код кастомного дженерик класса DependecyProperty:
Данный класс в качестве дженерик параметра принимает тип DependencyObject`а и содержит несколько перегруженных методов Register. Метод Register достает из выражения свойства его имя, конвертирует колбек и создает DependencyProperty стандартным методом.
Код класса DependecyPropertyChangedEventArgs:
Код дополнительного класса ExpressionExtensions, который используется для получения имени свойства по выражению:
Для начала приведу стандартный код объявления 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"); } }
