Tuesday, January 6, 2009

Using Web User Controls to Create Rich Internet Application Subassembies - Part 2

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.