Summary
This templated TextBox
control is
really handy when you have specialized/formatted numeric input. Use
it like a 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 Data | Sample of Desired Formatting |
Latitude | 22º33.445'N
|
Range\ |
12.5 NM
|
Elapsed Time |
12:34:56.7
|
Time of Day |
10:34:56.7PM (or 22:34:56.7) depending on the user's preference
|
Internally, all of these values are stored as double
s
(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 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
TextBoxTemplate
display for all value
types and protecting the units from user input. If I had included
the units as Labels
,
a time input
control (for example) could consist of three TextBoxe
s
for the hours, minutes, and seconds, another (or a ComboBox
)
for the AM/PM designation, and Label
s
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:
To display the value in the format the user prefers (or the program dictates)
To constrain the user to inputting only valid data
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):
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):
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):
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)
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:
<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:
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:
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:
PreviewKeyDown
SelectionChanged
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