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

Behaviors to create double or TimeSpan Value TextBox for WPF Masked TextBox

0.00/5 (No votes)
26 Oct 2016 6  
Presents a concept for creating a masked TextBox, and has the implementation for a TimeSpan and double value

Introduction

I have used and application for a long time that has a TextBox that use used to specify a time on a music track, and have been very unhappy with it. I finally got around to seeing what I could implement. I work primarily in WPF for I figured that I should do it in this technology. I could implement it as a control, but decided to do it as a behavior. This does have the advantage in that all the styling for TextBox controls would work for this control, and could possibly be used in other ways. I have recently added a control for double values.

Notes

This is work in progress, and will change with time to add additional features that are not in the initial version. In particular I plan to handle pasting, but that is not in this version. There have been some improvements in the TimeSpan behavior. The double behavior is a cleaner design. It is probably the easier template to use for creating new behaviors.

The DoubleTextBoxBehavior has a format DependencyProperty that should be set. It is only somewhat similar to the format strings that are used elsewhere in Visual Studio and this is on purpose. Currently the right most period or comma is used for the decimal point. Any zero character will be used for the number regardless of location, and the right most plus or minus (dash) will be used for the sign. Any other charaters will be displayed as in the format string, which gives a lot of flexibility. If a plus sign is used that positive numbers will have a sign, otherwise only negative numbers. Using the minus key at any position will make the number positive, and the plus key will make it positive. Using the up and down arrow keys when the cursor is on the sigh position will change from positive to negative. However, if the specified minimum values is above zero or the maximum value is below zero, sign will not change.  And if sign is changed then the number will be immediately adjusted to remain within the specified minimum and maximum. 

Design

The design has a single class, but the class has two very different parts implemented as the static and instance code. Quite often with behaviors I will only implement the static part and use only static event handlers. I felt in this case it would be better to separate the code.

The static part is as follows:

                public static readonly DependencyProperty MaxTimeProperty =
                        DependencyProperty.RegisterAttached("MaxTime", typeof(string),
                                typeof(TimeSpanTextBoxBehaviour), new UIPropertyMetadata(null, OnMaxTimeChanged));

                public static string GetMaxTime(Control o)
                {
                        return (string)o.GetValue(MaxTimeProperty);
                }

                public static void SetMaxTime(Control o, string value)
                {
                        o.SetValue(MaxTimeProperty, value);
                }

                private static void OnMaxTimeChanged(DependencyObject dependencyObject,
                        DependencyPropertyChangedEventArgs e)
                {
                        var timeTextBoxBehaviour = GetTimeTextBoxBehaviour(dependencyObject);
                        var timeString = (string)e.NewValue;
                        var timeSpan = TimeSpanParse(timeString, false);
                        timeTextBoxBehaviour.MaxTimeSpanChanged(timeSpan);
                }

                public static readonly DependencyProperty ValueProperty =
                        DependencyProperty.RegisterAttached("Value", typeof(TimeSpan),
                                typeof(TimeSpanTextBoxBehaviour), new FrameworkPropertyMetadata(TimeSpan.Zero, 
                                        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged));

                public static TimeSpan GetValue(Control o)
                {
                        return (TimeSpan)o.GetValue(ValueProperty);
                }

                public static void SetValue(Control o, TimeSpan value)
                {
                        o.SetValue(ValueProperty, value);
                }

                public static readonly DependencyProperty TimeFormatProperty =
                        DependencyProperty.RegisterAttached("TimeFormat", typeof(TimerFormats),
                                typeof(TimerFormats), new UIPropertyMetadata(TimerFormats.Seconds10Ths, OnTimeFormatChanged));

                public static TimerFormats GetTimeFormat(Control o)
                {
                        return (TimerFormats)o.GetValue(TimeFormatProperty);
                }

                public static void SetTimeFormat(Control o, TimerFormats timeFormat)
                {
                        o.SetValue(TimeFormatProperty, timeFormat);
                }

                private static void OnTimeFormatChanged(DependencyObject dependencyObject,
                        DependencyPropertyChangedEventArgs e)
                {
                        var timeTextBoxBehaviour = GetTimeTextBoxBehaviour(dependencyObject);
                        timeTextBoxBehaviour.TimeFormatChanged((TimerFormats)e.NewValue);
                }

                public static TimeSpanTextBoxBehaviour GetTimeTextBoxBehaviour(object textBox)
                {
                        var castTextBox = (TextBox)textBox;
                        var control = GetTimeTextBoxBehaviour(castTextBox);
                        if (control == null)
                        {
                                control = new TimeSpanTextBoxBehaviour(castTextBox);
                                SetTimeTextBoxBehaviour(castTextBox, control);
                        }
                        return control;
                }

                private static void OnValueChanged(DependencyObject dependencyObject,
                        DependencyPropertyChangedEventArgs e)
                {
                        var timeTextBoxBehaviour = GetTimeTextBoxBehaviour(dependencyObject);
                        var timeSpan = (TimeSpan)e.NewValue;
                        timeTextBoxBehaviour.UpdateText(timeSpan);
                }

                /// <summary>
                /// This is the private DependencyProperty where the instance to handle the time behaviour is kept
                /// </summary>
                private static readonly DependencyProperty TimeTextBoxBehaviourProperty =
                        DependencyProperty.RegisterAttached("TimeTextBoxBehaviour", typeof(TimeSpanTextBoxBehaviour),
                                typeof(TimeSpanTextBoxBehaviour), new UIPropertyMetadata(null));

                private static TimeSpanTextBoxBehaviour GetTimeTextBoxBehaviour(TextBox o)
                {
                        return (TimeSpanTextBoxBehaviour)o.GetValue(TimeTextBoxBehaviourProperty);
                }

                private static void SetTimeTextBoxBehaviour(TextBox o, TimeSpanTextBoxBehaviour value)
                {
                        o.SetValue(TimeTextBoxBehaviourProperty, value);
                }
                #endregion

Image 1

Modification

The timer is a more complex case. If the data to provide the format for is something like a Social Security Number, it could be a lot simpler. As long as the "-"'s should be in the saved social security number, the ValueProperty would not be required. 

Input Errors

I have not done anything about reporting errors to the user, in particular is an invalid key is pressed. Only the number keys and a few other keys are supported, and the code just ignores the input. Quite often a beep is programmed on invalid input, but this can be objectionable to some users. I have thought of possibly coming up with a an interface to allow binding to a command that will handle these input issues, Interested in feedback.

History

  • 2016/10/26: Initial version
  • 2016/11/03: Fix of some issues with arrow key up down behavior and cursor selection, and added time format flexibility.
  • 2016/11/04: Updated behavior to allow a larger digit than normally allowed if all positions left of the digit are zeroes (i.e., if you have "0:00:00" and enter a 9 in the 10's of seconds then the value will be "0:01:30:00".
  • 2016/12/13: Fix small bug when enter a too large value in the first position, and also support entering a too large value in the first position and that value is moved to the first allowed position.
  • 2017/09/15: Fixed Value DependencyProperty so defaults to Mode=TwoWay. Also fix for when cursor is at end of text.
  • 2020/07/25: Serious error in the original version that would not bind from the ViewModel property. This means that no feedback. Also the changes to make it two way were not in the DependencyProperty definition. Changed the example to bind through a ViewModel instead of direclty.
  • 2020/07/26: Problem found caused by rounding error fixed when incrementing with 10ths of seconds, and probably caused issues anytime there were fractons of a second being displayed.
  • 2020/08/07: Improved TimeSpan behavior, and also included a behavior for double values.
  • 2020/08/09: Fix a small bug that prevented using arrow key to move to right most position.
  • 2020/08/15: Bug fix

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