The other day I was playing around with how to do cascading drop-down lists using only declarative markup. But as I said then, that works only if you have a straightforward scenario, as in two standalone DropDownList controls just somewhere on the page.
Another common situation is having the drop-down lists inside the template of a data-bound control. I showed an example of an all-declarative drop-down list in a FormView control a while back, but that was just a single drop-down inside the template.
What about cascading drop-down lists inside a template? It can be done, but alas, here we must abandon our effort to use only markup. About pure declarative code, the awesome Polita says:This specific scenario doesn’t quite work correctly in a FormView control because when the DataSource raises its DataSourceChanged event, the second dropdown list’s Eval statement attempts to evaluate against the container’s (FormView’s) databinding context, which is only valid at DataBind time. Since this happens outside of DataBind time, the context isn’t there and the Eval fails. The best way to work around this is to write some code to manually databind either the second (dependent) dropdown list.
Dang. As Polita told me later, "There are lots of ways to do this." So here's one.
I'll again use the Cars.mdb file. The extremely simplistic schema is this:
Customers CustomerID Name Manufacturer Model | Manufacturers Manufacturers | Models ModelID Manufacturer Model |
We cheat a bit here by not using proper unique IDs and stuff. Bear with me, that's not the interesting part. Here's what we want to accomplish, using a FormView control:
For my example, the FormView control is bound to a data source control for the Customers table; a typical tutorial-type of scenario. The data source control has a Select and an Update command so that we can view and edit data in the FormView control:<asp:AccessDataSource
ID="CustomersDataSource"
runat="server"
DataFile="~/App_Data/cars.mdb"
SelectCommand="SELECT [CustomerID], [Name], [Manufacturer],
[Model] FROM [Customers]"
UpdateCommand="UPDATE [Customers] SET [Name] = ?,
[Manufacturer] = ?, [Model] = ? WHERE [CustomerID] = ?">
<UpdateParameters>
<asp:Parameter Name="Name" Type="String" />
<asp:Parameter Name="Manufacturer" Type="String" />
<asp:Parameter Name="Model" Type="String" />
<asp:Parameter Name="CustomerID" Type="Int32" />
</UpdateParameters>
</asp:AccessDataSource>
Note: Lines are wrapped so they'll fit here.
In the FormView control, the ItemTemplate (read-only) uses Label controls bound to fields in the Customer record. Here's an excerpt:Name:
<asp:Label ID="NameLabel" runat="server" Text='<%# Eval("Name") %>' />
<br />
Manufacturer:
<asp:Label ID="ManufacturerLabel" runat="server" Text='<%# Eval("Manufacturer") %>' />
What we want is cascading drop-down lists in the EditItemTemplate, as we had in the original, all-declarative FormView example. As in that example, we need data source controls inside the template that the drop-down lists can bind to. The drop-down list for Manufacturer can use declarative markup to bind to the data source control and set its SelectedValue property:Manufacturer:
<asp:DropDownList
ID="DropDownList1"
runat="server"
AutoPostBack="True"
DataTextField="Manufacturer"
DataValueField="Manufacturer"
SelectedValue='<%# Bind("Manufacturer") %>'
DataSourceID="ManufacturersDataSource"
OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged" />
<asp:AccessDataSource
ID="ManufacturersDataSource"
runat="server"
DataFile="~/App_Data/cars.mdb"
SelectCommand="SELECT Manufacturer FROM Manufacturer ORDER BY Manufacturer" />
Note that the AutoPostBack property is set to true; more on that in a sec.
It's the second (dependent) drop-down list (Model) where declarative code falls down. As Polita says, there are various ways to handle this. What I did was to go as far as I could declaratively, which meant adding the DropDownList control and a data source control, as with the first drop-down list. However, I did not attempt to set the second DropDownList control's SelectedValue property (which is the property that would normally use Bind or Eval for data binding.) The Select query for the second data source control is parameterized (where Manufacturer = ?
), and there's a <parameter> element, but no value is assigned (yet) to the parameter:Model:
<asp:DropDownList ID="DropDownList2" runat="server"
DataSourceID="ModelsDataSource"
DataTextField="Model" DataValueField="Model" />
<asp:AccessDataSource ID="ModelsDataSource" runat="server"
DataFile="~/App_Data/cars.mdb"
SelectCommand="SELECT * FROM [Models] where Manufacturer = ?">
<SelectParameters>
<asp:Parameter Name="Manufacturer" />
</SelectParameters>
</asp:AccessDataSource>
We really need code for three tasks:- Dynamically get the parameter value for the second data source control (from the selected value of the first drop-down list) and then bind the second drop-down list to that data source.
- Pass the selected value of the second drop-down list to the FormView control when it's doing an update. (Normally this would be auto-magically handled by a declarative Bind call, but we're not using that for this control).
- Update the items in the second drop-down list when the first one changes -- the second list, after all, is dependent on the first. This is why we needed to set AutoPostBack to true for the first drop-down list -- so that it will raise an event we can handle to repopulate the second drop-down list.
Here are the corresponding bits of code. First, the code to dynamically get the parameter value and bind the second drop-down list:Protected Sub FormView1_DataBound(ByVal sender As Object, _
ByVal e As System.EventArgs)
If FormView1.CurrentMode = FormViewMode.Edit Then
Dim dv As System.Data.DataRowView = FormView1.DataItem
Dim ddl2 As DropDownList = FormView1.FindControl("DropDownList2")
Dim dsc As AccessDataSource = FormView1.FindControl("ModelsDataSource")
dsc.SelectParameters("Manufacturer").DefaultValue = dv("Manufacturer")
ddl2.DataBind()
If Not IsDBNull(dv("model")) Then
ddl2.SelectedValue = dv("Model")
End If
End If
End Sub
This is the code to pass the current selection to the update code:Protected Sub FormView1_ItemUpdating(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.FormViewUpdateEventArgs)
Dim ddl2 As DropDownList = FormView1.FindControl("DropDownList2")
e.NewValues("Model") = ddl2.SelectedValue
End Sub
And finally, the code to repopulate the second list when the first one changes:Protected Sub DropDownList1_SelectedIndexChanged(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim dsc As AccessDataSource = FormView1.FindControl("ModelsDataSource")
Dim ddl1 As DropDownList = FormView1.FindControl("DropDownList1")
Dim ddl2 As DropDownList = FormView1.FindControl("DropDownList2")
dsc.SelectParameters(0).DefaultValue = ddl1.SelectedValue
ddl2.DataBind()
End Sub
I wouldn't be surprised to hear that there is a more efficient way to do this, but this seems to work.
You can see the code in action here. And since these little snippets are way out of context, you can see the source code for the entire page here.