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 notFrameworkPropertyMetadata
- 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'.