In my previous blog entry I gave an overview of the advantages of using ASP.Net web user controls to create generic, reusable
subassemblies that can form the basis of your own library of web application
building blocks. In this post I will walk you step-by-step through a real
world example.
Creating our Visual Studio Project
To follow along, first make sure you have the AJAX Control Toolkit installed for your version of Visual Studio. If you’re using Visual Studio 2008 then go to the ASP.Net AJAX CodePlex site to download the latest version. If you’re still using Visual Studio 2005 then you’ll need to create a new AJAX Control Toolkit Website, which should appear under My Templates after you have installed the AJAX Control Toolkit for ASP.NET 2.0.
After you get the toolkit then you need to add it to the Visual Studio toolbox so that you can drop and drop the components into your web page using the designer. MBrock.com has a helpful blog entry that walks you through the process.
Open Visual Studio 2008 and create a net ASP.NET Web Site
named AJAXsubassemblyExample. Lets keep our web user controls separate from
the other parts of the site by creating a folder for them called “UIControls.”
Building the Web User Control
Right-click the UIControls folder and select Add New Item.
Select Web User Control and name it NumericData.
The first item we’ll add will to the web user control will
be text box. Drag a textbox from the Standard tab and change the ID to txtNumericData.
Next, add a Masked Edit Extender. Set TargetControlID="txtNumericData",
MaskType="Number" and Mask="999999999999".
12 digits will be our default; later we’ll add some properties for minimum and maximum values and then programmatically adjust the mask to match the property settings.
We want to support a public property named, “IsRequired” that can control whether our numeric data subassembly will be a required field on the page. Add a RequiredFieldValidator from under the Validation tab, make ControlToValidate="txtNumericData"
and Enabled="false".
This makes the field not required by default. We’ll add a property later that will allow the user to set this.
Finally, we’ll add a RangeValidator, again setting ControlToValidate="txtNumericData"
and Enabled="false".
The source for UIControls/NumericData.ascx will look like this:
<%@ Control
Language="C#"
AutoEventWireup="true"
CodeFile="NumericData.ascx.cs"
Inherits="UIControls_NumericData"
%>
<%@ Register
Assembly="AjaxControlToolkit"
Namespace="AjaxControlToolkit"
TagPrefix="cc1"
%>
<asp:TextBox ID="txtNumericData" runat="server"></asp:TextBox>
<cc1:MaskedEditExtender ID="MaskedEditExtender1" runat="server"
TargetControlID="txtNumericData"
MaskType="Number" Mask="999999999999">
</cc1:MaskedEditExtender>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server"
ControlToValidate="txtNumericData" Enabled="false"
ErrorMessage="RequiredFieldValidator">
</asp:RequiredFieldValidator>
<asp:RangeValidator ID="RangeValidator1" runat="server"
ControlToValidate="txtNumericData" Enabled="false"
ErrorMessage="RangeValidator">
</asp:RangeValidator>
We now have all the components for numeric data subassembly
in one neat little package.
Defining the Public Properties
Text Box Pass-Through Properties
The web user control will appear as a “black box” to the web
pages that are using it. The only properties available are those of the
UserControl class and any public properties that we define. Since the text
box, txtNumericData,
is the main UI element then we should allow users to get and set some of its
properties. We’ll start with a short list and add more as needed:
Height
Width
Font and Colors
Borders
CSS Class
For each property that we want to expose we can define a
public property in the NumericData.ascx.cs code file and tie to the text box’s
property. Here’s how you would define the Height property:
public
Unit Height
{
get { return
txtNumericData.Height; }
set { txtNumericData.Height = value; }
}
There’s really not much code at all; we’re just tying the
property of the web user control directly to the property of the text box. The
property type is Unit, which is the same data type as the text box Height
property. Any errors that occur when setting the property would get thrown
through to the code that’s doing the setting, so it behaves the same as a
regular text box.
The rest of the properties from our list above are done in
the same way. Fonts, colors, and borders are actually controlled by many
properties so I will spare you the tedium of going through each one and let you
look through the source code below.
using System.Web.UI.WebControls;
using System.Drawing;
...
#region TextBox Properties
public
Unit Height
{
get { return
txtNumericData.Height; }
set { txtNumericData.Height = value; }
}
public
Unit Width
{
get { return
txtNumericData.Width; }
set { txtNumericData.Width = value;
}
}
public
Color BackColor
{
get { return
txtNumericData.BackColor; }
set { txtNumericData.BackColor = value; }
}
public
Color BorderColor
{
get { return
txtNumericData.BorderColor; }
set { txtNumericData.BorderColor = value; }
}
public
BorderStyle BorderStyle
{
get { return
txtNumericData.BorderStyle; }
set { txtNumericData.BorderStyle = value; }
}
public
Unit BorderWidth
{
get { return
txtNumericData.BorderWidth; }
set { txtNumericData.BorderWidth = value; }
}
public
String CssClass
{
get { return
txtNumericData.CssClass; }
set { txtNumericData.CssClass = value; }
}
public
FontInfo Font
{
get { return
txtNumericData.Font; }
}
public
CssStyleCollection Style
{
get { return
txtNumericData.Style; }
}
public
Color ForeColor
{
get { return
txtNumericData.ForeColor; }
set { txtNumericData.ForeColor = value; }
}
public
int MaxLength
{
get { return
txtNumericData.MaxLength; }
set { txtNumericData.MaxLength = value; }
}
#endregion
The Value Property
I did not add Text to the list of properties because we want
this control to be specialized and handle only numeric data. Therefore we’ll
have a Value property instead which will handle validation and conversion. We
want the control to handle either numeric or integer data so we should make
Value type a double so that it can handle all types of numbers.
Here’s the code for getting the value:
public
double Value
{
get
{
try { return Double.Parse(txtNumericData.Text); }
catch (Exception
ex)
{
throw
new Exception("Invalid numeric data in control " + ID,
ex);
}
}
We have a try-catch and inside the try we use the Parse
method of the double type to parse whatever is in the text into a double. If
the Masked Edit Extender is doing its job then this parsing will work. If
something goes wrong we throw the exception, adding a message which will
identify to the programmer the ID of the control that encountered the error.
The set function is similar:
set
{
try { txtNumericData.Text = value.ToString();
}
catch (Exception
ex)
{
throw
new Exception("Invalid numeric data in control " + ID,
ex);
}
}
Because the property type is double then the try-catch should
never fail because the error checking would happen in the code trying to assign
a value to this property of type double. However, it doesn’t hurt to have the
error handling just in case.
Value as Int
The requirements also specify that we be able to limit the
values entered to integers if the situation calls for it. It would then make sense
to have a property that provides the value as an integer. This becomes a
little complicated because what if you access the integer value when the
control is configured to accept real numbers? We have to choose whether to
truncate, round or throw an exception. Let’s try to be as user-friendly as
possible. The Int.Parse() method will throw an exception if you give it a
string that includes a decimal point. What we can do is start with
Int.Parse(), then try to truncate if that fails:
public
int ValueInt
{
get
{
try
{
return int.Parse(txtNumericData.Text);
}
catch
{
return (int)Math.Truncate(Value);
}
}
set
{
try { txtNumericData.Text = value.ToString();
}
catch (Exception
ex)
{
throw new Exception("Invalid
numeric data in control " + ID, ex);
}
}
}
Notice that in the catch of the get we call the Value
property. This lets us reuse the error handling built into the get of that
property. If the text in the numeric text box can be converted to a double then the
Math.Truncate method will get the integer portion for us.
The set ends up being the same as that for the Value
property, since the data type of ValueInt is int, then any errors would occur
in the calling code and we merely need to convert the int value to a string if we
get this far.
Is Required Property
Is Required allows the user to turn the required field
validator on or off. It’s off by default because that’s the way we set it in
the control designer. We can tie the property directly to the Enabled property
of the validator:
public
bool IsRequired
{
get { return
RequiredFieldValidator1.Enabled; }
set { RequiredFieldValidator1.Enabled = value; }
}
Range Validation
We want our numeric data control to support range validation.
We added a Range Validator to our control and can deal with the range validation in a similar way, exposing the minimum and maximum values as properties. One variation that we
can add is changing the type. On a standard range validator the minimum and
maximum values are strings. This is because the validators can be used for numbers,
text, dates, and currency. Since our control is specialized for numbers only
then we can make the minimum and maximum values of type double. Furthermore,
since we want to make the values optional then we will make our property types
nullable doubles:
public
double? MinimumValue
{
get
{
if (String.IsNullOrEmpty(RangeValidator1.MinimumValue))
return null;
else return double.Parse(RangeValidator1.MinimumValue);
}
set
{
if (value == null)
RangeValidator1.MinimumValue = String.Empty;
else RangeValidator1.MinimumValue = value.ToString();
SetRangeValidationEnabled();
}
}
public
double? MaximumValu
{
get
{
if (String.IsNullOrEmpty(RangeValidator1.MaximumValue))
return null;
else return double.Parse(RangeValidator1.MaximumValue);
}
set
{
if (value == null)
RangeValidator1.MaximumValue = String.Empty;
else RangeValidator1.MaximumValue = value.ToString();
SetRangeValidationEnabled();
}
}
private
void SetRangeValidationEnabled()
{
if (MinimumValue == null
&
MaximumValue == null)
RequiredFieldValidator1.Enabled = false;
else
RequiredFieldValidator1.Enabled = true;
}
The last feature to add is determining whether the validator
will need to be enabled. If both values are null then we can turn it off.
Since the code is the same for both properties I moved it into a help function SetRangeValidationEnabled.
Number Format Properties
The next two properties will determine how the number gets
formatted: DecimalPlaces and ShowCommas. These properties will be used when
creating the input mask. The rule for DecimalPlaces is that it can’t be
negative. ShowCommas does not require any special logic so we’ll take
advantage of the new automatic properties feature in C# 3:
int
_decimalPlaces = 0;
public
int DecimalPlaces
{
get { return
_decimalPlaces; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("decimal places can not be less than 0.");
else
_decimalPlaces = value;
}
}
public
bool ShowCommas { get;
set; }
Setting the Mask
Now we have enough groundwork done that we can determine the
mask for the masked edit extender. The factors that go into the mask are:
- Number of decimal places
- Show commas
- The magnitude of the minimum and maximum values
The mask will consist of nines, since this is a numeric
control, combined with commas and the negative sign. The first thing we need
to determine is the number of digits left of the decimal place. For this we
can use the logarithm of the maximum and minimum values, rounding up:
int maxNumDigits;
maxNumDigits = (int)Math.Ceiling(Math.Log10((double)MaximumValue));
Note that because MaximumValue is a nullable double (double?) we need to cast it to a double within the Log10 function. We also should
have code that deals with the null situation and sets the maximum digits to some default number of digits, maybe 18 so that we can handle up to a quadrillion. If we need anything more we just set the maximum value higher. We also have to allow for the possibility that the minimum value could all accept
more digits than the maximum. Here’s what that part of the code would look like:
const
int DEFAULT_MAX_DIGITS = 18
private
void SetMask()
{
int maxNumDigits;
if (MaximumValue == null
MinimumValue == null)
maxNumDigits =
DEFAULT_MAX_DIGITS;
else
{
maxNumDigits = Math.Max((int)Math.Ceiling(Math.Log10((double)MinimumValue)),
(int)Math.Ceiling(Math.Log10((double)MinimumValue)));
}
Testing the Control
In my next post I will demonstrate how to create a custom web user control test page that will let you play with all the different properties and provide examples on how to use your component.
Summary
This post demonstrates how you can combine several AJAX extenders with other web UI elements into a nice, reusable package. You can use the code-behind of your web user controls to expose the properties, do error handling and set defaults.
You can have a highly-functional generic control which will work well simply by adding it to a page. Through the various properties the developer can customize it to his or her desires making your control very flexible as well.