3
\$\begingroup\$

I've been working on another builder, this time for the DependencyProperty because it's object-based. This means that every parameter is an object and thus their usage is inconvenient (requires casting).

In my experiment I tried to make its creation easier and at the same time generic. This is what it looks like.


The core class is the generic DependencyPropertyBuilder. It allows you to set all properties of a DependencyProperty by providing strongly typed methods.

The line breaks in method signatures are to avoid horizontal scrolling.

class DependencyPropertyBuilder<T, TValue> where T : DependencyObject
{
    private readonly string _name;
    private readonly Type _propertyType;
    private readonly Type _ownerType;
    private readonly PropertyMetadata _propertyMetadata;
    private ValidateValueCallback _validateValueCallback;

    public DependencyPropertyBuilder(
        string name, 
        Type propertyType, 
        Type ownerType
    )
    {
        _name = name;
        _propertyType = propertyType;
        _ownerType = ownerType;
        _propertyMetadata = new PropertyMetadata();
    }

    public DependencyPropertyBuilder<T, TValue> ValidateValue(
        Func<TValue, bool> validateValueCallback
    )
    {
        _validateValueCallback = new ValidateValueCallback(
            value => validateValueCallback(
                (TValue)value
            )
        );
        return this;
    }

    public DependencyPropertyBuilder<T, TValue> DefaultValue(
        TValue defaultValue
    )
    {
        _propertyMetadata.DefaultValue = defaultValue;
        return this;
    }

    public DependencyPropertyBuilder<T, TValue> PropertyChanged(
        Action<T, DependencyPropertyChangedEventArgs<TValue>> propertyChangedCallback
    )
    {
        _propertyMetadata.PropertyChangedCallback = new PropertyChangedCallback(
            (sender, e) =>
                propertyChangedCallback((T)sender,
                new DependencyPropertyChangedEventArgs<TValue>(e)
            )
        );
        return this;
    }

    public DependencyPropertyBuilder<T, TValue> CoerceValue(
        Func<T, TValue, TValue> coerceValueCallback
    )
    {
        _propertyMetadata.CoerceValueCallback = new CoerceValueCallback(
            (d, baseValue) =>
                coerceValueCallback((T)d,
                (TValue)baseValue
            )
        );
        return this;
    }

    public DependencyProperty ToDependencyProperty()
    {
        return DependencyProperty.Register(
            _name,
            _propertyType,
            _ownerType,
            _propertyMetadata,
            _validateValueCallback
        );
    }

    public static implicit operator DependencyProperty(
        DependencyPropertyBuilder<T, TValue> builder
    ) 
       => builder.ToDependencyProperty();
}

The core builder is accompanied by another non-generic builder that provides two overloads of the Register method that gather the mandatory information about a property via generics or Expressions.

class DependencyPropertyBuilder
{
    public static DependencyPropertyBuilder<T, TValue> Register<T, TValue>(
        string propertyName
    ) 
    where T : DependencyObject
    {
        var propertyInfo = typeof(T).GetProperty(propertyName);

        return new DependencyPropertyBuilder<T, TValue>(
            name: propertyName,
            propertyType: propertyInfo.PropertyType,
            ownerType: propertyInfo.DeclaringType
        );
    }

    public static DependencyPropertyBuilder<T, TValue> Register<T, TValue>(
        Expression<Func<TValue>> expression
    ) 
    where T : DependencyObject
    {
        var memberExpression = expression.Body as MemberExpression;

        return new DependencyPropertyBuilder<T, TValue>(
            name: memberExpression.Member.Name,
            propertyType: ((PropertyInfo)memberExpression.Member).PropertyType,
            ownerType: ((PropertyInfo)memberExpression.Member).DeclaringType
        );
    }
}

In order to provide strongly typed EventArgs for the PropertyChangedCallback I created a generic wrapper:

struct DependencyPropertyChangedEventArgs<T>
{
    private readonly DependencyPropertyChangedEventArgs _e;
    public DependencyPropertyChangedEventArgs(
        DependencyPropertyChangedEventArgs e
    )
    {
        _e = e;
    }

    public T OldValue => (T)_e.OldValue;

    public T NewValue => (T)_e.NewValue;

    public DependencyProperty Property => _e.Property;
}

I also tried to make the wrapper-property usage more natural and created two more extensions for the getter and setter. They virtually flip the order of operations.

static class DependencyPropertyExtensions
{
    public static T GetValue<T>(
        this DependencyProperty dependencyProperty, 
        DependencyObject dependencyObject
    )
    {
        return (T)dependencyObject.GetValue(dependencyProperty);
    }

    public static void SetValue(
        this DependencyProperty dependencyProperty, 
        DependencyObject dependencyObject, 
        object value
    )
    {
        dependencyObject.SetValue(dependencyProperty, value);
    }
}

Example usage

As Dependency Properties work only with a Dependency Objects I created one for testing that defines such a property:

class TestObject : DependencyObject
{
    public static readonly DependencyProperty CountProperty =
        DependencyPropertyBuilder
        .Register<TestObject, int>(nameof(TestObject.Count))
        // Alternatively with an expression
        // requires #pragma warning disable 1720 
        //.Register<TestObject, int>(() => default(TestObject).Count)
        .DefaultValue(5)
        .PropertyChanged((sender, e) =>
        {
            Console.WriteLine($"{e.Property.Name} = {e.OldValue} --> {e.NewValue}");
        })
        .ValidateValue(value => value < 10);

    // Default usage
    public int Count
    {
        get { return (int)GetValue(CountProperty); }
        set { SetValue(CountProperty, value); }
    }

    // Using DependencyPropertyExtensions
    public int Count2
    {
        get { return CountProperty.GetValue<int>(this); }
        set { CountProperty.SetValue(this, value); }
    }
}

In the Console:

var testObject = new TestObject();
testObject.Count.Dump(); // 5, as this is the default value

testObject.Count2 = 8; // PropertyChanged kicks-in and prints to the Console
testObject.Count2.Dump(); // 8

testObject.Count2 = 12; // throws '12' is not a valid value for property 'Count'.
\$\endgroup\$
1
\$\begingroup\$

If I were to nitpick I would point out a couple of limitations:

  1. You can no longer return DependencyProperty.UnsetValue from CoerceValueCallback since return value now is strongly typed.
  2. You can not set other types of metadata, such as FrameworkPropertyMetadata.
  3. Not exactly a limitation, but I feel like current API adds a lot of boiler-plate code, which is a common problem with builder pattern. I think I would rather write

    .Metadata(5, ChangedCallback, CoerceValueCallback)
    

    instead of

    .DefaultValue(5).PropertyChanged(ChangedCallback).ValidateValue(CoerceValueCallback);
    

    but maybe its just me.

All in all, I think that if you decide to use this builder, you should stick to that decision and use it everywhere. This means that you will have to add support for all possible registration scenarios to your API. Otherwise I feel like its going to be really confusing for a reader to see half the properties registered using builder and half registered without it.

P.S. Otherwise your code looks pretty clean to me.

P.P.S. I would use Count just because it does not require any prior knowledge about your extensions, therefore it is easier to understand for fellow developers (and because I'm too lazy to modify re-sharper templates). However if I were to pretend that I know nothing about WPF, I would say that Count2 looks cleaner.

P.P.P.S. It just occurred to me that the main advantage of your registration lies in strong typed event handlers. After all Register<TestObject> syntax does not add any additional "safety" compared to regular Register(typeof(TestObject)). So what you could do is this. Instead of overwriting entire registration process, you could extend/wrap PropertyMetadata class. I see this as a compromise approach, where you do both: keep the ugly familiar syntax, but add type safety to event handlers, if it is needed.

\$\endgroup\$
2
  • \$\begingroup\$ I like nitpicking ;-) I think the ProperetyMetadata part in the original implementation does not add any value and just confuses. It's all about the property you create anyway so the metadata isn't necessary. I like the point about the UnsetValue and the FrameworkPropertyMetadata and I need to do some research about it. Yes, I plan to use it everywhere because I find the metadata and casting very confusing and and redundant. The purpose of the new Register is to extract all necessary information for registration automatically rather then specifying three parameters manually. \$\endgroup\$
    – t3chb0t
    Dec 27 '16 at 10:31
  • \$\begingroup\$ Ok, I understand my mistakes now. There's no way around a distinction between various PropertyMetadata types which I wasn't aware they existed. The UnsetValue needs its place too. \$\endgroup\$
    – t3chb0t
    Dec 27 '16 at 10:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.