Introduction
In this article, we'll learn step-by-step how to create a custom textbox control that only accepts numeric characters. The control will allow the client code to specify whether it should work as an integer or decimal textbox.
This custom control will be implemented using Visual Studio 2008. However, the same code can be written using previous versions of Visual Studio. The programming languages I'm going to use are: C# and JavaScript.
Prepare the Project
First of all, let's prepare the project and the files that we're going to use. Please follow these simple steps:
- Create a new project of type Class Library and name it MyCustomControls.
- Make sure that you add a reference to the System.Web library.
- By default, Visual Studio will add a file named 'Class1.cs'. Please delete this file and add two files with the following names: NumericTextBox.cs and NumericTextBoxExtention.cs. In addition, create a new folder with the name 'Resources' and add a JavaScript file with the name 'NumericTextBox.js'.
- In order to make the JavaScript embedded in the component, we need to change its
BuildAction
property to 'Embedded Resource'. Please refer to the figure below:
The C# Code
Why Two C# Files?
As you may have already noticed, we have two C# files. Both of them will be used to implement the same class: NumericTextBox
. However, the keyword partial
will be added to the definition so the compiler will know that both classes represent one class in the end. The NumericTextBox.cs file will be used to implement the logic of our control, while the NumericTextBoxExtention.cs will contain any fields or properties.
Please have a look at the code below to see how we're going to define the NumericTextBox
class in the NumericTextBox.cs file. Code Snippet 2 shows the code in the NumericTextBoxExtention.cs file.
Please note that the partial classes is a feature added to .NET version 2.0 and later versions. If you're using a 1.* version of .NET, you can alternatively place all your code in one class.
using System;
using System.Web.UI;
[assembly: WebResource("MyCustomControls.Resources.NumericTextBox.js", "text/javascript")]
namespace MyCustomControls
{
[ToolboxData(@"<{0}:NumericTextBox Text="""" runat=""server""></{0}:NumericTextBox>")]
public partial class NumericTextBox : System.Web.UI.WebControls.TextBox
{
}
}
namespace MyCustomControls
{
public partial class NumericTextBox : System.Web.UI.WebControls.TextBox
{
}
}
Fields and Properties
There are some attributes added in Code Snippet 1. Don't worry about them now, we'll talk about them later in this article. Now we want to see what properties we need to implement in the NumericTextBoxExtention.cs file.
Let's think of properties that we might need. First of all, we've said previously that our control will work for both integers and decimals. So, it would be wise to add a property named Type
. This property can be of type int
or string
. However, I think it's a better practice to add an enum
named TextBoxType
and let our property's type be of this enum.
Additionally, we'll need a property to specify the number of integers allowed. And, in the case of the decimal textbox, we'll also need a property to specify the maximum number of fractions allowed after the decimal point. Furthermore, we'll add a property to specify whether this control should accept negative values or not. Please have a look at Code Snippet 3 to see how this will be implemented.
public enum TextBoxType
{
Integer,
Decimal
}
private int _NumberOfFraction = 0;
private int _NumberOfInteger = 0;
private bool _AllowNegative = false;
private TextBoxType _Type = TextBoxType.Integer;
public int NumberOfFraction
{
get { return _NumberOfFraction; }
set { _NumberOfFraction = value; }
}
public int NumberOfInteger
{
get { return _NumberOfInteger; }
set { _NumberOfInteger = value; }
}
public bool AllowNegative
{
get { return _AllowNegative; }
set { _AllowNegative = value; }
}
public TextBoxType Type
{
get { return _Type; }
set { _Type = value; }
}
Attributes
In order to let the compiler know that there's a resource that should be attached here, we need to add an attribute to the namespace, as you can see in Code Snippet 1. We've specified the path of the JavaScript file following the pattern: "Project Name.Folder Name.File Name".
In Visual Studio, drag and drop any ASP.NET control to your page. It will automatically create the necessary tags and attributes. In order to let our control have the same behavior, we've added an attribute to the class name. Please refer to Code Snippet 1. {0}
will be replaced by a value that you specify in your web.config or in any config file. For ASP.NET controls, it's by default asp
.
Please refer to Code Snippet 4 to see how you can specify this value in your web.config. So, when dragging and dropping our control to a web page, it will look like Code Snippet 5.
<pages>
<controls>
<add tagPrefix="mcc" namespace="MyCustomControls" assembly="MyCustomControls"/>
.
.
.
<--//
// Code Snippet 5
//-->
<mcc:NumericTextBox Text="" runat="server"></mcc:NumericTextBox>
Override OnPreRender
We need to override OnPreRender
to implement the way we want the control to be pre-rendered. In other words, we want to let the OnPreRender
method know that we want our control not only do what a normal textbox does, but also have other features.
There are basically two things that we're going to do in the OnPreRender
method. The first thing is to add code that will let the method add a reference to our JavaScript file. And, the second this is adding calls to JavaScript functions on certain events. Please refer to Code Snippet 6.
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
ClientScriptManager scriptManager = this.Page.ClientScript;
string resourceFilePath = "MyCustomControls.Resources.NumericTextBox.js";
scriptManager.RegisterClientScriptInclude("NumericTextBoxScript",
scriptManager.GetWebResourceUrl(this.GetType(), resourceFilePath));
if (this.Type == TextBoxType.Decimal)
this.Attributes.Add("onkeydown",
string.Format("return CheckDecimal(this,'{0}','{1}', {2})",
NumberOfInteger, NumberOfFraction, _AllowNegative.ToString().ToLower()));
else if (this.Type == TextBoxType.Integer)
this.Attributes.Add("onkeydown", string.Format("return CheckInteger({0})",
_AllowNegative.ToString().ToLower()));
this.Attributes.Add("onkeyup", string.Format("return CheckNegative(this)",
_AllowNegative.ToString().ToLower()));
}
JavaScript Code
We're going to have three JavaScript functions. The first one, CheckInteger
, will be called on the keydown
event if the control is working as an integer textbox. The second, CheckDecimal
, will be called on the keydown
event when the control is working as a decimal textbox. Finally, the third function, CheckNegative
, will be called on the keyup
event no matter how the control is working.
CheckInteger
will simply check whether the ASCII code of the character pressed is within the allowed list. It will also make sure that there's no more than one dash (-) in the textbox.
The reason why this function is being called on the keydown
event, not the keyup
, is that the keydown
event is raised prior to setting the value of the textbox, so I still have a chance to terminate this action. Therefore, if the function returns false
on the keydown
event, the character won't be added to the textbox.
On the other hand, if the function returns false
on the keyup
event, nothing will change. However, the benefit of the keyup
event is that it allows me to read the value of the textbox, since it's raised after the value has been set.
function CheckInteger(allowNegative) {
if ((event.keyCode >= 48 && event.keyCode <= 57 && event.shiftKey == false) ||
(event.keyCode >= 96 && event.keyCode <= 105 && event.shiftKey == false) ||
(event.keyCode >= 37 && event.keyCode <= 40) ||
event.keyCode == 8 ||
event.keyCode == 9 ||
event.keyCode == 16 ||
event.keyCode == 17 ||
event.keyCode == 35 ||
event.keyCode == 36 ||
event.keyCode == 46)
return true;
else if (event.keyCode == 189 && allowNegative == true) {
if (sender.value.indexOf('-', 0) > -1)
return false;
else
return true;
}
else
return false;
}
CheckDecimal
is very similar to CheckInteger
. But they are different in certain things. CheckDecimal
will treat a numeric character in a different way than the other allowed keys. This is because when a number is pressed, we need to check if the number of integers and fractions is still within the limits.
In addition, it has a special treatment for the decimal point character. It shouldn't be the first character in the textbox, and it shouldn't appear more than once.
function CheckDecimal(sender, numberOfInteger, numberOfFrac, allowNegative) {
var valueArr;
if ((event.keyCode >= 37 && event.keyCode <= 40) ||
event.keyCode == 8 ||
event.keyCode == 9 ||
event.keyCode == 16 ||
event.keyCode == 17 ||
event.keyCode == 35 ||
event.keyCode == 36 ||
event.keyCode == 46)
return true;
else if (event.keyCode == 189 && allowNegative == true) {
if (sender.value.indexOf('-', 0) > -1)
return false;
else
return true;
}
valueArr = sender.value.split('.');
if (event.keyCode == 190) {
if (valueArr[0] != null && valueArr[1] == null)
return true;
else
return false;
}
if ((event.keyCode >= 48 && event.keyCode <= 57 && event.shiftKey == false) ||
(event.keyCode >= 96 && event.keyCode <= 105 && event.shiftKey == false)) {
if (valueArr[1] == null) {
if (valueArr[0].indexOf('-', 0) > -1)
numberOfInteger++;
if (valueArr[0].length <= numberOfInteger)
return true;
}
else {
if (valueArr[1].length <= numberOfFrac)
return true;
}
}
return false;
}
When the key is up, the CheckNegative
function will make sure that the dash/minus character is added to the left.
function CheckNegative(sender) {
if (event.keyCode == 189) {
if (sender.value.indexOf('-', 0) > 0)
sender.value = sender.value.replace('-', '');
}
}