Introduction
This article was motivated by a recent attempt I made to create a control that allows you to choose a TimeSpan
.
This article is aimed at beginners and quickly runs through a few of the common aspects of WPF. This article will construct a UserControl
from basic WPF controls.
WPF (4.0) lacks a numeric up/down control (see this question on StackOverflow
for example) so we will use sliders.
As usual, please download the source code to see what is going on in more detail.
Some Basics
In any XAML, I add a name to the root element, called root
of course:
<UserControl x:Class="Btl.Controls.ShortTimeSpanControl" x:Name="root" >
This allows us to easily refer to properties on the root element in question when we are data binding.
Also, I make further use of the data-binding nature of WPF so that I do not have to create unnecessary events in my XAML, and wire them into my code-behind,
hence my UserControl
implements INotifyPropertyChanged
.
The Control: ShortTimeSpanControl
For my requirements, I am only interested in creating a positive TimeSpan
from 00:00:00 to 23:59:59. Since, for reasons known only unto Microsoft,
a numeric up/down, or spinner, was omitted from the default WPF Toolkit, and users are generally error prone, the easiest way to control numbers is to use a Slider
.
I implement the properties Seconds
, Minutes
, and Hours
as int
s, and raise PropertyChanged
if their value is changed.
This then allows me to bind each slider to the Seconds
/Minutes
/Hours
properties. We will revisit these in a moment.
Using DependencyProperty
We want to allow the consumer of a ShortTimeSpanControl
to just use a property that returns a TimeSpan
. So whilst we have implemented
the Seconds
/Minutes
/Hours
properties, we want to implement a DependencyProperty
that allows consumers of the control
to bind to it. This is achieved by registering the property called Value
:
public partial class ShortTimeSpanControl : UserControl, INotifyPropertyChanged
{
public TimeSpan Value
{
get { return (TimeSpan)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(TimeSpan), typeof(ShortTimeSpanControl),
new FrameworkPropertyMetadata(TimeSpan.Zero, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged));
}
It is worth noting that Value
is a very common property name, but for this example it is sufficient, and hopefully obvious.
We do several things here that are worth explaining:
- We use the standard convention of suffixing the property with
Property
.
- We register the property with a default value (
TimeSpan.Zero
).
- The
Value
property is trivially defined; we do not add anything into the setter: if the property is set in XAML,
WPF calls SetValue
directly, and any custom code in your setter is not called.
- We state that our property binds two-way by default.
The last point is worth re-iterating: in most simple examples that you may find on the web, you will probably see this:
private static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(TimeSpan), typeof(ShortTimeSpanControl),
new PropertyMetadata(TimeSpan.Zero, OnValueChanged));
By default, that means the binding is one-way, i.e., if a consumer binds to your DependencyProperty
, they have to add Mode=TwoWay
to their binding/XAML markup. If anyone knows why this is the default, please leave a comment below, as I find it a little strange that all the properties on default WPF controls are two-way.
Putting It All Together
Taking a look at one of the three sliders:
<Slider Name="SecondsSlider"
Grid.Row="2"
IsDirectionReversed="True"
LargeChange="5"
Maximum="59"
SmallChange="1"
Value="{Binding ElementName=root, Path=Seconds}" />
We bind the Slider
to the Seconds
property on our root
element, i.e., our UserControl
code-behind:
private int _seconds;
public int Seconds
{
get
{
return _seconds;
}
set
{
if (value == _seconds)
return;
_seconds = value;
RaisePropertyChanged("Seconds");
var v = Value;
Value = new TimeSpan(v.Hours, v.Minutes, _seconds);
}
}
private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
ShortTimeSpanControl control = obj as ShortTimeSpanControl;
var newValue = (TimeSpan)e.NewValue;
control.Seconds = newValue.Seconds;
control.Minutes = newValue.Minutes;
control.Hours = newValue.Hours;
}
Since we now have two ways of updating the control: either by setting the Value
, or by setting Seconds
, Minutes
, Hours
,
we need to ensure that the UserControl
displays the correct values. By using _seconds
as a backing store for the property,
we use the setter guard to prevent us from getting into a loop: if Value
gets set, we then set the Seconds
property within
the OnValueChanged
callback. Once _seconds
is set, we check to ensure we don't set it again, and then set the Value
again.
Finally, we add the control to our main project window, and wire that up using data-binding and INotifyPropertyChanged
to demonstrate the control.
The image shows the UserControl
in grey, and the data-bound value in the MainWindow
in white.
Final Words
The next step for this control would be to swap out the sliders for numeric up/down/spinner controls, and to make it easy to theme.
If you found this helpful, please leave a comment below. If you didn't find this helpful, or found a mistake, also, please leave a comment below saying why!