Introduction
A few weeks ago, I posted a concept for a numbers only textbox here. The idea was good but the design was not exactly flawless. Now, I decided to make up for my mistakes and rework my design. I'll show you how to set up a reusable control step by step.
Background
This solution will check all characters of the Textbox.Text
property as soon as the TextChanged
event is activated. This can use slightly more resources that other solutions (especially if your Textbox
holds much text), but make sure that the text is always clean and ready to process, even if multiple characters are pasted in.
I'm using a UserControl
with a textbox in it to make this control useable in all your projects.
Using the Code (Step by step guide for beginners)
This is the beginner firendly guide. If you're more experienced and just looking for the code, scroll down to Using the Code (for experienced).
First off, we're going to create a new UserControl
so that we can use the Textbox in other projects later on.
The UserControl
will then hold the TextBox that the user can interact with.
In Visual Studio, create a new project. Make sure that you have "Templates" selected, then search for "Windows Forms Control Library" and select the Visual C# preset. Name your Control and click OK.
Now you are in the designer. Click on the white flat and in the properties, rename your control to something like "NumTextBox". After that, press F7 once to get to the code.
For this, you'll need the following usings:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.Windows.Forms.Design;
Now, we need to add one more reference. In the Solution Explorer, right-click on References and select Add Reference. Make sure to select Assemblies -> Framework on the left and then search for System.Design
. Check the checkbox in front of it and hit "OK"
The sizing
Now, we need to do some technical stuff. Above public partial class [your control's name] : UserControl
, add this:
[Designer(typeof(CustomSizeDesigner))]
Then, below your class but still in the namespace, add annother internal class:
internal class CustomSizeDesigner : ControlDesigner
{
CustomSizeDesigner()
{
base.AutoResizeHandles = true;
}
public override SelectionRules SelectionRules
{
get
{
return SelectionRules.LeftSizeable | SelectionRules.RightSizeable | SelectionRules.Moveable;
}
}
}
And one more step: In your class, add following method:
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
height = 22;
base.SetBoundsCore(x, y, width, height, specified);
}
These things will make sure that our UserControl
always has a Height
of 22 (like a textbox), so it won't cover anything else. You can now fold in the internal class
, as we won't need to edit it again.
To finalize the this, we'll need to one more thing. The TextBox
does not yet scale it's Width
, so we're going to add this. Go back to the design editor, select the while flat and in the properties window, click the small lightning. In the list below, search for ControlAdded
and doubleclick it. VS will generate a new function and show it to you. In the function header, make sure to change ControlEventArgs e
to EventArgs e
. Now, write down the following:
originalTextBox.Width = this.Width;
originalTextBox.Height = 22;
originalTextBox
will be underlined in red, but we'll sort that out in a few moments. This will essentially make sure that our TextBox
will be sized correctly. And just to make sure that the TextBox
also resizes if our UserControl
is resized at runtime, go back to the design editor, click the white flat and lightning again, search for Resize
, select the field to it's right, click the little arrow and select [your control's name]_ControlAdded
and you're done.
The actual TextBox
Now that we have the framework, we're going to add the centerpiece of our control: the TextBox
that the user can interact with. At the top of your class, declare two new variables:
private bool acceptsNegative;
private TextBox originalTextBox = new TextBox();
The first bool
will determine, if your TextBox
will accept a minus at the front of the number or not. The second variable will be our TextBox
.
Next, we'll add a AcceptsNegative
property, which will allow us to allow or prohibit negative numbers in our TextBox
. add the following code to your class:
[Description("Determines if the textbox will accept negative numbers."),Category("Input")]
public bool AcceptsNegative
{
get { return acceptsNegative; }
set { acceptsNegative = value; }
}
The [Description]
tag adds some information that will show up in the design editor and the get
and set
statements are used to read out and change the acceptsNegative
bool from above.
Now, we need to do the same thing for the Text
property of our TextBox
, so that we can later access it from the code:
[Description("The Text of the textbox"),Category("Data")]
public string Text
{
get { return originalTextBox.Text; }
set { originalTextBox.Text = value; }
}
This will just "forward" the Text
property of our TextBox
to a property of our UserControl
.
The next thing to do is to actually add our TextBox
to the UserControl
. Search for the following constructor (it is auto-generated):
public NumTextBox()
{
InitializeComponent();
}
This constructor will be called when your control is added to the Form you may put it in later. So this is the perfect place to bring in our TextBox
. Just add the following two lines:
originalTextBox.TextChanged += originalTextBox_TextChanged;
this.Controls.Add(originalTextBox);
The first line will add an EventHandler
that will call the method originalTextBox_TextChanged
as soon as you change the text in our TextBox.
We didn't create the method originalTextBox_TextChanged
yet, so it will be underlined in red, but you can just ignore that. The second line adds the TextBox
to our control, so it will show up later. There is just one step left now: creating the method that actually makes our TextBox
a numbers only TextBox
.
If VS didn't do it for you yet, create the function originalTextBox_TextChanged
and populate it like this:
void originalTextBox_TextChanged(object sender, EventArgs e)
{
string text = originalTextBox.Text;
int selectionIndex = originalTextBox.SelectionStart;
List<char> invalidTokens = new List<char>();
foreach (char c in text)
{
if (!Char.IsNumber(c))
{
if (!invalidTokens.Contains(c))
invalidTokens.Add(c);
}
}
if (invalidTokens.Count > 0)
{
foreach (char c in invalidTokens)
{
if (c == '-' && acceptsNegative)
{
int index;
while (true)
{
index = text.LastIndexOf(c);
if (index == 0 || index == -1)
break;
else
text = text.Remove(index, 1);
}
}
else
text = text.Replace(c.ToString(), string.Empty);
}
originalTextBox.TextChanged -= originalTextBox_TextChanged;
originalTextBox.Text = text;
originalTextBox.TextChanged += originalTextBox_TextChanged;
if (originalTextBox.Text.Length > selectionIndex)
originalTextBox.SelectionStart = selectionIndex;
else
originalTextBox.SelectionStart = originalTextBox.Text.Length;
}
}
How it works
We'll go over this step by step. First off, we save the Text
of our TextBox
to a new string
named text
. After that, we save the position of the text-edit cursor. Last but not least, we create a new List<char>
named invalidTokens
. After that, in the foreach
loop, we go over every character in our string
(the current character will be saved to the variable c
). We then check if the character is a number and if not (and it's not yet in our List<char>
), we add it to our List<char>
. The reason that we don't directly remove it is because it's dangerous to edit the data you're looping though (and this will also throw an error more often than not).
When our loop has finished, we check if any invalid characters got added to our List<char>
. If this is not the case, the function will simply return. If there are any, we'll go through them again in the next foreach
loop. First off, we check if the current invalid character is a minus and if we accept negative numbers (= acceptsNegative
is true
). If one of these is not the case, we replace all instances of this character in our text with string.Empty
, effectively removing it (We first convert the char
to a string
, since you can't replace a char
with a string
). But if the character is a minus and we allow negative numbers, we need to do some more work. There may be mutiple instances of the minus, but we can't delete them all since this would also remove the minus at the front of the text, which we want to have there. So we get the index
of the last minus in the string
. If the index
is not 0
(which would mean the minus is at the front of the numbers) or -1
(which would mean there is no minus, this may happen if we've removed all), then we know that there is a minus inside or at the end of the string
, which is a illegal position for it. We then remove 1
character at the position of the minus (this is the minus ;) ) check again. If the last minus is at index 0
, so at the front, or at -1
, which means there is no, we can be sure that the text is free of minuses and break out of our infinite loop. We then proceed to removing the other invalid characters.
When we are done with removing all invalid characters, our string
is now clear and we can put it into our TextBox
. First off, we remove the EventHandler
so the function won't call itself (to save some resources), then we set the Text property
of our TextBox
to our cleaned string
and after that, we add the EventHandler
again, so next time the Text
property changes, it will be cleaned again.
The last step is to position the cursor right. First off, we check if the Text
is longer than the position that the cursor was (you know what I mean). If yes, we position the cursor where it was before. If no, the Text
is now shorter than before and the cursor would be 'behind' the Text
(and throw an error), so we just put it at the end, which is the closest position to the old one.
Using the Code (for experienced)
Create a custom UserControl
and add System.Design
as a reference. Make sure to edit the classes name according to the name of your UserControl
.
This is the code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.Windows.Forms.Design;
namespace SampleNumbersOnlyTextbox
{
[Designer(typeof(CustomSizeDesigner))]
public partial class NumTextBox: UserControl
{
private bool acceptsNegative;
private TextBox originalTextBox = new TextBox();
public NumTextBox()
{
InitializeComponent();
originalTextBox.TextChanged += originalTextBox_TextChanged;
this.Controls.Add(originalTextBox);
}
void originalTextBox_TextChanged(object sender, EventArgs e)
{
string text = originalTextBox.Text;
int selectionIndex = originalTextBox.SelectionStart;
List<char> invalidTokens = new List<char>();
foreach (char c in text)
{
if (!Char.IsNumber(c))
{
if (!invalidTokens.Contains(c))
invalidTokens.Add(c);
}
}
if (invalidTokens.Count > 0)
{
foreach (char c in invalidTokens)
{
if (c == '-' && acceptsNegative)
{
int index;
while (true)
{
index = text.LastIndexOf(c);
if (index == 0 || index == -1)
break;
else
text = text.Remove(index, 1);
}
}
else
text = text.Replace(c.ToString(), string.Empty);
}
originalTextBox.TextChanged -= originalTextBox_TextChanged;
originalTextBox.Text = text;
originalTextBox.TextChanged += originalTextBox_TextChanged;
if (originalTextBox.Text.Length > selectionIndex)
originalTextBox.SelectionStart = selectionIndex;
else
originalTextBox.SelectionStart = originalTextBox.Text.Length;
}
}
[Description("Determines if the textbox will accept negative numbers."), Category("Input")]
public bool AcceptsNegative
{
get { return acceptsNegative; }
set { acceptsNegative = value; }
}
[Description("The Text of the textbox"),Category("Data")]
public string Text
{
get { return originalTextBox.Text; }
set { originalTextBox.Text = value; }
}
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
height = 22;
base.SetBoundsCore(x, y, width, height, specified);
}
private void NumTextBox_ControlAdded(object sender, EventArgs e)
{
originalTextBox.Width = this.Width;
originalTextBox.Height = 22;
}
}
internal class CustomSizeDesigner : ControlDesigner
{
CustomSizeDesigner()
{
base.AutoResizeHandles = true;
}
public override SelectionRules SelectionRules
{
get
{
return SelectionRules.LeftSizeable | SelectionRules.RightSizeable | SelectionRules.Moveable;
}
}
}
}
Customizing the NumTextBox
Of course you may want to edit for example the look or function of your NumTextBox
. You can easily add the option to edit any property of your NumTextBox
as long as it's a property of a normal TextBox
. I'll demonstrate it with the BorderStyle
property here.
All you need to do is create a new property (in your class) like this and set the get
and set
to the textbox:
[Description("Sets the BorderStyle of your TextBox"),Category("Appearence")]
public BorderStyle TextBoxBorderStyle
{
get { return originalTextBox.BorderStyle; }
set { originalTextBox.BorderStyle = value }
}
In the designer, this will now show up as a property of your UserControl
and you can also edit this property at runtime. You can do that for every property of your TextBox
.
Including the NumTextBox in WinForms applications
Now that your UserControl
is finished, you can try it out by clicking the Start button (don't forget to set AcceptsNegative
to true
while debugging to be able to enter negative numbers). If you want to include your NumTextBox
in other projects like your WinForms
application, we need to include it there. We will use the .dll file
so you don't need to import this whole project everytime you want to use the UserControl
.
First off, start your UserControl
once in Release
mode instead of Debug
mode. Just hit Start and close it down again.
Now, in the solution explorer, right click on your solution (It's called Solution '[your solution's name]'
) and select 'Open Folder in File Explorer'. Then open the topmost folder, navigate to bin
-> Release
and copy to .dll file
to your clipboard. You can then paste it anywhere you want, for example just on the desktop or into your .dll collection if you have one.
Now you can close the solution and open any other WinForms
project. Go to the Toolbox
, rightclick anywhere and select "Choose items...". Wait for the list to load, click on "Browse...", select your .dll
and hit OK when you're done. You now have your NumTextBox
in your Toolbox and you can simply drag it onto your Form and use it.
Have fun!
Note: I posted the sample project that is ready to use. Feel free to download and use everywhere.