Richard Lemmens website

A WPF time picker control

Windows Presentation Foundation (WPF) comes with a lot of prefab controls. Among many, it includes a date picker, but for some reason a time picker is missing. Time to make our own. Below is an example.
You can enter hours, minutes and seconds. These are combined into a single dependency property name "Time", which is a nullable System.TimeSpan. The property is read-write. There are also properties for Hour, Minute, Second and DateTime. If you instantiate the control from code rather than from XAML, you can also call one of the parameterized constructors to provide an initial time value.
The control raises a "SelectedTimeChanged" event when its time value changes.
There is also a button to pick the current time. Note that that button requires an image to be included in your project, which is not included below. Pick your own and integrate it.

            
<UserControl
        x:Class="myNamespace.TimePickerControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:myNamespace"
        Loaded="Control_Loaded">
    <UserControl.Resources>
        <local:NullableIntStringConverter x:Key="nullableIntStringConverter"/>
    </UserControl.Resources>
    <UserControl.CommandBindings>
        <CommandBinding Command="local:TimePickerCommands.Now" Executed="Now_Executed"/>
    </UserControl.CommandBindings>
    <Grid DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}">
        <Grid.RowDefinitions>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" MinWidth="24"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="1*" MinWidth="24"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="1*" MinWidth="24"/>
            <ColumnDefinition Width="3"/>
            <ColumnDefinition Width="24"/>
        </Grid.ColumnDefinitions>
        <TextBox Grid.Row="0" Grid.Column="0" Name="txtHours">
            <TextBox.Text>
                <Binding Path="Hour" Mode="TwoWay" Converter="{StaticResource ResourceKey=nullableIntStringConverter}">
                    <Binding.ValidationRules>
                        <local:IntegerRangeValidationRule Min="0" Max="23"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock Grid.Row="0" Grid.Column="1" Text=":" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBox Grid.Row="0" Grid.Column="2" Name="txtMinutes">
            <TextBox.Text>
                <Binding Path="Minute" Mode="TwoWay" Converter="{StaticResource ResourceKey=nullableIntStringConverter}">
                    <Binding.ValidationRules>
                        <local:IntegerRangeValidationRule Min="0" Max="59"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock Grid.Row="0" Grid.Column="3" Text=":" VerticalAlignment="Center" HorizontalAlignment="Center"/>
        <TextBox Grid.Row="0" Grid.Column="4" Name="txtSeconds">
            <TextBox.Text>
                <Binding Path="Second" Mode="TwoWay" Converter="{StaticResource ResourceKey=nullableIntStringConverter}">
                    <Binding.ValidationRules>
                        <local:IntegerRangeValidationRule Min="0" Max="59"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <Button Grid.Row="0" Grid.Column="6" ToolTip="Zet op nu" Command="local:TimePickerCommands.Now">
            <Image Source="pack://application:,,,/MyLibrary;component/images/clock.png" Height="20" Width="20"/>
        </Button>
    </Grid>
</UserControl>
            
        
            
namespace myNamespace
{
    /// <summary>
    /// Interaction logic for TimePickerControl.xaml
    /// </summary>
    public partial class TimePickerControl : System.Windows.Controls.UserControl, System.ComponentModel.INotifyPropertyChanged
    {
        /// <summary>
        /// Occurs when the value of a property used in databinding changes.
        /// </summary>
        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Occurs when the selected time value changes.
        /// </summary>
        public event System.EventHandler SelectedTimeChanged;

        /// <summary>
        /// Dependency property for the Time property.
        /// </summary>
        public static System.Windows.DependencyProperty TimeProperty = null;

        /// <summary>
        /// Sets static property values of the TimePickerControl class.
        /// </summary>
        static TimePickerControl()
        {
            System.Windows.PropertyChangedCallback timeChanged = new System.Windows.PropertyChangedCallback(OnTimechanged);
            System.Windows.PropertyMetadata timeMetadata = new System.Windows.PropertyMetadata(timeChanged);
            TimeProperty = System.Windows.DependencyProperty.Register("Time", typeof(System.TimeSpan?), typeof(TimePickerControl), timeMetadata);
        }

        /// <summary>
        /// Minimum date that SQL Server 2005 accepts.
        /// </summary>
        private static System.DateTime minDateSQLServer = new System.DateTime(1753, 1, 1);

        /// <summary>
        /// Creates a new instance of the TimePickerControl class
        /// with default value.
        /// </summary>
        public TimePickerControl()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Initializes time properties.
        /// </summary>
        /// <param name="sender">The source of the event</param>
        /// <param name="e">Object that contains the event data</param>
        private void Control_Loaded(object sender, System.Windows.RoutedEventArgs e)
        {
            OnPropertyChanged("Hour");
            OnPropertyChanged("Minute");
            OnPropertyChanged("Second");
        }

        /// <summary>
        /// Creates a new instance of the TimePickerControl class
        /// with given timepsan value.
        /// </summary>
        /// <param name="timespan">Time, as TimeSpan</param>
        public TimePickerControl(System.TimeSpan? timespan)
        {
            SetValue(TimeProperty, timespan);
            InitializeComponent();
        }

        /// <summary>
        /// Creates a new instance of the TimePickerControl class
        /// with given datetime value.
        /// </summary>
        /// <param name="datetime">Time, as DateTime, where the date part is ignored</param>
        public TimePickerControl(System.DateTime? datetime)
        {
            if (datetime == null)
            {
                SetValue(TimeProperty, null);
            }
            else
            {
                SetValue(TimeProperty, datetime.Value.TimeOfDay);
            }
            InitializeComponent();
        }

        /// <summary>
        /// Gets or sets the time, as a TimeSpan struct.
        /// </summary>
        public System.TimeSpan? Time
        {
            get { return (System.TimeSpan?) GetValue(TimeProperty); }
            set
            {
                SetValue(TimeProperty, value);
                OnPropertyChanged("Hour");
                OnPropertyChanged("Minute");
                OnPropertyChanged("Second");
            }
        }

        /// <summary>
        /// Gets or sets the time, as a DateTime struct.
        /// </summary>
        public System.DateTime? DateTime
        {
            get
            {
                System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                if (time == null)
                {
                    return null;
                }
                else
                {
                    return minDateSQLServer + time.Value;
                }
            }
            set
            {
                if (value == null)
                {
                    SetValue(TimeProperty, null);
                }
                else
                {
                    SetValue(TimeProperty, value.Value.TimeOfDay);
                }
                OnPropertyChanged("Hour");
                OnPropertyChanged("Minute");
                OnPropertyChanged("Second");
            }
        }

        /// <summary>
        /// Gets or sets the hour part of the time.
        /// </summary>
        public int? Hour
        {
            get
            {
                System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                if (time == null)
                {
                    return null;
                }
                else
                {
                    return time.Value.Hours;
                }
            }
            set
            {
                if (value == null)
                {
                    SetValue(TimeProperty, null);
                }
                else
                {
                    System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                    if (time == null)
                    {
                        time = new System.TimeSpan();
                    }
                    SetValue(TimeProperty, new System.TimeSpan(value.Value, time.Value.Minutes, time.Value.Seconds));
                }
                this.OnPropertyChanged("Hour");
            }
        }

        /// <summary>
        /// Gets or sets the minute part of the time.
        /// </summary>
        public int? Minute
        {
            get
            {
                System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                if (time == null)
                {
                    return null;
                }
                else
                {
                    return time.Value.Minutes;
                }
            }
            set
            {
                if (value == null)
                {
                    SetValue(TimeProperty, null);
                }
                else
                {
                    System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                    if (time == null)
                    {
                        time = new System.TimeSpan();
                    }
                    SetValue(TimeProperty, new System.TimeSpan(time.Value.Hours, value.Value, time.Value.Seconds));
                }
                this.OnPropertyChanged("Minute");
            }
        }

        /// <summary>
        /// Gets or sets the second part of the time.
        /// </summary>
        public int? Second
        {
            get
            {
                System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                if (time == null)
                {
                    return null;
                }
                else
                {
                    return time.Value.Seconds;
                }
            }
            set
            {
                if (value == null)
                {
                    SetValue(TimeProperty, null);
                }
                else
                {
                    System.TimeSpan? time = (System.TimeSpan?) GetValue(TimeProperty);
                    if (time == null)
                    {
                        time = new System.TimeSpan();
                    }
                    SetValue(TimeProperty, new System.TimeSpan(time.Value.Hours, time.Value.Minutes, value.Value));
                }
                this.OnPropertyChanged("Second");
            }
        }

        /// <summary>
        /// Signals the GUI controls that underlying data has been changed.
        /// </summary>
        /// <param name="propName">Name of property of which the value has changed</param>
        private void OnPropertyChanged(string propName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propName));
            }
        }

        /// <summary>
        /// Triggers PropertyChanged events to update the GUI
        /// and raises the SelectedTimechanged event to signal other objects of time value change.
        /// </summary>
        /// <param name="target">The source of the event</param>
        /// <param name="e">Object that contains the event data</param>
        private static void OnTimechanged(System.Windows.DependencyObject target, System.Windows.DependencyPropertyChangedEventArgs e)
        {
            TimePickerControl timePicker = (TimePickerControl) target;
            timePicker.OnPropertyChanged("Hour");
            timePicker.OnPropertyChanged("Minute");
            timePicker.OnPropertyChanged("Second");
            if (timePicker.SelectedTimeChanged != null)
            {
                timePicker.SelectedTimeChanged(target, new System.EventArgs());
            }
        }

        /// <summary>
        /// Updates the time to the current time.
        /// </summary>
        /// <param name="sender">The source of the event</param>
        /// <param name="e">Object that contains the event data</param>
        private void Now_Executed(object sender, System.Windows.RoutedEventArgs e)
        {
            SetValue(TimeProperty, System.DateTime.Now.TimeOfDay);
            OnPropertyChanged("Hour");
            OnPropertyChanged("Minute");
            OnPropertyChanged("Second");
        }
    }

    /// <summary>
    /// Container for usercontrol-specific commands.
    /// </summary>
    public static class TimePickerCommands
    {
        /// <summary>
        /// Initializes the TimePickerCommands class.
        /// </summary>
        static TimePickerCommands()
        {
            Now =
                new System.Windows.Input.RoutedUICommand("Nu", "now", typeof(TimePickerCommands));
        }

        /// <summary>
        /// Command now.
        /// </summary>
        public static System.Windows.Input.RoutedUICommand Now
        {
            get;
            private set;
        }
    }

    /// <summary>
    /// ValueConvertor class that converts a nullable integer to a string and back.
    /// </summary>
    /// <remarks>
    /// Useful for databinding string value properties to nullable integer properties.
    /// </remarks>
    [System.Windows.Data.ValueConversion(typeof(string), typeof(int?))]
    public class NullableIntStringConverter : System.Windows.Data.IValueConverter
    {
        /// <summary>
        /// Converts a nullable integer to a string.
        /// </summary>
        /// <param name="value">The value produced by the binding source</param>
        /// <param name="targetType">The type of the binding target property</param>
        /// <param name="parameter">The converter parameter to use</param>
        /// <param name="culture">The culture to use in the converter</param>
        /// <returns>Converted value</returns>
        public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null && !(value is int?))
            {
                throw new System.InvalidOperationException("The source must be a nullable integer");
            }
            if (targetType != typeof(string))
            {
                throw new System.InvalidOperationException("The target must be a string");
            }
            if (((int?) value).HasValue)
            {
                return System.Convert.ToString((int?) value);
            }
            else
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// Converts a string to a nullable integer.
        /// </summary>
        /// <param name="value">The value produced by the binding source</param>
        /// <param name="targetType">The type of the binding target property</param>
        /// <param name="parameter">The converter parameter to use</param>
        /// <param name="culture">The culture to use in the converter</param>
        /// <returns>Converted value</returns>
        public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (!(value is string))
            {
                throw new System.InvalidOperationException("The source must be a string");
            }
            if (targetType != typeof(int?))
            {
                throw new System.InvalidOperationException("The target must be a nullable integer");
            }
            if (string.IsNullOrEmpty((string) value))
            {
                return null;
            }
            else
            {
                return System.Convert.ToInt32((string) value);
            }
        }
    }

    /// <summary>
    /// Validation rule that checks if a given value is an integer within a given range.
    /// Also accepts nulls and empty strings.
    /// </summary>
    public class IntegerRangeValidationRule : System.Windows.Controls.ValidationRule
    {
        private int min;
        private int max;

        /// <summary>
        /// Gest or sets the bottom value for the range.
        /// </summary>
        public int Min
        {
            get { return this.min; }
            set { this.min = value; }
        }

        /// <summary>
        /// Gest or sets the top value for the range.
        /// </summary>
        public int Max
        {
            get { return this.max; }
            set { this.max = value; }
        }

        /// <summary>
        /// Validates a given value against the configured range.
        /// </summary>
        /// <param name="value">Value to validate</param>
        /// <param name="cultureInfo">Culture object</param>
        /// <returns>True if the value is valid; otherwise false</returns>
        /// <remarks>
        /// Alternative: Use System.Convert.ToInt32() instead of int.TryParse()
        /// </remarks>
        public override System.Windows.Controls.ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if (!(value is string))
            {
                return new System.Windows.Controls.ValidationResult(false, string.Format("{0} is not a string", value));
            }
            if (!string.IsNullOrEmpty((string) value))
            {
                int number;
                if (!int.TryParse((string) value, out number))
                {
                    return new System.Windows.Controls.ValidationResult(false, string.Format("{0} is not a number", value));
                }
                if (number < this.min || number > this.max)
                {
                    return new System.Windows.Controls.ValidationResult(false, string.Format("Enter a value between {0} and {1}", this.min, this.max));
                }
            }
            return System.Windows.Controls.ValidationResult.ValidResult;
        }
    }
}