Synchronized DropDownLists

Exploring ASP.NET & Web Development

Synchronized DropDownLists

By Don Kiely

Sometimes it amazes me just how hard some stuff is in ASP.NET Web Forms. For a technology that makes so many things so easy, too much is still just too dang hard.

I had a recent example of one of the hard things. I had a requirement in a website for entering demographic information for a customer. (This is for a back-end database-maintenance application, not a public-facing site.) Although the vast majority of our customers are in the United States and Canada, we have a growing list of international customers and need flexibility in how the people who maintain the data enter address information. What really made this hard is that the container is a ListView, so the lists are in both the EditItemTemplate and the InsertItemTemplate.

The theoretically simple solution was to have a country drop-down list with a synchronized State/Province/Territory/Region drop-down list that displays only the country subdivisions. As a failsafe, we also have Other Country and Other Region text boxes in case our lists are not complete. (And they aren't, but we have all the ones we normally need.)

I quickly found that having a bound drop-down list (bound both for the contents of the list and the current selection) synchronized to another drop-down list is not an easily supported scenario in ASP.NET ListViews. You can find exhaustive discussions online about all the many issues, but it is fairly hard to find the solution. Which isn't pretty, but here it is.

First bind the Country drop-down list (or the parent in your scenario) to the database table with the list of countries. In my solution, I used a SqlDataSource control, but you can bind it in code, of course. Then bind your drop-down list to the SqlDataSource, and the SelectedValue property to the item data that the ListView is bound to. Here's my code:

<td>

<asp:DropDownList ID="CountryDropDownList"

DataSourceID="CountrySqlDataSource"

DataValueField="CountryID"

DataTextField="Country"

runat="server"

SelectedValue='<%# Bind("CountryID") %>'

OnSelectedIndexChanged="CountryIndexChanged"

AutoPostBack="true"

AppendDataBoundItems="true">

<asp:ListItem Value="" Text="" />

</asp:DropDownList>&nbsp;

<asp:SqlDataSource ID="CountrySqlDataSource" runat="server"

ConnectionString="<%$ ConnectionStrings:MyConnectionString %>"

SelectCommand="SELECT [CountryID], [Country], [SortOrder] FROM Lookup.Country ORDER BY [SortOrder], Country">

</asp:SqlDataSource>

<asp:TextBox ID="OtherCountryTextBox" runat="server" Text='<%# Bind("OtherCountry") %>' MaxLength="40"></asp:TextBox>

</td>

</tr>

Nothing special or hard there, so far.

Now it's time for the RegionDropDownList control, which is synchronized to the country list. It, too, is bound to a SqlDataSource for the list of regions, with the SelectCommand using a @CountryID parameter. But that parameter is not bound to the CountryDropDownList because of all the bad things that happen when you try to bind them together. Instead, the SqlDataSource has a CountryID parameter which, as you'll see soon, is changed in code.

Another interesting thing is that the drop-down list's SelectedValue property can't be bound to the RegionID field of the item data of the ListView. This is because there seems to be no way to bind the list of regions before that selected value gets bound. If you try, you'll end up with an error message that the value you are binding SelectedValue to doesn't exist in the list.

The last interesting thing is that the RegionDropDownList doesn't have a blank ListItem like the Country did. The problem here is that it is lost when you rebind the list to the regions in the newly selected country. So instead, the SELECT statement in the SqlDataSource does a UNION ALL SELECT NULL, NULL, NULL so that the blank item is always there. Here's the HTML:

<td>

<asp:DropDownList ID="RegionDropDownList"

DataSourceID="RegionSqlDataSource"

DataValueField="RegionID"

DataTextField="Region"

runat="server"

AppendDataBoundItems="false">

<asp:ListItem Value="" Text="" />

</asp:DropDownList>&nbsp;

<asp:SqlDataSource ID="RegionSqlDataSource" runat="server"

ConnectionString="<%$ ConnectionStrings:MyConnectionString %>"

SelectCommand="SELECT [RegionID], [RegionCode], [Region] FROM Lookup.Region WHERE CountryID = @CountryID UNION ALL SELECT NULL, NULL, NULL ORDER BY [Region]">

<SelectParameters>

<asp:Parameter Name="CountryID" DefaultValue="1" />

</SelectParameters>

<FilterParameters>

<asp:ControlParameter Name="CountryID" ControlID="CountryDropDownList" PropertyName="SelectedValue" />

</FilterParameters>

</asp:SqlDataSource>

<asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("OtherRegion") %>' MaxLength="40"></asp:TextBox>

</td>

And now for the code. First, you'll notice in the HTML for the CountryDropDownList, I set the OnSelectedIndexChanged attribute to CountryIndexChanged. This is where the code sets the value of the CountryID parameter of the RegionDropDownList's SqlDataSource control. Then it rebinds the list. This causes the region list to be populated with the selected country's regions, if any.

protected void CountryIndexChanged(object sender, EventArgs e)

{

DropDownList ddlCountry = (DropDownList)sender;

DropDownList ddlRegion = (DropDownList)ddlCountry.Parent.FindControl("RegionDropDownList");

SqlDataSource source = (SqlDataSource)ddlCountry.Parent.FindControl("RegionSqlDataSource");

source.SelectParameters["CountryID"].DefaultValue = ddlCountry.SelectedValue;

ddlRegion.Items.Clear();

//ddlRegion.Items.Add("");

ddlRegion.DataBind();

}

There are two more pieces of code you need. Remember that we couldn't bind the RegionID value in the item data bound to the parent ListView. That means that you have to do it in code.

The ItemDataBound event of the ListView has to do two things. First it needs to set the initial value of the CountryID parameter for the region's drop-down list. This duplicates some of the work of the CountryIndexChanged method earlier, but there it was being set when the user changed the selected country. Here it has to set the value for the initial view of the edit or insert template.

Second, it gets the RegionID value of the ListView's data item and sets the region drop-down list's SelectedValue property. Here's the code:

protected void ContactsListView_ItemDataBound(object sender, ListViewItemEventArgs e)

{

if (ContactsListView.EditIndex >= 0)

{

ListViewDataItem item = (ListViewDataItem)e.Item;

DataRowView row = (DataRowView)item.DataItem;

DropDownList ddlRegion = (DropDownList)item.FindControl("RegionDropDownList");

SqlDataSource source = (SqlDataSource)item.FindControl("RegionSqlDataSource");

source.SelectParameters["CountryID"].DefaultValue = row["CountryID"].ToString();

ddlRegion.DataBind();

if (!Convert.IsDBNull(row["RegionID"]))

{

ddlRegion.SelectedValue = row["RegionID"].ToString();

}

}

}

You also need to reverse the process and read the ID of the selected region when the user saves the data. Here I used the ItemUpdating event to read the selected value of the region drop-down list and set the new value of the RegionID field of the item data, using the NewValues property of the ListViewUpdateEventArgs object.

protected void ContactsListView_ItemUpdating(object sender, ListViewUpdateEventArgs e)

{

DropDownList ddl = (DropDownList)ContactsListView.EditItem.FindControl("RegionDropDownList");

e.NewValues["RegionID"] = ddl.SelectedValue;

}

Whew! That's a lot of work for what seems like it should be a simple thing! There are a few variations you can use to accomplish all this, but the basic idea remains the same.

This obviously cries out to be a custom control to encapsulate all the code and the design, and that's the next step I'll take. And either solution could be AJAXified, even to the point of using ASP.NET AJAX 4.0 to easily do everything on the client.

Or I may get deeper into ASP.NET MVC, since that is the shiny new technology that overcomes all ASP.NET warts!

Don Kiely ([email protected]), MVP, MCSD, is a senior technology consultant, building custom applications and providing business and technology consulting services. His development work involves tools such as SQL Server, Visual Basic, C#, ASP.NET, and Microsoft Office.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish