Quantcast
Viewing all articles
Browse latest Browse all 10

Parent-Child DropDownList Controls In ASP.NET Web Forms (VB.NET)

Some time ago I promised a formspring anon I would do a tutorial on parent / child DropDownList controls in ASP.NET Web Forms. At long last, I’m delivering. (From here on out, I will use “DropDownList” and “select list” interchangeably.)

Parent-child DropDownList controls means you have a parent, or main / master select list. Based on whatever selection is made in that first DropDownList, a second, “child” or “detail” select list is populated with relevant results.For example, if we had a database of cars, we might have a parent (master) DropDownList of manufacturers — Ford, Chevy, Toyota, etc. — and populate the child DropDownList with models from the selected manufacturer. So, if someone chose Ford in the parent select list, the child select list would automatically populate with choices such as Fusion, Mustang, Explorer, F-150, etc.

I’ll be demonstrating two different ways to accomplish this task, as well as a few variations on the first: First, binding with only SqlDataSource controls, which is by far the easiest way to proceed and will suffice for about 90 percent of applications; second, using code behind and CompareValidator controls, to cover all possible roadblocks.

Some notes before we proceed:

  • I’ll use the ZIP Code database I’ve been using for a while as the back-end data. I also have a separate table of USPS state codes I’m using as the parent data, to help illustrate that you can use several tables / data sources to fuel this solution, provided the keys that relate the data make sense to each data store. In other words, you can use a lot of different sources for your data, so long as the data is relational.
  • I’m going to use SQL Server, stored procedures and a connection string stored in my web.config file to access my data. I recognize that some people prefer to use SQL statements / LINQ and the like, or can only use Access / MySQL or other data stores. I really urge you to always use stored procedures, as they are way safer than inline SQL statements. As far as alternative data stores go, that should be a easy a fix as switching your code to use to the relevant ADO.NET ODBC classes, rather than the SQL Client classes.
  • This solution should work for ASP.NET versions 2.0 forward.

Example 1: Using SqlDataSource Controls

Most ASP.NET Web developers know about the SqlDataSource control and how to databind a DropDownList with that control. What they may not know is that, via the ControlParameter class, we can use the values in any other ASP.NET control(s) — including other databound controls — on the page as query parameters.

All you need to do is indicate, as properties of the ControlParameter, the ID of the control that will provide the key value for your child query, the property of that control (e.g., selected value, text, etc.) which should serve as the key, and the variable name that should be used in that query.

In our example, I have a select list named ddlParent, which will contain a list of state codes I get from a database table. Another select list, named ddlChild, will bind to a SqlDataSource control (named sqlChildDDL) that uses the selected value in ddlParent to retrieve proper records.

In my case, the stored procedure that will get the child records expects a parameter named “StateInitials”, the value of which comes from ddlParent and serves as the key for selecting the proper child records. By setting the AutoPostBack property of the parent DropDownList to true, I ensure the child DropDownList is rebound with the proper records each time the selected index of the parent list is changed.

<asp:Label runat="server" ID="lblParent" Text="Select A State: " />
<!-- parent ddl -->
<asp:DropDownList runat="server" ID="ddlParent" DataSourceID="sqlParentDDL" DataValueField="StateCode" DataTextField="StateName" AutoPostBack="true" />
<!-- parent ddl is populated with a parameterless SQL query -->
<asp:SqlDataSource runat="server" ID="sqlParentDDL" SelectCommand="GetStates" SelectCommandType="StoredProcedure" ConnectionString="<%$ ConnectionStrings:MyConnection %>" />
<br />
<!-- child ddl -->
<asp:Label runat="server" ID="lblChild" Text="Select A Town: " />
<asp:DropDownList runat="server" ID="ddlChild" DataSourceID="sqlChildDDL" DataValueField="ZIPCode" DataTextField="CityName" />
<!-- child ddl gets its parameter value from the selected value of the parent ddl -->
<!-- 
    ControlParamter property values
    ControlID: The ASP.NET control that is providing the paramter's value
    Name: The parameter's name as it appears in your stored procedure / SQL query
    PropertyName: The property of the ASP.NET control that will supply the value to your parameter
-->
<asp:SqlDataSource runat="server" ID="sqlChildDDL" SelectCommand="GetZIPCodes" SelectCommandType="StoredProcedure" ConnectionString="<%$ ConnectionStrings:MyConnection %>">
    <SelectParameters>
        <asp:ControlParameter ControlID="ddlParent" Name="StateInitials" PropertyName="SelectedValue" />
    </SelectParameters>
</asp:SqlDataSource>
Update, 26 June 2018: I no longer offer demos of retired .NET Frameworks, or ASP.NET WebForms, at dougv.net.
Note that we can use any ASP.NET control on the page as a ControlParamter, and we can use any of that control’s properties as a parameter value, regardless of data type.

In other words, we could have a textbox on the page and use its Text property to let people type in free-text terms. (That’s a huge security risk, but it can be done.)

Or we could even use a static control with a non-text property, such as the Parent value of a Label control.

Of course, we need to exercise care here; any parameter value we send to a database query has to be the right data type / something the database store understands how to parse. The point is, ControlParamter is a powerful tool; respect it and it will open a number of possibilities.

Now that we have a parent-child select lists pair, how do we use that data to power some other control’s data? Simple: We just use the SelectedValue of the child DropDownList as the ControlParamter value for another SqlDataSource — in this example, it will bind to a GridView that gives the selected town’s ZIP Code, CityLatitude and CityLongitude.

All we need to do is add the GridView and its SqlDataSource, plus set the AutoPostBack property for ddlChild to true, so that the GridView’s values will change any time ddlChild’s selected index is changed.

<asp:Label runat="server" ID="lblParent" Text="Select A State: " />
<!-- parent ddl -->
<asp:DropDownList runat="server" ID="ddlParent" DataSourceID="sqlParentDDL" DataValueField="StateCode" DataTextField="StateName" AutoPostBack="true" />
<!-- parent ddl is populated with a parameterless SQL query -->
<asp:SqlDataSource runat="server" ID="sqlParentDDL" SelectCommand="GetStates" SelectCommandType="StoredProcedure" ConnectionString="<%$ ConnectionStrings:MyConnection %>" />
<br />
<!-- child ddl -->
<asp:Label runat="server" ID="lblChild" Text="Select A Town: " />
<asp:DropDownList runat="server" ID="ddlChild" DataSourceID="sqlChildDDL" DataValueField="ZIPCode" DataTextField="CityName" AutoPostBack="true" />
<!-- child ddl gets its parameter value from the selected value of the parent ddl -->
<!-- 
    ControlID: Name of the ASP.NET control that is providing the paramter's value
    Name: The name of the parameter in your stored procedure / SQL query
    PropertyName: The property of the control, specified in ControlID, that will supply the value to your query, specified in Name
-->
<asp:SqlDataSource runat="server" ID="sqlChildDDL" SelectCommand="GetZIPCodes" SelectCommandType="StoredProcedure" ConnectionString="<%$ ConnectionStrings:MyConnection %>">
    <SelectParameters>
        <asp:ControlParameter ControlID="ddlParent" Name="StateInitials" PropertyName="SelectedValue" />
    </SelectParameters>
</asp:SqlDataSource>
<br />
<br />

<!-- A GridView to show the resuls -->
<asp:GridView runat="server" ID="gvDetails" DataSourceID="sqlZipDetails" DefaultMode="ReadOnly" CellPadding="5" AutoGenerateColumns="false">
    <AlternatingRowStyle BackColor="LightGray" />
    <HeaderStyle BackColor="LightYellow" Font-Bold="True" HorizontalAlign="Center" />
    <Columns>
        <asp:BoundField HeaderText="City" DataField="CityName" />
        <asp:BoundField HeaderText="State" DataField="StateInitials" />
        <asp:BoundField HeaderText="ZIP Code" DataField="ZIPCode" />
        <asp:BoundField HeaderText="Latitide" DataField="CityLatitude" />
        <asp:BoundField HeaderText="Longitude" DataField="CityLongitude" />
    </Columns>
</asp:GridView>
<!-- the GridView gets records based on the SelectedValue of ddlChild -->
<asp:SqlDataSource runat="server" ID="sqlZipDetails" SelectCommand="GetCityDetails" SelectCommandType="StoredProcedure" ConnectionString="<%$ ConnectionStrings:MyConnection %>">
    <SelectParameters>
        <asp:ControlParameter ControlID="ddlChild" Name="ZIPCode" PropertyName="SelectedValue" />
    </SelectParameters>
</asp:SqlDataSource>
Update, 26 June 2018: I no longer offer demos of retired .NET Frameworks, or ASP.NET WebForms, at dougv.net.

Example 1B: A Little Code Behind To Clean Things Up

If you play with the demo above, you’ll notice a problem: Any time you change the selected ListItem in ddlParent, the ListItems in ddlChild are updated, but not the data in the details GridView.

In other words, if you select a new state from the parent select list, the town values in the child select will change to the appropriate entries, but the GridView retains the old data from the previously selected town. Only choosing a new town from the child select list will update the GridView.

That happens because although ddlChild has been populated with new ListItems, the SelectedValue of ddlChild hasn’t been changed. Changing the SelectedValue is an act that must take place within ddlChild, not ddlParent.

In other words, the event that triggered the page to post back was a change in ddlParent’s selected value, not ddlChild. Until we explicitly instruct ddlChild to update its selected value, either programmatically or by hand, as far as ASP.NET is concerned, it retains the last SelectedValue, regardless of the fact that its ListItems have changed.

This behavior is by design and as it should be. But it does make for an annoyance we need to address.

Fortunately, we can fix this with just a little codebehind; namely, an event handler that fires on ddlParent’s OnSelectedIndexChanged event. That handler will:

  • Rebind ddlChild to its SqlDataSource, which will now contain the correct town list.
  • Set the SelectedIndex of ddlChild to 0, or the first town in the new list.
  • Rebind the GridView’s SqlDataSource so it draws from the first town in the updated town list.
Sub ddlParent_selectedIndexChanged(Sender As Object, E As EventArgs) Handles ddlParent.SelectedIndexChanged
	ddlChild.DataBind()
	ddlChild.SelectedIndex = 0
	sqlZipDetails.DataBind()
End Sub
Update, 26 June 2018: I no longer offer demos of retired .NET Frameworks, or ASP.NET WebForms, at dougv.net.

This pretty much solves our data latency problem, but it adds duplicate queries to the process and thus is inefficient. We can do a better if we get more code-intensive.

Example 2: Using Code Behind And CompareValidator Controls

To streamline the process, reduce database queries and handle potentially bad inputs, we can use code behind to populate our DropDownLists and GridView, plus use CompareValidators to ensure we have legitimate data before sending our database queries.

We’ll bind records to a SqlDataReader, then use that reader to populate the DropDownLists and, where appropriate, the detail GridView. Additionally, we’ll use CompareValidators to ensure that the users have selected a valid option from each DropDownList.

First up, we need to create the DropDownLists and GridView. Notice that both DropDownLists begin with a single, default ListItem with a value of 0.

That’s because our valid options, once this list is populated, will never be zero; as a result, we can test with the CompareValidators whether the selected value of either DropDownList is 0. If it is, we know we have an invalid choice; otherwise, we expect the choice to be valid.

<asp:Label runat="server" ID="lblParent" Text="Select A State: " />
<asp:DropDownList runat="server" ID="ddlParent" AutoPostBack="true">
    <asp:ListItem Text="Select A State ..." Value="0" />
</asp:DropDownList>
<asp:CompareValidator runat="server" ID="cvParent" ControlToValidate="ddlParent" ValueToCompare="0" Operator="NotEqual" ErrorMessage="Please select a valid option" Display="Dynamic" />
<br />
<asp:Label runat="server" ID="lblChild" Text="Select A Town: " />
<asp:DropDownList runat="server" ID="ddlChild" AutoPostBack="true">
    <asp:ListItem Text="Select A State First" Value="0" />
</asp:DropDownList>
<asp:CompareValidator runat="server" ID="cvChild" ControlToValidate="ddlChild" ValueToCompare="0" Operator="NotEqual" ErrorMessage="Please select a valid option" Display="Dynamic" />
<br />
<br />
<asp:GridView runat="server" ID="gvDetails" CellPadding="5" AutoGenerateColumns="false">
    <AlternatingRowStyle BackColor="LightGray" />
    <HeaderStyle BackColor="LightYellow" Font-Bold="True" HorizontalAlign="Center" />
    <Columns>
        <asp:BoundField HeaderText="City" DataField="CityName" />
        <asp:BoundField HeaderText="State" DataField="StateInitials" />
        <asp:BoundField HeaderText="ZIP Code" DataField="ZIPCode" />
        <asp:BoundField HeaderText="Latitide" DataField="CityLatitude" />
        <asp:BoundField HeaderText="Longitude" DataField="CityLongitude" />
    </Columns>
</asp:GridView>

We have several subroutines needed to power this solution.

First, we need to initially populate the parent DropDownList; we’ll do that in the Page_Load subroutine, checking whether this page is a postback. (We don’t want to repopulate the parent select list on every page refresh; if we do, we won’t ever be able to change the child select list’s values, since the selected index of the parent DropDownList becomes 0, automatically, every time it is databound.

Sub Page_Load(Sender As Object, E As EventArgs) Handles Me.Load
	If Not Page.IsPostBack Then
		ddlParent_dataBind()
	End If
End Sub

The process of binding the parent DropDownList is just connecting to the database server, populating a SqlDataReader with records, binding that to the parent DropDownList and indicating which fields should be used for text and values of the resulting ListItems.

Sub ddlParent_dataBind()
	Dim objConn As New SqlConnection(ConfigurationManager.ConnectionStrings("MyConnection").ConnectionString)
	Dim objCmd As New SqlCommand("GetStates", objConn)
	objCmd.CommandType = CommandType.StoredProcedure

	Dim objReader As SqlDataReader

	objConn.Open()
	objReader = objCmd.ExecuteReader()

	If Not objReader.HasRows Then
		ddlParent.Items.Clear()
		ddlParent.Items.Add("No records found.")
		ddlParent.Enabled = False
	Else
		ddlChild.Enabled = True
		ddlParent.DataSource = objReader
		ddlParent.DataTextField = "StateName"
		ddlParent.DataValueField = "StateCode"
		ddlParent.DataBind()
		ddlParent.Items.Insert(0, New ListItem("Select A State ...", "0"))
	End If

	objConn.Close()
	objCmd.Dispose()
	objConn.Dispose()
End Sub

We bind data to the child select list in a similar way, except we’re going to get some sort of key value from the parent DropDownList. So we pass that in as a parameter.

Sub ddlChild_dataBind(strKey As String)
	ddlChild.Items.Clear()

	Dim objConn As New SqlConnection(ConfigurationManager.ConnectionStrings("MyConnection").ConnectionString)
	Dim objCmd As New SqlCommand("GetZIPCodes", objConn)
	objCmd.CommandType = CommandType.StoredProcedure

	objCmd.Parameters.Add(New SqlParameter("StateInitials", SqlDbType.Char, 2))
	objCmd.Parameters("StateInitials").Value = strKey

	Dim objReader As SqlDataReader

	objConn.Open()
	objReader = objCmd.ExecuteReader()

	If Not objReader.HasRows Then
		ddlChild.Items.Clear()
		ddlChild.Items.Insert(0, "Error getting towns list from database")
	Else
		ddlChild.Enabled = True
		ddlChild.DataSource = objReader
		ddlChild.DataTextField = "CityName"
		ddlChild.DataValueField = "ZIPCode"
		ddlChild.DataBind()
		ddlChild.Items.Insert(0, New ListItem("Select A Town ...", "0"))
	End If

	objConn.Close()
	objCmd.Dispose()
	objConn.Dispose()
End Sub

And binding the GridView data is pretty much more of the same.

Sub gvDetails_dataBind(strKey As String)
	Dim objConn As New SqlConnection(ConfigurationManager.ConnectionStrings("MyConnection").ConnectionString)
	Dim objCmd As New SqlCommand("GetCityDetails", objConn)
	objCmd.CommandType = CommandType.StoredProcedure

	objCmd.Parameters.Add(New SqlParameter("ZIPCode", SqlDbType.Char, 5))
	objCmd.Parameters("ZIPCode").Value = strKey

	Dim objReader As SqlDataReader

	objConn.Open()
	objReader = objCmd.ExecuteReader()

	gvDetails.DataSource = objReader
	gvDetails.DataBind()

	objConn.Close()
	objCmd.Dispose()
	objConn.Dispose()
End Sub

Note that in all this databinding, I am not checking for exceptions, such as failure to connect to the database, a problem with the stored procedures, etc.

As I have said before, what makes for workable error trapping on your end is impossible for me to know, which is why I am not including it here. You should always check for errors. Don’t trust your programs won’t break; expect them to break. How you do that is up to you, but you definitely should prepare for failure.

We’ve got one more non-event subroutine to consider before we get into the action: A way to reset the GridView when we don’t have a valid child DropDownList selection.

To do that, we’ll simply set the DataSource of the GridView to nothing (null), then bind it; when a GridView is bound to a null, it is thus empty and, absent having its EmptyDataTemplate or EmptyDataText properties set, simply doesn’t render.

(I recognize that using nulls in this way is lazy. An alternative is to toggle the visibility of the GridView so that it only shows when we know we have good data, but that’s wordier and yes, I am being kind of lazy here.)

Sub gvDetails_reset()
	gvDetails.DataSource = Nothing
	gvDetails.DataBind()
End Sub

OK, with the mechanical parts of our code out of the way, we can handle changes to the selected item indexes of both the parent and child lists.

In the case of a parent select list change, we always know that whatever information is currently in the detail GridView will be outdated. So we’ll reset it, and we’ll rebind the child DropDownList if the selection in the parent DropDownList is valid (which we check with the relevant CompareValidator).

Sub ddlParent_selectedIndexChanged(Sender As Object, E As EventArgs) Handles ddlParent.SelectedIndexChanged
	cvParent.Validate()
	If cvParent.IsValid Then
		ddlChild_dataBind(ddlParent.SelectedValue)
		ddlChild.SelectedIndex = 0
	End If
	gvDetails_reset()
End Sub

When the child DropDownList’s selected index is changed, one of two things are true: Either the selection isn’t valid, in which case we reset the detail GridView, or it’s time to populate the details GridView with a new town’s information.

Sub ddlChild_selectedIndexChanged(Sender As Object, E As EventArgs) Handles ddlChild.SelectedIndexChanged
	cvChild.Validate()
	If cvChild.IsValid Then
		gvDetails_dataBind(ddlChild.SelectedValue)
	Else
		gvDetails_reset()
	End If
End Sub

This code on github: https://github.com/dougvdotcom/aspnet_parent_child_dropdown


Viewing all articles
Browse latest Browse all 10

Trending Articles