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

Universal Numeric Edit Control

0.00/5 (No votes)
17 Sep 2013 1  
User control for editing numbers writtem using different numeral base.

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
{
    /// <summary>
    /// One byte long unsigned integer.
    /// </summary>
    Byte, 
    /// <summary>
    /// Two bytes long unsigned integer.
    /// </summary>
    UInt16, 
    /// <summary>
    /// Four bytes long unsigned integer.
    /// </summary>
    UInt32, 
    /// <summary>
    /// Eight bytes long unsigned integer.
    /// </summary>
    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)
{
    //Check if pressed key is valid digit
    int pressedKeyIndex = IntegerBaseConverter.DefaultDigitsSet.IndexOf(e.KeyChar);
    if ((pressedKeyIndex >= Radix) || (pressedKeyIndex < 0))
    {
        e.Handled = true; return;
    }

    //This is needed to overwrite next character
    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; // m_CurrentBackColor;
    if (e.KeyCode == Keys.Delete)
    {
        bool isRangeSelected = SelectionLength > 0;
        //We are replacing character left form caret with '0' (delete)
        if ((!isRangeSelected) && (SelectionStart>0)) SelectionStart--; 
        OnKeyPress(new KeyPressEventArgs(IntegerBaseConverter.DefaultDigitsSet[0]));
        if ((!isRangeSelected) && (SelectionStart>0)) SelectionStart--;  //moving left one character
        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)
    )
    {
       //Fire ExitAttempt event for hosting app to reposition floating control
        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 TextBoxUniversalNumericEditBox 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:

  1. User might type invalid digit.  
  2. 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
{
    /// <summary>
    /// Validates value every time text changes.
    /// </summary>
    [Description("Validates value every time text changes.")]
    Continuous,
    /// <summary>
    ///  Validate only if control is about to lose focus. If validation fails,
    /// focus stays with control till user corrects value.
    /// </summary>
    [Description("Validates only after editing ends (when focus is about to change).")]
    Lazy,
    /// <summary>
    /// Never validates. Text will contain characters from proper character set, 
    /// but value might exceed maximum vale allowed, and in turn Value property getter
    /// can return number with value greater than MaxValue of NumericType property.
    /// </summary>
    [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
{
    /// <summary>
    /// No action is performed in case of validation error.
    /// </summary>
    None=0,
    /// <summary>
    /// If validation error occurs, single beep is issued.
    /// </summary>
    Beep=1,
    /// <summary>
    /// If validation error occurs, ToolTip with error description pop-ups.
    /// </summary>
    ShowToolTip=2,
    /// <summary>
    /// If validation error occurs, control box background changes to the color
    /// defined in <c>OnErrorBackgroundColor</c> property.
    /// </summary>
    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
{
    /// <summary>
    /// No action is performed in case of validation error.
    /// </summary>
    None=0,
    /// <summary>
    /// If validation error occurs, single beep is issued.
    /// </summary>
    Beep=1,
    /// <summary>
    /// If validation error occurs, ToolTip with error description pop-ups. 
    /// </summary>
    ShowToolTip=2,
    /// <summary>
    /// If validation error occurs, control box background changes to the color
    /// defined in <c>OnErrorBackgroundColor</c> property.
    /// </summary>
    ChangeBackColor=4
}
  • ValidationMode - Determines if and when number validation is performed.   Possible values are defined below. 
public enum ValidationMode
{
    /// <summary>
    /// Validates value every time text changes.
    /// </summary>
    [Description("Validates value every time text changes.")]
    Continuous,
    /// <summary>
    ///  Validate only if control is about to lose focus. If validation fails, focus stays with control till user corrects value.
    /// </summary>
    [Description("Validates only after editing ends (when focus is about to change).")]
    Lazy,
    /// <summary>
    /// Never validates. Text will contains characters from proper character set, 
    /// but value might exceed maximum vale allowed, and in turn Value property getter
    /// can return number with value greater than <c>MaxValue</c> of <c>NumericType</c> property.
    /// </summary>
    [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. 

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