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 his 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:

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 (in decimal notation) 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:

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:

[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
}
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
}

Events

The following events are specific to UniversalNumericEdit box only:

Methods

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.


Home  bullet Products  bullet Downloads  bullet About Us  bullet Contact Us

Credit Card Logo