Typed dependency properties in WPF

 
27 mei 2011

Microsoft heeft met de DependencyProperties een mooi concept bedacht, maar de syntax mag wel een stuk verbeterd worden. In deze post laat ik een alternatief zien.

Introductie

Om een DependencyProperty te registeren voor een DependencyObject, is er de volgende syntax bedacht:

    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.Register(“IsValid”, typeof(bool),
            typeof(MyObject), new PropertyMetadata(false,
            OnIsValidChanged));

    public void OnIsValidChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs args)
    {
        var myObject = (MyObject) d;
        …
    }

Waarom Microsoft nog dit soort syntax aanbiedt in de 21e eeuw, is voor mij een groot raadsel. De naam van de property is een string (dus wordt niet meegenomen in de refactoring), het type van de property moet worden vermeld (terwijl dit gewoon te achterhalen is dmv reflectie), de callback geeft een DependencyObject door (maar we weten precies welk type dat zou moeten zijn), het is niet in een keer duidelijk dat ‘false’ de default waarde voorstelt.

Dat moet toch beter kunnen:

    public static readonly DependencyProperty IsValidProperty =
        TypedDependencyProperty<MyObject>
            .For(x => x.IsValid)
            .Default(false)
            .Callback(OnIsValidChanged)
        ;

    public void OnIsValidChanged(MyObject d,
        DependencyPropertyChangedEventArgs args)
    {
        …
    }

Het WPF-framework vereist dat de DependencyProperties op de Microsoft-manier worden geregistreerd, dus hoeveel lagen er ook omheen worden gebouwd, uiteindelijk zal de volgende code uitgevoerd moeten worden:

    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.Register(propertyName, propertyType,
        ownerType, new PropertyMetadata(defaultValue, callback))

of, in het geval van attached properties:

    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.RegisterAttached(propertyName,
            propertyType, ownerType,
            new PropertyMetadata(defaultValue, callback))

Het plan is om deze variabelen op een ontwikkelaar-vriendelijke manier te gaan bepalen.

OwnerType bepalen

De DependencyProperties moeten static worden gedeclareerd. Hierdoor kan de ownerType niet worden achterhaald zonder dat expliciet aan te geven. Er is geen alternatief dan het meegeven van de ownerType als generic parameter:

    TypedDependencyProperty<T>

    DependencyProperties mogen alleen maar aan een DependencyObject worden gekoppeld, dus er kan een constraint gelegd worden op type T:

    TypedDependencyProperty<T> where T : DependencyObject

PropertyType en PropertyName

Het zou slordig zijn om het volgende te kunnen doen met deze API:

    TypedDependencyProperty<MyObject>
        .For(x => x.IsValid)
        .Default(DateTime.Now)
    ;

De default moet van hetzelfde type zijn als de return type van de expressie:

    public class TypedDependencyProperty<T, R>
    {
        public TypedDependencyProperty<T, R>
            Default(R defaultValue)
        {
        }
    }

waarbij R het return-type is van de expressie. R kunnen we bepalen nadat het attribuut ‘For’ wordt gebruikt. Het is daarom logisch dat TypedDependencyProperty<T> alleen de methode ‘For’ heeft, met als returntype TypedDependencyProperty<T, R>, zodat ‘For’ de eerste en enige methode is die aangeroepen kan worden voordat andere properties worden geset.

Voor de Java-programmeurs onder ons

In tegenstelling tot Java, worden de generic types niet door de .NET-compiler uit de uiteindelijke code gehaald. Daarom is TypedDependencyProperty<T> een ander type dan TypedDependencyProperty<T, R>.

    public class TypedDependencyProperty<T>
        where T : DependencyObject
    {
        public static TypedDependencyProperty<T, R>
            For<R>(Expression<Func<T, R>> expression)
        {
            return new TypedDependencyProperty<T, R>
                (expression);
        }
    }

Type R wordt door de compiler afgeleid door te kijken naar de lambda-expressie en de ontwikkelaar hoeft dit dan niet expliciet aan te geven.

In TypedDependencyProperty<T, R> zijn al 3 van de 6 parameters te achterhalen. OwnerType is typeof(T), propertyType is typeof(R) en propertyName kan worden achterhaald door de expressie te parsen.

    public class TypedDependencyProperty<T, R>
        where T : DependencyObject
    {
        private Type OwnerType { get; set; }
        private Type PropertyType { get; set; }
        private string PropertyName { get; set; }

        public TypedDependencyProperty(
            Expression<Func<T, R>> expression)
        {
            OwnerType = typeof(T);
            PropertyType = typeof(R);
            PropertyName = expression.GetPropertyName();
        }
    }

Parsen van de lamda-expressie

Expression.GetPropertyName() bestaat niet, maar daar kunnen extension methods voor worden gebruikt (helaas ondersteunt .NET extension properties (nog) niet).

    public static class ExpressionExtensions
    {
        public static string GetPropertyName(
            Expression expression)
        {
            MemberExpression member;

            switch (expression.NodeType)
            {
                case ExpressionType.Lambda:
                    var lamdaExpression = (LambdaExpression)expression;
                    return lamdaExpression.Body.GetPropertyName();

                case ExpressionType.MemberAccess:
                    member = (MemberExpression)expression;
                    break;

                default:
                    throw new NotSupportedException(string.Format(“NodeType ‘{0}’ wordt niet ondersteund”, expression.NodeType));
            }

            var propertyInfo = member.Member as PropertyInfo;
            if (propertyInfo == null) throw new NotSupportedException(string.Format(“‘{0}’ is geen property. DependencyProperties moeten properties zijn.”, memberInfo.Name));

            return propertyInfo.Name;
        }
    }

Als we te maken hebben met de lamda-expressie zelf (de statement x => x.IsValid), dan vragen we de body op (x.IsValid) en proberen het opnieuw. Het enige ondersteunde alternatief is een expressie is dat naar een property-member van het type verwijst.

Default waarde

De method om default waarde aan te geven is niet moelijk.

    public class TypedDependencyProperty<T, R>
        where T : DependencyObject
    {
        private R DefaultValue { get; set; }

        public TypedDependencyProperty<T, R>
            Default(R value)
        {
            DefaultValue = value;

            return this;
        }
    }

Attached en unattached properties

Er is in WPF een verschil tussen een attached en een unattached property. Deze wordt d.m.v. DependencyProperty.Register of DependencyProperty.RegisterAttached aangegeven. De methode-signatuur is hetzelfde (string, Type, Type, PropertyMetadata). TypedDependencyProperty levert standaard een unattached property op, tenzij het expliciet is aangeven door de Attached() methode.

    public class TypedDependencyProperty<T, R>
        where T : DependencyObject
    {
        private Func<string, Type, Type, PropertyMetadata, DependencyProperty> Register { get; set; }

        public TypedDependencyProperty(
            Expression<Func<T, R>> expression)
        {
            …
            Register = DependencyProperty.Register;
        }

        public TypedDependencyProperty<T, R> Attached()
        {
            Register =
                DependencyProperty.RegisterAttached;

            return this;
        }
    }

De callback

Het laatste is de callback. Deze callback is niets anders dan een getypeerde wrapper om de System.Windows.PropertyChangedCallback.

    public delegate void TypedPropertyChangedCallback<in T>
        (T d, DependencyPropertyChangedEventArgs e)
        where T : DependencyObject;

    public class TypedDependencyProperty<T, R>
        where T : DependencyObject
    {
        private PropertyChangedCallback PropertyChangedCallback { get; set; }

        public TypedDependencyProperty<T, R> Callback(TypedPropertyChangedCallback<T> callback)
        {
            PropertyChangedCallback =
                (d, e) => callback((T)d, e);

            return this;

        }
    }

Omzetten naar een DependencyProperty

Het enige dat nog resteert is het omzetten van een TypedDependencyProperty<T, R> naar een System.Windows.DependencyProperty.

    public class TypedDependencyProperty<T, R>
        where T : DependencyObject
    {
        public static implicit operator DependencyProperty(
            TypedDependencyProperty<T, R> property)
        {
            return property.Register(
                property.PropertyName,
                property.PropertyType,
                property.OwnerType,
                new PropertyMetadata(property.DefaultValue,
                    property.PropertyChangedCallback)
            );
        }
    }

Resultaat

Orginele WPF-methode:

    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.RegisterAttached(
            “IsValid”,
            typeof(bool),
            typeof(MyObject),
            new PropertyMetadata(false, OnIsValidChanged)
        );

    public void OnIsValidChanged(DependencyObject d,
        DependencyPropertyChangedEventArgs args)
    {
        var myObject = (MyObject) d;
        …
    }

De TypedDependencyProperty-methode:

    public static readonly DependencyProperty IsValidProperty =
        TypedDependencyProperty<MyObject>.For(x => x.IsValid)
            .Default(false)
            .Attached()
            .Callback(OnIsValidChanged)
        ;

    public void OnIsValidChanged(MyObject d,
        DependencyPropertyChangedEventArgs args)
    {
        …
    }

De code

En dat allemaal door het onderstaande stuk code.

    public delegate void TypedPropertyChangedCallback<T>(T d, DependencyPropertyChangedEventArgs e) where T : DependencyObject;

    public class TypedDependencyProperty<T> where T : DependencyObject
    {
        public static TypedDependencyProperty<T, R> For<R>(Expression<Func<T, R>> expression)
        {
            return new TypedDependencyProperty<T, R>(expression);
        }
    }

    public class TypedDependencyProperty<T, R> where T : DependencyObject
    {
        private Type OwnerType { get; set; }
        private Type PropertyType { get; set; }
        private string PropertyName { get; set; }
        private R DefaultValue { get; set; }
        private Func<string, Type, Type, PropertyMetadata, DependencyProperty> Register { get; set; }
        private PropertyChangedCallback PropertyChangedCallback { get; set; }

        protected internal TypedDependencyProperty(Expression<Func<T, R>> expression)
        {
            OwnerType = typeof(T);
            PropertyType = typeof(R);
            PropertyName = expression.GetPropertyName();
            Register = DependencyProperty.Register;
        }

        public TypedDependencyProperty<T, R> Default(R value)
        {
            DefaultValue = value;

            return this;
        }

        public TypedDependencyProperty<T, R> Attached()
        {
            Register = DependencyProperty.RegisterAttached;

            return this;
        }

        public TypedDependencyProperty<T, R> Callback(TypedPropertyChangedCallback<T> callback)
        {
            PropertyChangedCallback = (d, e) => callback((T)d, e);

            return this;
        }

        public static implicit operator DependencyProperty(TypedDependencyProperty<T, R> property)
        {
            return property.Register(property.PropertyName, property.PropertyType, property.OwnerType, new PropertyMetadata(property.DefaultValue, property.PropertyChangedCallback));
        }
    }

    internal static class ExpressionExtensions
    {
        public static string GetPropertyName(this Expression expression)
        {
            MemberExpression member;

            switch (expression.NodeType)
            {
                case ExpressionType.Lambda:
                    var lamdaExpression = (LambdaExpression)expression;
                    return lamdaExpression.Body.GetPropertyName();

                case ExpressionType.MemberAccess:
                    member = (MemberExpression)expression;
                    break;

                default:
                    throw new NotSupportedException(string.Format(“NodeType ‘{0}’ wordt niet ondersteund”, expression.NodeType));

            }

            var propertyInfo = member.Member as PropertyInfo;
            if (propertyInfo == null) throw new NotSupportedException(string.Format(“‘{0}’ is geen property. DependencyProperties moeten properties zijn.”, member.Member.Name));

            return propertyInfo.Name;
        }
    }

Microsoft heeft WPF in de System namespace gezet, dus het lijkt erop dat zij vinden dat WPF de nieuwe standaard moet gaan worden voor het maken van user interfaces in .NET. Dan lijkt mij deze wrapper een handige toevoeging in de Sogyo-toolbox.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development, .Net

Tags: ,


Reactie

  • Paul schreef:

    Dat ziet er goed uit Jeroen. Vooral de fluent API maakt het gebruik mooi. Nu de propdp code snippet voor ons project hier even op aanpassen :)

    Geplaatst op 05 juni 2011 om 9:21 Permalink