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

в 8:00, , рубрики: .net, silverlight, wpf, метки: , , ,

При работе с 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");
    }
}

Автор: shev986

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js