Click here to Skip to main content
Click here to Skip to main content

A WPF Short TimeSpan Custom Control

By , 19 Jan 2012
 

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Barry Lapthorn
United Kingdom United Kingdom
Member
Jack of all trades.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 4memberR0n1n0831 Jan '12 - 1:32 
GeneralRe: My vote of 4protectorBarry Lapthorn31 Jan '12 - 2:16 
GeneralMy vote of 3memberMember 308248723 Jan '12 - 21:09 
GeneralRe: My vote of 3protectorBarry Lapthorn31 Jan '12 - 2:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 19 Jan 2012
Article Copyright 2012 by Barry Lapthorn
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid