Introduction
When it comes to editing numbers in WinForms application, there is very short list of controls you can use.
You can use TextBox
control, but if you decide to do so, everything is on you. You have to provide validation to make sure that number is in valid format and range. You have to provide editing, key press handling etc. In another words it means a lot of work.
You can use MaskedEdtBox
and set it up do accept only numbers in format you require, but I always saw awkwardness in how this control feels and looks. Maybe it's personal an there's nothing wrong with it, but whenever I tried to use it in the past, I was never satisfied with end results.
There is NumericUpDown
control. It does it's job but is not not very customizable. For example: you cannot hide UpDownButtons portion of it; there is no build in support for numbers written in different numeral system than decimal, for example you cannot show and edit numbers as hexadecimal strings.
In my another article GenericUpDown Control I wrote about enhancing NumericUpDown
functionality, but solutions I provided there do not go far enough in order to provide editing and validation of numeric values.
After reviewing of all available options I decided that I need to fill the gap and provide Universal Numeric Edit control that can display and edit numbers expressed as strings encoded with any given base (radix).
Background
What I had to do was:
- Create user control that inherits from
TextBox
. Initially I was hesitating what should I use for the base control but quickly decided that
TexBox
is the beast match. It provides basic editing, caret display and movement and selections. Only thing needed was to add extra constraints to allow entering only valid characters and proper display formatting. - Add ability to show numbers in different numeral system encoding. Fortunately, In recently posted article Integer Base Converter I described class allowing to display and convert numbers encoded with any given numeral base. With
IntegerBaseConverter
class in hand this task becomes trivial.
- Override handling of context menu items.
TextBox
context menu behavior is too generic for purpose of this control. Along the way I decided to add more functionality to the context menu by adding Redo
functionality (Undo
along with basic editing methods is there already). Additional benefit of having new context menu is that it is easy accessible for the host. You can change or update it with own additional functionality. One obvious extension would be adding new editing functions specific to numbers, for example: logical AND/OR/XOR/NOT shifting/cycling bits and so on.
Making Control
For start I created new user control hat inherits from TextBox
:
public partial class UniversalNumericEditBox : TextBox
{
}
This will act exactly as TextBox
control. So to make it truly Numeric Edit Box we need to define few key properties and change behavior when user press a key. Let start from properties.
Defining essential properties
Since I wanted for the control to handle integer numbers and .NET has several integer types available I wanted to be able to define what integer type control is going to contain:
public NumericType NumericType
NumericType
is defined as enumerator:
public enum NumericType
{
Byte,
UInt16,
UInt32,
UInt64
}
Next, I wanted Value
property to tell the control instance what value is currently edited.
public BigInteger Value
Please note the type associated with this property. It is BigInteger
. BigInteger
can handle all NumericTypes
allowed for the control and much more. This is handy in some scenarios when control may contain text representing number bigger than allowed for given NumericType
.
Third essential property is Radix
which will tell how to display control's Value
. Radix
means: base of positional numeral system (also known as radix). It defines how Value
will be presented to the user. Radix
can be any integer equal or greater than 2.
public int Radix;
Finally, I wanted to instruct control what characters should be used for displaying digits in the number. We usually don't think about how numbers are represented in writing. We have it hardwired in our brains that first value of unsigned integer is shown as '0', second one as '1', third as '3'. Conventionally to write decimal number we use "0123456789" digits, while for hexadecimal we use "01234567890ABCDEF" . But there is no reason we have to stick forever with "0123...." sequence. To write a number we could use Hebrew characters instead: 0גבא... or perhaps Chinese: 〇一二三四 or invent your own graphical representation of digits. Perhaps is better to follow convention in writing numbers with commonly used numeral bases like 2, 8, 10, 16. But what about radix-256 numbers? I didn't hear about any standard set of digits (we need exactly 256 distinct characters) for radix-256. So in such case we probably will need to invent own set of digits. The next property called Digits
serves purpose of providing your own character set do display numbers.
If you are OK with conventional thinking that 0 is Zero, 1 is One, 2 is Two etc. you don't have to assign this property at all and relay on default set which is 64 character string: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/"
public string Digits
Above four basic properties (NumericType
, Value
, Radix
and Digits
) fully define how values should be presented for editing.
Now, let's talk about formatting itself. UniverslaNumericEditBox
fully depends on IntegerBaseConverter
class in this matter. Expanding body of Value
property reveals that is amazingly short: setter and getter just call single method in IntegerBaseCoverter
to convert BigInteger
number to proper string representation and vice versa.
public BigInteger Value
{
set { Text = IntegerBaseConverter.GetPaddedValue(value, Radix, NumericTypeToSystemType(NumericType), Digits); }
get { return IntegerBaseConverter.Parse(Text, Radix, Digits); }
}
Consider the following example:
Let say that Value
property holds 100 and Radix
is 2, and NumericType
is Byte and Digits
is not assigned (standard). With such values, control should display and allow editing the following string "1100100" what is binary representation of 100.
In fact UniversalNumericEditBox
has much more properties. All of them are listed in Using Code section, but four properties discussed above are the most essential for functionality of the control.
Handling of user input
Almost all code related to editing is encapsulated in overridden method: OnKeyPress
.
Please note that only portion of code is presented here - portion essential to illustrate this article. If you want to see all details please download and inspect source code attached to this article.
protected override void OnKeyPress(KeyPressEventArgs e)
{
int pressedKeyIndex = IntegerBaseConverter.DefaultDigitsSet.IndexOf(e.KeyChar);
if ((pressedKeyIndex >= Radix) || (pressedKeyIndex < 0))
{
e.Handled = true; return;
}
if (SelectionLength == 0)
SelectionLength = 1;
string insertString = new string(e.KeyChar, Math.Max(SelectionLength, 1));
base.Text = base.Text.Substring(0, this.SelectionStart) + insertString +
base.Text.Substring(this.SelectionStart + this.SelectionLength);
base.OnKeyPress(e);
}
Handling arrow keys
There is nothing unusual in arrow key handling - same standard behavior as in TextBox
control. But there is one point I want to draw your attention to. I wanted to be able to notify host when combination of control caret position and arrow key pressed might indicate user intention of exiting from edit mode or changing focus to completely different control. Cases I am talking about are when:
- Caret position is at first character and user presses left arrow.
- Caret position is at last character and user presses right arrow.
- Ctrl/Tab was pressed.
- User presses
KeyUp
or KeyDown .
For such cases I created ExitAttempt
event. It is called for each above "border" case. Subscriber to the event then might decide what to do with it next: move focus, change control position, perform some other tasks or do nothing.
protected override void OnKeyDown(KeyEventArgs e)
{
this.BackColor = Color.Empty; if (e.KeyCode == Keys.Delete)
{
bool isRangeSelected = SelectionLength > 0;
if ((!isRangeSelected) && (SelectionStart>0)) SelectionStart--;
OnKeyPress(new KeyPressEventArgs(IntegerBaseConverter.DefaultDigitsSet[0]));
if ((!isRangeSelected) && (SelectionStart>0)) SelectionStart--; e.Handled = true;
}
else if (
((e.KeyCode == Keys.Left) && (this.SelectionStart == 0)) ||
((e.KeyCode == Keys.Right) && (this.SelectionStart == Text.Length)) ||
((e.KeyCode == Keys.Tab) && (e.Modifiers == Keys.Control)) ||
(e.KeyCode==Keys.Up) || (e.KeyCode==Keys.Down)
)
{
OnExitAttempt(e.KeyCode, (char)0);
}
base.OnKeyDown(e);
}
Context Menu
Control's context menu looks almost identical to the context menu of standard TextBox
control.
There is only one item I decided to add: Redo
menu item
to enhance user editing experience. Also Undo
behaves quite differently form what is present in TextBox
: UniversalNumericEditBox
remembers ALL editing steps and you can move bask and forth at will using Undo
/Redo
pair of commands.
Of course all internal handling of context menu items is overwritten to make sure that proper validations are in place when editing numbers.
One positive side effect of this is that context menu is accessible via ContextMenuStrip
property and can be easily customized.
Validation
When editing numbers couple things can go wrong:
- User might type invalid digit.
- Text when interpreted a a number might be bigger than maximum value allowed for current
NumericType
.
First type of potential error is avoided by filtering all keys user presses and validation content of Clipboard when pasting. For example If current Radix is 16, you can chose only characters 1,2,3,4,5,6,7,8,9,0,A,B,C,D,E,F. If you press any other character, for example 'G' it will be filtered out and input will be ignored. There is no way to enter invalid character.
Second type of potential error is handled differently and it is controlled by ValidationMode
property. Possible values and their meaning are described below:
public enum ValidationMode
{
[Description("Validates value every time text changes.")]
Continuous,
[Description("Validates only after editing ends (when focus is about to change).")]
Lazy,
[Description("Never validates. It means that text value might be not valid (too large)
and Value method getter might return value larger than allowed for given NumericType.")]
None
}
If validation finds issue with the number it has to somehow show it to the user. How to behave when validation error happens is controlled by ValidationErrorAction
flag. Possible values and their meaning are shown below:
[Flags]
public enum ValidationErrorAction
{
None=0,
Beep=1,
ShowToolTip=2,
ChangeBackColor=4
}
Using the code
First few paragraphs highlighted only key elements of UniversalNumericEditBox
. Now it's time to show the rest.
UniversalNumericEditBox
behaves as any other WinForms control. You can drop it on the window form, set properties you want to modify, hookup events (probably ValueChanged
will be one of them) and you are ready to go.
Properties
Besides properties inherited form ancestors, UniversalNumericEditBox
adds several new properties and modifies behavior of existing properties. Here is the full list:
BackgroundColorOnValidationError -
Background color used to indicate validation error. If ChangeBackgroundColor
flag in ValidationErrorAction
property is set, every time validation error occurs, control's background color will change to color set in this property. Default value is Color.Coral
. BorderColor
- Control's border color. Border color can only be set if BorderStyle == BorderStyle.FixedSingle
. BorderWidth
- Control's border width. Border width can only be set if BorderStyle == BorderStyle.FixedSingle
. CanRedo
- Indicates if Redo
action can be performed. Redo
action is possible only if there was Undo
action performed before. CanUndo
- indicates if Undo
action can be performed. Undo
action is possible only if there was any editing done with control that was not undone already. CanUseCaseInsensitiveInputMode
- Validates, if it is possible, with the current Digits
and Radix
settings, to accept case insensitive input. In another words it checks if substring of Digits
string of length equal Radix
value, does not contain case insensitive duplicates. With default Digits
value, property returns true when Radix
is less or equal 36 and false for larger numbers. -
CaseInsensitiveInputHint
- Hint asking control to perform case insensitive input when feasible. Hint will be followed only if digits needed to express number for current Radix
don't have same character repeated regardless on their case.
For example:
if Digits contains following characters: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/", hint will be obeyed for Radix
because the first 36 characters of Digits
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ do not have duplicates. If Radix
is greater than 36 hint will be ignored. Digits
- Ordered set of digits that can be used to represent number. Default value contains 64 characters in order to accommodate base 64 encoding. If you want to use UniversalNumericEditBox
to edit numbers encoded with Radix
bigger than 64, you have to supply longer string containing distinct digits. InsertKeyMode
- Defines editing behavior: Insert vs. Overwrite. Proper mode for most situations is InsertKeyMode.Overwrite.
MaxValue
- Gets maximum value for current NumericType
. NumericType
- NumericType
determines "capacity" of the instance of UniversalNumericEditBox
. Valid values are: Byte, UInt16, UInt32 or UInt64. Radix
- Numerical base (radix) of the edited number. SizeInBytes
- Size of control's Value
property in bytes. Size depends directly on NumericType
. Text
- Sets or gets stored value as text encoded using current Radix
. If current Radix
does not have capacity to hold the number represent by the Text, NumericType
is modified to the smallest one able to hold the value.
For example:
If current NumericType
is Byte
, Radix
is 16 and we are trying to set Text
to "100", control NumericType
will automatically change to UInt16
because "100" represents decimal value 256 which is bigger than maximum allowed for the Byte
. ValidationErrorAction
- Flag defining control behavior when error occurs during validation. Possible values are defined below.
[Flags]
public enum ValidationErrorAction
{
None=0,
Beep=1,
ShowToolTip=2,
ChangeBackColor=4
}
ValidationMode
- Determines if and when number validation is performed. Possible values are defined below.
public enum ValidationMode
{
[Description("Validates value every time text changes.")]
Continuous,
[Description("Validates only after editing ends (when focus is about to change).")]
Lazy,
[Description("Never validates. It means that text value might be not valid
(too large) and Value method getter might return vlue larger than allowed for given NumericType.")]
None
}
-
Value
- Sets or gets numerical value. If NumericType
is not big enough to hold the Value
exception is thrown in the setter.
Events
The following events are specific to UniversalNumericEdit
box only:
EventHandler<EventArgs> BorderWidthChanged
- Event triggered every time control border width (BorderWidth
property) changes.
EventHandler<EventArgs> BorderColorChanged
- Event triggered every time border color (BorderColor
property) changes.
EventHandler<EventArgs> CaseSensitiveInputChanged
- Event triggered every time CaseSensitiveInputHint
property value changes. EventHandler<ValidationErrorEventArgs> ValidationErrorOccured
- Event triggered every time when validation error occurs. EventHandler<EventArgs> ValidationModeChanged
- Event triggered every time value of ValidationMode
property changes. EventHandler<EventArgs> RadixChanged
- Event triggered every time value of property Radix
changes. EventHandler<EventArgs> ValidationErrorActionChanged
- Event triggered every time value of property ValidationErrorAction
changes.EventHandler<EventArgs> ValidationErrorBackgroundColorChanged
- Event triggered every time when value of property ValidationErrorBackgroundColor
changes.EventHandler<DigitSetChangedEventArgs> DigitSetChanged
- Event triggered every time when value of property Digits
changes.EventHandler<ValueChangedEventArgs> ValueChanged
- Event triggered every time Value
changes. EventHandler<ExitAttemptEventArgs> ExitAttempt
- Event triggered every time when key suggesting exit from editing was pressed.
Methods
Clear()
- Clears (zeros all position) of edited number. ClearUndo()
- Cleanups undo and redo history. Cut()
- Cuts selected portion of edited number (copies selected area to the clipboard and replaces selection with zeroes) Paste()
-Pastes text from the Clipboard to the control. Action is performed only if text represents valid number. Paste(String)
- Pastes text to the control. Action is performed only if text represents valid number. Redo() -
Reverses last Undo operation. Undo()
- Reverses last edit operation.
Demo Program
Downloadable demo program along with source code and full documentation provides means to test, and discover all functionality of UniversalNumericEditBox
. Image shown at the beginning of this article shows screenshot of the main screen of demo program.
History
- September 12th, 2013 - Initial version of the article.
- September 17th, 2013 - I changed the source code to fix issues related to control's behavior in development mode. Most notably I added property grid editor for
BigInteger
to allow editing of Value
property in development mode.