Introduction
When you develop a Web Application form, very often, there is a need to create a combination of Country/State controls. Usually, it starts with a country drop down list. After selecting a particular country, it displays the drop down list of states for countries such as USA and/or Canada, or it displays a text box for free text for other countries. Some applications do not require the state for countries that do not have a list of states and thus, it should be a flag to display (or not) the textbox.
This trivial situation requires some coding for every page where you want to place these two controls. Of course, it can be done in many different ways: on the client-side using JavaScript, using AJAX, jQuery, etc...
For my applications, I wanted something really light and convenient, which would eliminate a lot of coding, is easy to implement, and allows ASP.NET native validations.
From the very beginning, I decided to reload the page when the country is changed, rather than process the states using JavaScript. This approach has pros and cons. A major con is that you have to reload a page, making an extra trip to the server. For a really busy page, this might be a problem because you have to keep all the data already inserted into the page controls. But this is what the ViewState is for, is it not? On the positive side, this approach does not require any JavaScript code.
So I wrote the list of requirements:
- There should be two controls: one for countries and one for states.
- The states control should have the ability to assign the corresponding countries control.
- Each control must be able to work independently of each other.
- Each control must have the default settings (country and state).
- Each control must have the ability to be validated by native validation controls.
- The states control should be displayed either as a drop down list, or a text box if the country does not have a predefined list of states.
- The states control must have a flag which indicates whether to display the text box, in situations where the country does not have any states.
Keeping this in mind, I decided to use the following XML document as a source of the countries/states, which I called "TheWorld.xml":
<world>
<countries>
<country>
<name>AFGHANISTAN</name>
<code>AFG</code>
<states />
<country>
<name>ALBANIA</name>
<code>ALB</code>
<states />
</country>
..............................................
<country>
<name>CAMEROON</name>
<code>CAM</code>
<states />
</country>
<country>
<name>CANADA</name>
<code>CAN</code>
<states>
<state>
<name>ALBERTA</name>
<code>AB</code>
</state>
<state>
<name>BRIT. COLUMBIA</name>
<code>BC</code>
</state>
<state>
<name>MANITOBA</name>
<code>MB</code>
</state>
<state>
<name>N.W. TERRITORY</name>
<code>NT</code>
</state>
<state>
<name>NEW BRUNSWICK</name>
<code>NB</code>
</state>
<state>
<name>NEWFOUNDLAND</name>
<code>NF</code>
</state>
<state>
<name>NOVA SCOTIA</name>
<code>NS</code>
</state>
<state>
<name>ONTARIO</name>
<code>ON</code>
</state>
<state>
<name>PR. EDWARD IS.</name>
<code>PE</code>
</state>
<state>
<name>QUEBEC</name>
<code>PQ</code>
</state>
<state>
<name>SASKATCHEWAN</name>
<code>SK</code>
</state>
<state>
<name>YUKON TERR.</name>
<code>YK</code>
</state>
</states>
</country>
<country>
<name>CAPE VERDE</name>
<code>CAP</code>
<states />
</country>
..............................................
</countries>
</world>
As you can see, the structure is very simple, and it allows you to easily maintain the list up to date. In my application, I compiled this file as a Web Resource. Usually I place all my custom controls in a single Class Library which I can include into any web project. Using any document (image, CSS file, JavaScript file, XML file, etc...) as a Web Resource allows you to place it into a compiled format and not care about moving it from application to application. Use this link to read more about Web Resources.
As I wrote before, I want to have the ability to assign the countries control to the states control via the states control's ID. This means that in my application, I should be able to find the control by its ID.
You know that the FindControl()
function looks only through the list of controls in a particular level. I would rather have the ability to look for a control by ID through all levels. That is why I found a very useful blog on the Internet.
I reused this code written by Steve Smith:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
namespace ControlLibrary
{
public static class ControlFinder
{
public static T FindControl<T>(Control startingControl,
string id) where T : Control
{
T found = startingControl.FindControl(id) as T;
if (found == null)
{
found = FindChildControl<T>(startingControl, id);
}
return found;
}
public static T FindChildControl<T>(Control startingControl,
string id) where T : Control
{
T found = null;
foreach (Control activeControl in startingControl.Controls)
{
found = activeControl as T;
if (found == null || (string.Compare(id, found.ID, true) != 0))
{
found = FindChildControl<T>(activeControl, id);
}
if (found != null)
{
break;
}
}
return found;
}
}
}
Everything has been prepared for the controls, and now we can start.
Countries Control
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml;
namespace ControlLibrary
{
public class CountryListControl : System.Web.UI.WebControls.DropDownList
{
public string DefaultCountry { get; set; }
protected override void OnLoad(EventArgs e)
{
if (!this.Page.IsPostBack)
{
string URLString = this.Page.ClientScript.GetWebResourceUrl(
typeof(CountryListControl),
"LOLv2.Resources.TheWorld.xml");
URLString = this.Page.Request.Url.Scheme + "://" +
this.Page.Request.Url.Authority + URLString;
XmlDocument doc = new XmlDocument();
doc.Load(URLString);
this.DataTextField = "InnerText";
this.DataSource = doc.SelectNodes("world/countries/country/name");
this.DataBind();
if (!string.IsNullOrEmpty(DefaultCountry))
this.SelectedValue = DefaultCountry;
base.OnPreRender(e);
}
}
}
}
The control inherits from DropDownList
. The control defines the DefaultCountry
property. It also has an OnLoad
event: When the page is being loaded for the first time, the XML document is created from the Web Resource. The DataSource
is set to the XmlNodeList
with the country names and is bound to the list. If the DefaultCountry
is assigned, it is set as the selected value.
The States Control
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Web;
using System.Xml;
using System.Web.UI.WebControls;
using System.Web.UI;
using LOLv2;
namespace ControlLibrary
{
public class StateListControl : DropDownList
{
public string CountryControl { get; set; }
public string Country { get; set; }
public bool DisplayEmptyTextBox { get; set; }
public string DefaultState;
private TextBox textBox;
private DropDownList countryDll;
private bool hasStates;
protected override void OnLoad(EventArgs e)
{
this.Visible = true;
}
protected override void OnPreRender(EventArgs e)
{
if (!string.IsNullOrEmpty(CountryControl))
{
countryDll = ControlFinder.FindControl<DropDownList>(
this.Page, CountryControl);
}
if (null != countryDll)
Country = countryDll.SelectedItem.Value;
else
Country = string.IsNullOrEmpty(Country) ? "UNITED STATES" : Country;
string URLString = this.Page.ClientScript.GetWebResourceUrl(
typeof(StateListControl), "LOLv2.Resources.TheWorld.xml");
URLString = this.Page.Request.Url.Scheme + "://" +
this.Page.Request.Url.Authority + URLString;
XmlDocument doc = new XmlDocument();
doc.Load(URLString);
XmlNodeList stateNames = doc.SelectNodes("world/countries/country[name=\"" +
Country + "\"]/states/state/name");
XmlNodeList stateCodes = doc.SelectNodes("world/countries/country[name=\"" +
Country + "\"]/states/state/code");
if (stateNames.Count == 0)
{
textBox = new TextBox();
hasStates = false;
}
else
{
hasStates = true;
this.Items.Clear();
for (int i = 0; i < stateNames.Count; i++)
{
ListItem li = new ListItem();
li.Text = stateNames[i].InnerText;
li.Value = stateCodes[i].InnerText;
this.Items.Add(li);
}
if (!string.IsNullOrEmpty(DefaultState))
this.SelectedValue = DefaultState;
else
if (null != this.Page.Request[this.ClientID])
try
{
this.SelectedValue = this.Page.Request[this.ClientID];
}
catch { }
}
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
if (null != textBox)
{
CopyProperties(this, textBox);
textBox.AutoPostBack = this.AutoPostBack;
textBox.CausesValidation = this.CausesValidation;
textBox.RenderControl(writer);
}
else
if (hasStates)
base.Render(writer);
}
private void CopyProperties(WebControl from, WebControl to)
{
to.ID = from.ID;
to.CssClass = from.CssClass;
ICollection keys = from.Attributes.Keys;
foreach (string key in keys)
{
to.Attributes.Add(key, from.Attributes[key]);
}
to.BackColor = from.BackColor;
to.BorderColor = from.BorderColor;
to.BorderStyle = from.BorderStyle;
to.BorderWidth = from.BorderWidth;
to.ForeColor = from.ForeColor;
to.Height = from.Height;
ICollection styles = from.Style.Keys;
foreach (string key in keys)
{
to.Style.Add(key, from.Style[key]);
}
to.ToolTip = from.ToolTip;
to.Visible = from.Visible;
to.Width = from.Width;
}
}
}
As you can see, the states control is more complicated.
The control inherits from DropDownList
. It has the following properties:
- The public string
CountryControl
, which is an assigned a country control ID. - The public string
Country
, which is the country name. - The public boolean
DisplayEmptyText
, which commands to display or not display the text box - when the country does not have states. - The public string
DefaultState
, which defines which state to set as the default. - The private
TextBox
object textbox
, which will be used for rendering a text box, rather than a drop down list, when the country does not have states. - The private
DropDownList
contryDll
, which is used as a place holder for the found countries control. - The private boolean
hasStates
, which is just a flag.
On the pageLoad
event, I set the visibility of the control to true
. This is done in order not to loose the control on the postback, if the control is not being displayed before the postback.
In the Prerender
event:
- Look for the countries control
- Define the country for this control
- Get the data from the Web Resource
- Populate either the existing drop down list, or create a new text box object
Using the Render Procedure
If a list of states exists, then render the drop down list, else render the text box. As you might have noticed, I use the CopyProperties
procedure to copy any possible property from the original control into the text box (if the text box is rendered).
The Result
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Default.aspx.cs" Inherits="CountryStateList.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@ Register Assembly="ControlLibrary"
Namespace="ControlLibrary" TagPrefix="cl1" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cl1:CountryListControl ID="ddlCountry"
runat="server" DefaultCountry="CANADA"
AutoPostBack="true">
</cl1:CountryListControl>
<br />
<cl1:StateListControl ID="ddlState" runat="server"
CountryControl="ddlCountry" CssClass="ffff"
AutoPostBack="true" DisplayEmptyTextBox="true">
</cl1:StateListControl>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
runat="server" ControlToValidate="ddlState"
ErrorMessage="*"></asp:RequiredFieldValidator>
<br />
<asp:Button ID="btnClick" runat="server" Text="Click" />
</div>
</form>
</body>
</html>
This is an example of the Default.aspx page. You have to register the assembly:
<%@ Register Assembly="ControlLibrary"
Namespace="ControlLibrary" TagPrefix="cl1" %>
I have placed the RequiredFieldValidator
to show that it works with the control.
You can see now how convenient and simple this method is to handle the tedious task of displaying the country/state functionality on your web page. You can download the source from the link at the top of this article. If you like the article, please vote.