Click here to Skip to main content
15,881,852 members
Articles / Desktop Programming / WPF

TextBoxTemplate for formatted numeric input

Rate me:
Please Sign up or sign in to vote.
4.71/5 (3 votes)
1 Apr 2012CPOL9 min read 34.5K   1.3K   25   3
For latitudes, longitudes, ranges, bearings, times and other formatted numeric input

Summary

This templated <code><code class="western">TextBox control is really handy when you have specialized/formatted numeric input. Use it like a <code><code class="western">TextBox and it will do the necessary formatting and validation driven by a single "template" string.

Introduction

An ocean-navigation application requires the user to input latitudes, longitudes, ranges, bearings, speeds, etc.—all of which have various numeric input constraints. I needed a TextBox with an input template which would do the job for input formats such as:

Type of DataSample of Desired Formatting
Latitude22º33.445'N
Range\12.5 NM
Elapsed Time12:34:56.7
Time of Day10:34:56.7PM (or 22:34:56.7) depending on the user's preference

Internally, all of these values are stored as <code><code class="western">doubles (or time values) and I wanted a standard control to get user input for these types as easily as possible.

Let's look at the Latitude data type. The internal double for latitude is stored as floating-point degrees and has a range from -90.0 to +90.0 degrees with positive values being displayed as “N” (North) and negative values are “S” (South). These are usually shown as degrees and minutes with a decimal fraction. Longitudes are similar but the range is from -180 to +180 and the values are displayed as “E” (East) and “W” (West). The fraction can be a single decimal digit (about 0.1 miles) but three decimal digits are also often displayed (e.g. 22º33.445'N) giving a real-world resolution of about 6 feet.

Bearings are angles to or from a physical object and are in degrees with a range from 0 to 360. A fraction is usually not included because bearings are physically difficult to measure accurately while on a moving boat.

It is usual practice to put the units designation (mi., yds., etc.) in a Label to the right of the input <code><code class="western">TextBox. Latitudes, times, etc., however, have the units (degree and minute marks) interspersed in the display of the value. So for consistency I made the choice to include all the units designations within the <code><code class="western">TextBoxTemplate display for all value types and protecting the units from user input. If I had included the units as <code><code class="western">Labels, a time input control (for example) could consist of three <code><code class="western">TextBoxes for the hours, minutes, and seconds, another (or a <code><code class="western">ComboBox) for the AM/PM designation, and <code><code class="western">Labels for the colons.. This could have been encapsulated in a user control but then I would have to create a separate user control for each data type and input format. I actually started writing this way but changed course after the number of different controls became cumbersome.

One issue which ran in my favor in that nautical navigators (my target users) are used to fixed-format data fields and are not concerned by leading zeroes. This means that all input can always be in “overstrike” mode. The user is presented with a valid input value (00º00.000'N) and can change the values but never needs to enter degree symbols, decimal points, etc.

The control's job is threefold:

  1. To display the value in the format the user prefers (or the program dictates)

  2. To constrain the user to inputting only valid data

  3. To allow use of the same control regardless of the data type and users' display preferences


About the Control

The solution is to have a TextBox which has been extended to include an InputTemplate string which defines the formatting and input constraints. With the embedded formatting, the control can look like this (note the highlighting of a single character):

Image 1

The TextBoxTemplate shown here has three input “fields”, the first being two digits of degrees, the second being a 2.2 decimal for minutes, and the third being the “N” (which the user could change to an “S”). The other symbols, the º mark, the ' (and also the .) are ignored and the user is prevented from placing the cursor on them or changing them. When inputting data, the cursor will automatically skip over these characters.

Because the control does its own formatting, instead of setting the “Text” property of the TextBox, set the new “Value” property. When this property is set, the value will be formatted based on the InputTemplate string.

The behavior of the control is defined by the “InputTemplate” property. Here are some example InputTemplate strings (which are included in the control):

C#
public static string latTemplate = "90º60.00'N";
public static string lonTemplate = "180º60.00'E";
public static string speedTemplate = "00.0kts";
public static string rangeTemplate = "00.000nm";
public static string rangeTemplateYds = "00000yds";
public static string rangeTemplateMtrs = "00000m";
public static string bearingTemplate = "360º";
public static string inclinationTemplate = "00.0ºE";
public static string timeTemplate = "000.0min";
public static string minutesTemplate = "00min";
public static string timeOfDayTemplate = "13:60:60.0AM";
public static string shortTimeOfDayTemplate = "13:60:60AM";
public static string timeOfDayTemplate24 = "24:60:60.0";
public static string shortTimeOfDayTemplate24 = "24:60:60";

Each template defines not only the format for the display of the data value but also the value's input constraints. For the latTemplate (above), the “90” means that the the degrees field must be two digits and be strictly less than 90; the “60.00” indicates that the minutes portion must be a 2.2 decimal number, be strictly less than 60, and that any fraction of a degree should be converted in base 60; the “N” is a special field which can be either “N” or “S”. The other possible special characters in an InputTemplate are “E” indicating either “E” or “W” and “A” indicating either “A” or “P” (for AM/PM). All other characters in the InputTemplate are considered to be "units designations" and are ignored/skipped over.

The resulting control looks like this when included in a window (with latitude and time-of-day templates shown):

Image 2

Want a three-digit decimal for minutes rather than a two-digit decimal? Just change the “60.00” in the InputTemplate to “60.000”. The control will take care of everything else.

Here's what the error message popup looks like positioned under the character position where the validation error occured: (I have just pressed the “9” on the keyboard)

Image 3

Using the Control

The control is inherited from the WPF TextBox control so it can be added to a Window and all the usual parameters can be accessed EXCEPT you MUST set the InputTemplate property and the

Value
property and you MUST NOT set the
Text
property. Create an instance of the
TextBoxTemplate
control (either in code or XAML) and pass it an InputTemplate which is a string which defines the control's behavior (see above). There are static strings for the tested templates such as Latitude, Longitude, time, range, bearing, etc. Then, rather than accessing the TextBox.Text property, access is through the Value property which is a double representing the value of the input (e.g. degrees, hours, etc.).

Here's how to declare the control and use one of the predefined templates—details of how to set up templates are included in the code:

XML
<my:TextBoxTemplate InputTemplate="{x:Static my:TextBoxTemplate.timeOfDayTemplate}" HorizontalAlignment="Left" Margin="150,50,0,0" x:Name="textBoxTemplate3" VerticalAlignment="Top" LostFocus="textBoxTemplate1_LostFocus" />

Here's how you can handle Value properties to and from the control—I like to use the LostFocus event in this because it is Excel-like and doesn't have to handle values while each keystroke of user input is being received:

C#
private void textBoxTemplate1_LostFocus(object sender, RoutedEventArgs e)
{
     if (sender == textBoxTemplate1)
     {
         textBoxTemplate2.Value = textBoxTemplate1.Value;
     }
}

The attached code includes a demonstration of the control and its use. It just copies values from one control to another to show how to get and set the value. To see it in the free application for which it was developed, click here.

Under the Hood / Points of Interest

Setting the InputTemplate: When the InputTemplate is set, it is parsed and a List of fields is created. Each Field has a start position, a length, a maximum value and a truncation flag. This list is used by the formatting and validation functions. The truncation flag is used for integer fields (like degrees) so that if a value is, say 30.9, the first field will be 30 and the .9 can be converted to minutes. The maximum value is also used as the numeric base for conversion of fractions, so that minutes are converted as base 60.

Setting or Getting the Value: There are two functions:

  • protected double GetValueFromText(string text)

  • protected string GetTextFromValue(double theVal)

which handle getting data formatted for the TextBox and returning the value. These rely on the list of fields which was build when the InputTemplate was set. Be aware that because the double carries more precision than the text string, some rounding will occur. Because setting the TextBox Text property directly would allow a program to put strings into the TextBox which are not compatible with the InputTemplate, setting the Text directly throws an exception. Otherwise, the GetValueFromText and input validation would have to be extended to handle the invalid values already in the string.

Evemts: The control handles three of the TextBox's events:

  1. PreviewKeyDown

  2. SelectionChanged

  3. GotFocus

In the PreviewKeyDown event handler, the control does most of the “heavy lifting”. It determines whether the input keystroke is valid for the cursor location, then checks to see that, if the keystroke is accepted, it would result in a valid value. If the value is valid, the keystroke is passed to the underlying

TextBox
. If not, the control uses a popup window to display a reasonable message at the cursor location. Some interesting issues here:

  • If the cursor position is at the beginning or end of the TextBoxTemplate, the control MoveFocus to move to the previous/next control in response to arrow keys.

  • If the user presses BACK, it is treated as a left arrow.

  • Interesting Case: if a field which has a value limit of 180, currently contains 090, the cursor is positioned at the first character position and the user presses “1”. If accepted, the TextBox would contain 190 which is invalid, but displaying an error message and rejecting the input would prevent the user from typing in “120” which is perfectly reasonable. The Solution: the “1” is accepted and the following digit is set to zero so the box contains “100”. Users seem to like this solution.

In the SelectionChanged event handler, the control forces the TextBox into “overstrike” mode by setting SelectionLength to 1 so one character is always selected/highlighted. It also checks to see if the cursor is positioned over a valid input character and skips over non-input characters. This required an additional variable “movingLeft” (set by PreviewKeyDown) so it can determine which direction to move the cursor if it is on an invalid character.

In GotFocus event handler, the control handles the issue of where to position the cursor when the control is entered. Normally the cursor would be positioned at the character it was at when the control was most recently used and this can be confusing. If the control was focused by a mouse-click, the character at the mouse cursor is selected, if by a Shift-Tab, the last character is selected, otherwise, the first character is selected.

Time values are handled by converting them to doubles TimeToDouble and TimeFromDouble functions. That way the Value is always a double. In a future version, I will probably choose a different solution to handling time values as doubles.

History

Initial Submission: 3/31/12

Revised: 4/1/12 with extensive detail added

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
An avid sailor, I combine writing useful programs with travel, sailing, diving, sailing...did I mention sailing?

I also used to work at Microsoft.

Try out my latest free sail racing management app, "Committee Boat Suite," here.

Most of my code posts are extracted from the app.

Comments and Discussions

 
QuestionPlz help, I'm trying to add DP to your control, to be embedded in a Usercontrol Pin
Member 795284819-Nov-13 23:52
Member 795284819-Nov-13 23:52 
QuestionNeeds more detail Pin
Patrick Kalkman31-Mar-12 20:57
Patrick Kalkman31-Mar-12 20:57 
AnswerRe: Needs more detail Pin
CharlieSimon2-Apr-12 3:47
CharlieSimon2-Apr-12 3:47 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.