I'm writing a real NumericUpDown/Spinner
control as an exercise to learn custom control authoring. I've got most of the behavior that I'm looking for, including appropriate coercion. One of my tests has revealed a flaw, however.
My control has 3 dependency properties: Value
, MaximumValue
, and MinimumValue
. I use coercion to ensure that Value
remains between the min and max, inclusive. E.g.:
// In NumericUpDown.cs
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(NumericUpDown),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal, HandleValueChanged, HandleCoerceValue));
[Localizability(LocalizationCategory.Text)]
public int Value
{
get { return (int)this.GetValue(ValueProperty); }
set { this.SetCurrentValue(ValueProperty, value); }
}
private static object HandleCoerceValue(DependencyObject d, object baseValue)
{
NumericUpDown o = (NumericUpDown)d;
var v = (int)baseValue;
if (v < o.MinimumValue) v = o.MinimumValue;
if (v > o.MaximumValue) v = o.MaximumValue;
return v;
}
My test is just to ensure that data binding works how I expect. I created a default wpf windows application and threw in the following xaml:
<Window x:Class="WpfApplication.MainWindow" x:Name="This"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nud="clr-namespace:WpfCustomControlLibrary;assembly=WpfCustomControlLibrary"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<nud:NumericUpDown Value="{Binding ElementName=This, Path=NumberValue}"/>
<TextBox Grid.Row="1" Text="{Binding ElementName=This, Path=NumberValue, Mode=OneWay}" />
</Grid>
</Window>
with very simple codebehind:
public partial class MainWindow : Window
{
public int NumberValue
{
get { return (int)GetValue(NumberValueProperty); }
set { SetCurrentValue(NumberValueProperty, value); }
}
// Using a DependencyProperty as the backing store for NumberValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty NumberValueProperty =
DependencyProperty.Register("NumberValue", typeof(int), typeof(MainWindow), new UIPropertyMetadata(0));
public MainWindow()
{
InitializeComponent();
}
}
(I'm omitting the xaml for the control's presentation)
Now if I run this I see the value from the NumericUpDown
reflected appropriately in the textbox, but if I type in a value that's out of range the out of range value gets displayed in the test textbox while the NumericUpDown
shows the correct value.
Is this how coerced values are supposed to act? It's good that it's coerced in the ui, but I expected the coerced value to run through the databinding as well.
When you define a custom XAML dependency property, one of the things you do is specify the default value. You can do this by providing the default value directly: DependencyProperty. Register("MyProperty", propertyType, ownerType, new PropertyMetadata(defaultValue));
The primary difference between a dependency droperty and a standard clr property is that a dependency property can be the target of a binding. This allows you to tie the value of the property to a value provided by some other object.
Default Data-bindingIt just defines which is the default binding mode for the control's property. In WPF different controls has different default data-binding modes. For example, TextBlock's Text property has one-way as default binding mode but a TextBox's Text property has a two-way binding mode.
A dependency property can reference a value through data binding. Data binding works through a specific markup extension syntax in XAML, or the Binding object in code. With data binding, determination of the final property value is deferred until run time, at which time the value is obtained from a data source.
Wow, that is surprising. When you set a value on a dependency property, binding expressions are updated before value coercion runs!
If you look at DependencyObject.SetValueCommon in Reflector, you can see the call to Expression.SetValue halfway through the method. The call to UpdateEffectiveValue that will invoke your CoerceValueCallback is at the very end, after the binding has already been updated.
You can see this on framework classes as well. From a new WPF application, add the following XAML:
<StackPanel>
<Slider Name="Slider" Minimum="10" Maximum="20" Value="{Binding Value,
RelativeSource={RelativeSource AncestorType=Window}}"/>
<Button Click="SetInvalid_Click">Set Invalid</Button>
</StackPanel>
and the following code:
private void SetInvalid_Click(object sender, RoutedEventArgs e)
{
var before = this.Value;
var sliderBefore = Slider.Value;
Slider.Value = -1;
var after = this.Value;
var sliderAfter = Slider.Value;
MessageBox.Show(string.Format("Value changed from {0} to {1}; " +
"Slider changed from {2} to {3}",
before, after, sliderBefore, sliderAfter));
}
public int Value { get; set; }
If you drag the Slider and then click the button, you'll get a message like "Value changed from 11 to -1; Slider changed from 11 to 10".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With