Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A WPF Short TimeSpan Custom Control

0.00/5 (No votes)
19 Jan 2012 1  
A WPF short TimeSpan custom control using a spinner custom control.

timespan control

Introduction

In my previous two articles, I first described a simple short TimeSpan UserControl that used sliders to pick the hours, minutes, and seconds of a TimeSpan. In my second article, I described a custom spinner control that could be used to replace the sliders.

In this third article, I will update the TimeSpan control to use the SpinnerControl, and make the TimeSpan control a custom control, rather than a UserControl (in particular so that we can apply custom themes to it). I refer to this as a ShortTimeSpanControl as it only represents a positive TimeSpan from 00:00:00 to 23:59:59. A full TimeSpan is described here:

A TimeSpan value can be represented as [-]d.hh:mm:ss.ff, where the optional minus sign indicates a negative time interval, the d component is days, hh is hours as measured on a 24-hour clock, mm is minutes, ss is seconds, and ff is fractions of a second. That is, a time interval consists of a positive or negative number of days without a time of day, or a number of days with a time of day, or only a time of day.

With the generic theme supplied, this is a selection control, where the user can choose a TimeSpan. As it is a custom control, you can apply any custom theme you like, and, perhaps, just make it a read-only control (interactively, that is) that updates its Value via data-binding.

Requirements

We would like the control to look something like this:

timespan control with generic theme

where we have three spinner controls for 0..23 hours, 0..59 minutes, and 0..59 seconds.

The control should:

  • Return and accept a TimeSpan Value.
  • Keep the TimeSpan bounded.
  • Raise an event when Value changes.

Data Binding and Not Repeating Yourself

We need to consider how to get the values from the SpinnerControl to update the value in the ShortTimeSpanControl and vice-versa. In the spinner control in the previous article, we set:

private static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof(decimal), typeof(SpinnerControl),
    new FrameworkPropertyMetadata(DefaultValue,
        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
        OnValueChanged,
        CoerceValue
        ));

such that the Value property binds two-way by default.

On our ShortTimeSpanControl, we create the following dependency properties with two-way binding: Hours, Minutes, and Seconds. Then by simply using XAML data-binding, we can bind the three SpinnerControls to the respective properties that they represent, and the framework takes care of the binding and update notifications for us.

However, this means that not only do we have a TimeSpan representing the underlying value of the control, but we also have a copy of the hours, minutes, and seconds, which is a little bit awkward as there isn't a single point of truth for the TimeSpan value that we are holding.

However, as long as we keep all the data in sync, we will not have a problem. To do this, we just need to check that if we set any property to a new value, that it doesn't match the existing value, and if it does, then do not call the setter on the other properties.

For example: when Value changes, we need to update the Hours, Minutes, and Seconds properties:

private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
    var control = d as ShortTimeSpanControl;
    if (d == null)
        return;

    var oldValue = (TimeSpan)args.OldValue;
    var newValue = (TimeSpan)args.NewValue;

    //  ensure we don't get into a loop with the 4 properties changing
    //  by only changing the value if it has changed. 

    if (oldValue != newValue)
    {
        control.SetValue(HoursProperty, (object)newValue.Hours);
        control.SetValue(MinutesProperty, (object)newValue.Minutes);
        control.SetValue(SecondsProperty, (object)newValue.Seconds);
    }

    var e = new RoutedPropertyChangedEventArgs<TimeSpan>(oldValue, newValue, ValueChangedEvent);

    control.OnValueChanged(e);
}

Databinding and CoerceValueCallback with the Spinner Control

Whilst I was adding my spinner control to the time span control generic theme, I discovered some odd behaviour: the problem was that my Value dependency property in the timespan control was over-running its bounds, but only during the XAML databinding to the spinner control. My decrease command in the spinner control was:

protected void OnDecrease()
{
    Value -= Change;
}

which causes the over/under-run, and the CoerceValueCallback that should have prevented this was:

private static decimal LimitValueByBounds(decimal newValue, SpinnerControl control)
{
    newValue = Math.Max(control.Minimum, Math.Min(control.Maximum, newValue));
    //  then ensure the number of decimal places is correct.
    newValue = Decimal.Round(newValue, control.DecimalPlaces);
    return newValue;
}

private static object CoerceValue(DependencyObject obj, object value)
{
    decimal newValue = (decimal)value;
    SpinnerControl control = obj as SpinnerControl;

    if (control != null)
    {
        //  ensure that the value stays within the bounds of the minimum and
        //  maximum values that we define.
        newValue = LimitValueByBounds(newValue, control);
    }

    return newValue;
}

It seems that the data-binding update occurs first, performed by the WPF framework (when it calls SetValue 'implicitly' on the dependency property), allowing the under-run, and then the CoerceValueCallback is called after the data-binding update. It turns out that this has been covered in numerous places on the web, e.g., here, but this item on the MS Connect site describes the problem, and the reasoning behind the behaviour.

I tried swapping out the SpinnerControl with a Slider and found that the Slider behaves as expected, implying that the problem is not really something to do with WPF, but rather how we approach the problem. The solution in my case is to also limit the bounds during the OnDecrease and OnIncrease commands that were causing the issue:

protected void OnDecrease()
{
    Value = LimitValueByBounds(Value - Change, this);
}

By adding this trivial fix, we prevent the Value from ever going below our minimum.

Events

As we have four properties, and each can change, it makes sense to create four corresponding events:

timespan control

These are just typical boilerplate custom control event implementations.

Conclusion

There is not a lot else to say about this control. It is really just a case of creating four DependencyProperty fields, and four corresponding events. All the validation work is delegated to the underlying SpinnerControls. The only extra work that we have to perform is that since we are storing the TimeSpan in two places (Value, and Hours, Minutes, Seconds), we ensure that the data is kept up to date equally in all the properties.

As mentioned at the beginning, this control is only supposed to represent a short and well defined TimeSpan from 00:00:00 to 23:59:59. This control could of course be extended to represent a full TimeSpan.

Please leave any comments or suggestions below, and don't forget to vote up the article if it is helpful. Thanks!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here