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'.
1 Answer 1
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
};
}
Explore related questions
See similar questions with these tags.