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 2 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 eliminates a lot of coding, easy to implement, and allows asp.net native validations.

From the very beginning I decided to reload the page when the country has been changed, rather then 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 it 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:
  1. There should be two controls: one for the countries and one for the states (state).
  2. States control should have the ability to assign the corresponding countries control.
  3. Each control must be able to work independently from each other.
  4. Each control must have the default settings (Country and State).
  5. Each control must have the ability to be validated by the native validation controls. 
  6. 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.
  7. The states control must have the flag which indicates if to display the text box, in the situation when 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 see the structure is very simple, but it allows you easily maintain the list up to date.

At my application I compiled this file as a Web Resource.
Usually I place all my custom controls into 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 the compiled format and not care about moving it from application to application.

Use the following link to read more about the Web Resources:

As I wrote before I want to create the ability to assign the countries control to the states control via the states control 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 of a particular level.
I would rather have the ability to look for a control by ID through all the levels.
That is why I found a very useful blog on the Internet.

I reused the 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
    {

        /// <summary>
        /// Similar to Control.FindControl, but recurses through child controls.
        /// </summary>
        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;
        }


        /// <summary>     
        /// Similar to Control.FindControl, but recurses through child controls.
        /// Assumes that startingControl is NOT the control you are searching for.
        /// </summary>
        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;
        }
    }
}

All has been prepared for the controls and 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.
OnLoad event:
When the page is being loaded for the first time, the xml document is being created from the Web Resource.
The DataSource is set to the XmlNodeList with countries names and is bounded 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 see the states control is more complicated.

The control inherits from the DropDownList.
It has the following properties:

  1. The public string CountryControl, which is an assigned country Control ID.
  2. The public string Country, which is the country name
  3. The public Boolean DisplayEmptyText, which commands to display or not to display the text box - when the country does not have states.
  4. The public string DefaultState, which defines which state to set as a default.
  5. 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 the state.
  6. The private DropDownList contryDll, which is used as a place holder for the found countries control
  7. The private bool hasStates, which is just a flag.

On 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 a control is not being displayed before the PostBack.

On Prerender event:
  1. Look for the countries control
  2. Define the Country for this control
  3. Get the data from the Web Resource
  4. Populate either the existing drop down list, or create a new text box object

Using Render procedure:

If a list of states exists, then render the drop down list, or else render the text box.
As you noticed, I used 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 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.
If you like the article, please vote.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架