4
\$\begingroup\$

This is my second attempt to create a strongly typed builder for the Dependency Property. I've improved the two main weaknesses of the previous version which were:

  • Can specify only PropertyMatadata but not FrameworkPropertyMetadata
  • Can coerce value but not cancel the operation with DependencyProperty.UnsetValue

The PropertyMatadata can now be created with its own builder.

class PropertyMetadataBuilder<T, TValue> where T : DependencyObject
{
    private readonly PropertyMetadata _propertyMetadata;

    internal PropertyMetadataBuilder()
    {
        _propertyMetadata = new PropertyMetadata();
    }

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

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

    public PropertyMetadataBuilder<T, TValue> PropertyChanging(
        Action<T, PropertyChangingEventArgs<TValue>> coerceValueCallback
    )
    {
        _propertyMetadata.CoerceValueCallback = new CoerceValueCallback(
            (d, baseValue) =>
            {
                var e = new PropertyChangingEventArgs<TValue>((TValue)baseValue);
                coerceValueCallback((T)d, e);
                return
                    e.Canceled
                    ? DependencyProperty.UnsetValue
                    : e.CoercedValue;
            }
        );
        return this;
    }

    public static implicit operator PropertyMetadata(
        PropertyMetadataBuilder<T, TValue> builder
    )
    {
        return builder._propertyMetadata;
    }
}

I initialize it via an extension for the DependencyPropertyBuilder.

static class DependencyPropertyBuilderExtensions
{
    public static DependencyPropertyBuilder<T, TValue> PropertyMetadata<T, TValue>(
        this DependencyPropertyBuilder<T, TValue> builder,
        Action<PropertyMetadataBuilder<T, TValue>> build
    ) where T : DependencyObject
    {
        var metadataBuilder = new PropertyMetadataBuilder<T, TValue>();
        build(metadataBuilder);
        return builder.Metadata(metadataBuilder);
    }
}

Another extension can be added to support the creation of a FrameworkPropertyMetadata.


The DependencyPropertyBuilder has lost the PropertyMetadata related APIs and got instead one for setting metadata:

public DependencyPropertyBuilder<T, TValue> Metadata(
    PropertyMetadata propertyMetadata
)
{
    _propertyMetadata = propertyMetadata;
    return this;
}

The missing option DependencyProperty.UnsetValue to cancel CoerceValue is now available via the PropertyChangingEventArgs.

This should unify the the property value changing/changed behavior. The CoerceValue API is a weird one. It actually handles the property-changing event but in a completely unconventional way.

class PropertyChangingEventArgs<TValue> : EventArgs
{
    internal PropertyChangingEventArgs(TValue baseValue)
    {
        NewValue = baseValue;
        CoercedValue = baseValue;
    }
    public TValue NewValue { get; }
    public TValue CoercedValue { get; set; }
    public bool Canceled { get; set; }
}

This does not require to use the UnsetValue directly as it is determined by the Canceled property:

public PropertyMetadataBuilder<T, TValue> PropertyChanging(
    Action<T, PropertyChangingEventArgs<TValue>> coerceValueCallback
)
{
    _propertyMetadata.CoerceValueCallback = new CoerceValueCallback(
        (d, baseValue) =>
        {
            var e = new PropertyChangingEventArgs<TValue>((TValue)baseValue);
            coerceValueCallback((T)d, e);
            return
                e.Canceled
                ? DependencyProperty.UnsetValue
                : e.CoercedValue;
        }
    );
    return this;
}

Example

The new implementation can be used like this

class TestObject : DependencyObject
{
    public static readonly DependencyProperty CountProperty =
        DependencyPropertyBuilder
        .Register<TestObject, int>(nameof(TestObject.Count))
        .PropertyMetadata(b => b
            .DefaultValue(5)
            .PropertyChanged((testObject, e) =>
            {
                Console.WriteLine($"{e.Property.Name} = {e.OldValue} --> {e.NewValue}");
            })
            .PropertyChanging((testObject, e) =>
            {
                if (e.NewValue > 20)
                {
                    e.CoercedValue = 15;
                }

                if (e.NewValue < 1)
                {
                    e.Canceled = true;
                }
            })
        ).ValidateValue(value => value >= 0);

    public int Count
    {
        get { return CountProperty.GetValue<int>(this); }
        set { CountProperty.SetValue(this, value); }
    }
}

Changing Count value:

var testObject = new TestObject();
testObject.Count.Dump("Default");

testObject.Count2 = 8;
testObject.Count2.Dump("Changed");

testObject.Count = 22;
testObject.Count.Dump("Coerced to max");

testObject.Count = 0;
testObject.Count.Dump("Property change canceled");

testObject.Count = -2; // bam!

Output

Default    
5 
Count = 5 --> 8    

Changed    
8 
Count = 8 --> 15

Coerced to max    
15 

Property change canceled    
15 

ArgumentException
'-2' is not a valid value for property 'Count'. 
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

I have combined this builder with the declarative one.


Now the Build resolves the DefaultValue and Validation attributes on the property:

public DependencyProperty Build()
{
    BuildDefaultValue();
    BuildValidateValueCallback();

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

private void BuildDefaultValue()
{
    var property = _ownerType.GetProperty(_name);

    // Use the default value specified by the user or try to use the attribute.
    _propertyMetadata.DefaultValue =
        _propertyMetadata.DefaultValue ??
        new Func<object>(() =>
            // Get the defualt value from the attribute...
            property.GetCustomAttribute<DefaultValueAttribute>()?.Value ??
            // or use the default value for the type.
            (property.PropertyType.IsValueType
                ? Activator.CreateInstance(property.PropertyType)
                : null
            )
        )();
}

private void BuildValidateValueCallback()
{
    var property = _ownerType.GetProperty(_name);

    // Use the callback specified by the user or try to use the attributes.
    _validateValueCallback =
        _validateValueCallback ??
        (value =>
            new Func<bool>(() => (
                property.GetCustomAttributes<ValidationAttribute>() ??
                Enumerable.Empty<ValidationAttribute>()
            ).All(x => x.IsValid(value)))()
        );
}

I changed the PropertyChanging API back to CoerceValue. The main one works like the WPF implementation:

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

and I extended it via an extension to work with EventArgs.

public static PropertyMetadataBuilder<T, TValue> CoerceValue<T, TValue>(
    this PropertyMetadataBuilder<T, TValue> builder,
    Action<T, CoerceValueEventArgs<TValue>> coerceValueCallback
) where T : DependencyObject
{
    builder.CoerceValue((d, baseValue) =>
    {
        var e = new CoerceValueEventArgs<TValue>(baseValue);
        coerceValueCallback(d, e);
        return
            e.Canceled
                ? DependencyProperty.UnsetValue
                : e.CoercedValue;
    });
    return builder;
}

The custom EventArgs

public class CoerceValueEventArgs<TValue> : EventArgs
{
    internal CoerceValueEventArgs(TValue baseValue)
    {
        NewValue = baseValue;
        CoercedValue = baseValue;
    }
    public TValue NewValue { get; }
    public TValue CoercedValue { get; set; }
    public bool Canceled { get; set; }
}

This is the new TestObject

internal class TestObject : DependencyObject
{
    public static readonly DependencyProperty CountProperty =
        DependencyPropertyBuilder
        .Register<TestObject, int>(nameof(Count))
        .PropertyMetadata(b => b
            .PropertyChanged((testObject, e) =>
            {
                Console.WriteLine($"{e.Property.Name} = {e.OldValue} --> {e.NewValue}");
            })            
            .CoerceValue((testObject, e) =>
            {
                if (e.NewValue > 20)
                {
                    e.CoercedValue = 15;
                }

                if (e.NewValue < 1)
                {
                    e.Canceled = true;
                }
            })
        );

    [DefaultValue(5)]
    [Range(0, 15)]
    public int Count
    {
        get { return CountProperty.GetValue<int>(this); }
        set { CountProperty.SetValue(this, value); }
    }
}

and the tests

[TestMethod]
public void Count_DefaultValue()
{
    var testObject = new TestObject();
    Assert.AreEqual(5, testObject.Count, "Default value.");
}

[TestMethod]
public void Count_ChangeValue()
{
    var testObject = new TestObject
    {
        Count = 8
    };
    Assert.AreEqual(8, testObject.Count, "Changed value");
}

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Count_ValueOutOfRange()
{
    new TestObject
    {
        Count = 22
    };
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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