Two-Way Databinding für Cascading DropDownList

15. Oktober 2010

“Das sollte doch einfach zu realisieren sein” war mein erster Gedanke, als die Anforderung kam, in ASP.NET zwei abhängige DropDownList Elemente in einer FormView zu implementieren.  

MSDN und Suchmaschinen liefern zudem eine hinreichende Anzahl von Beispielen zu diesem Thema. Die angebotenen Lösungen enthalten jedoch in der Regel eine Menge CodeBehind und unterstützen üblicherweise kein sauberes Two-Way Binding.

Im nachfolgenden werde ich eine Lösung mit manuellem Databinding sowie eine Lösung mit Two-Way Binding vorstellen. Beide Lösungen sind im ASP Page Lifecycle leicht nachvollziehbar und arbeiten mit einem geringen Teil an CodeBehind.

Und darum geht es:

Eine ASPX Seite besteht aus einem FormView, die einen Datensatz “Person” editiert. Der Datensatz wird durch eine DataSource bereitgestellt. In der Seite befinden sich zwei DropDownList Elemente, deren Auswahlwerte durch weitere DataSourcen bereitgestellt werden. Der Wertebereich der zweiten DropDownList “Detail-Level” ist von der Auswahl der ersten DropDownList “Master-Level” abhängig. Im Beispiel werden für den gewählten Master-Level “2” die Detail-Level “2-1” bis “2-9” angeboten.

DropDown

Die DropDowList wird mit einem Two-Way DataBinding angebunden. Das Property “SelectedValue” stellt die Verbindung her.

DropDownList Elemente

   1: <%-- gebundene DropDownList --%>

   2: <asp:DropDownList runat="server" ID="ddLevel1" AutoPostBack="true"

   3:  DataSourceId="dsLevel1"

   4:  DataTextField="Level1Name"

   5:  DataValueField="Level1Id"

   6:  SelectedValue='<%#Bind("Level1Id") 

   7:  %>'

   8:  />

   9:  

  10: <%-- gebundene DropDownList --%>

  11: <asp:DropDownList runat="server" ID="ddLevel2"

  12:  DataSourceId="dsLevel2"

  13:  DataTextField="Level2Name"

  14:  DataValueField="Level2Id"

  15:  SelectedValue='<%#Bind("Level2Id") %>'

  16:  />

Die Abhängigkeiten der DropDownList Elemente bilde ich über die DataSource ab. Diese nimmt den gewählten Wert aus dem Master-Level DropDownList mit der ID “ddLevel1” und nutzt ihn für die Datenabfrage.

DataSource Detail-Level

   1: <%-- Datenquelle DropDownList 2 in Abhängigkeit der DropDownList 1 --%>

   2: <asp:ObjectDataSource ...

   3:      SelectMethod="GetLevel2">

   4:     <SelectParameters>

5: <asp:ControlParameter Name="level1" ControlID="ddLevel1" PropertyName="SelectedValue" Type="Int32" />

   6:     </SelectParameters>

   7:  </asp:ObjectDataSource>

Leider führt diese Implementierung zu einer sehr missverständlichen Fehlermeldung.

InvalidOperationException

 

Realisierung Cascading DropDownList in einer FormView per manuellem Binding

Bei manuellem Binding der DropDown Elemente werden die SelectedValue nicht mehr gebunden. D.h. nachfolgender Code entfällt für die beiden DropDownList Elemente.

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

Ab jetzt muß das FormView die Verwaltung des Binding übernehmen. Bei einem Update werden die Parameter aus den DropDownList der Form bereitgestellt. Beim Databinding werden die SelecteValues gesetzt.

CodeBehind FormView

   1: protected void FormOnItemUpdating(object sender, FormViewUpdateEventArgs e)

   2: {

   3:     // Die Werte der beiden DropDownListen werden als Parameter der Datenquelle übergeben.

   4:     // Alle mit <%#Bind(...)%> versehenen Properties sind schon in den NewValues vorhanden.

   5:     var formView = sender as FormView;

   6:     var dd1 = formView.FindControl("ddLevel1") as DropDownList;

   7:     var dd2 = formView.FindControl("ddLevel2") as DropDownList;

   8:     e.NewValues["Level1Id"] = dd1.SelectedValue;

   9:     e.NewValues["Level2Id"] = dd2.SelectedValue;

  10: }

  11:  

  12: protected void FormOnDataBound(object sender, EventArgs e)

  13: {

  14:     // Nachdem das FormView gebunden wurde (d.h. alle Eval und Bind) werden manuelle die Werte der

  15:     // DropDownListen gebunden

  16:     var formView = sender as FormView;

  17:     var dd1 = formView.FindControl("ddLevel1") as DropDownList;

  18:     var dd2 = formView.FindControl("ddLevel2") as DropDownList;

  19:  

  20:     var aktuellerDatensatz = formView.DataItem as Person;

  21:     dd1.SelectedValue = aktuellerDatensatz.Level1Id.ToString();

  22:  

  23:     // Soeben wurde DropDown1 geändert. DropDown 2 muss neu geladen werden damit der selektierte Wert

  24:     // zur Verfügung steht.

  25:     dd2.DataBind();

  26:     // Jetzt kann der zweite Wert sicher gesetzt werden

  27:     dd2.SelectedValue = aktuellerDatensatz.Level2Id.ToString();

  28: }

Für die abhängige DropDownList muss zusätzlich (Zeile 26) ein DataBind aufgerufen werden, damit der zugehörige SelectedValue auch zur Verfügung steht.

 

Realisierung Cascading DropDownList in einer FormView per Two-Way Binding

Die Lösung aus dem manuellem Binding bildet auch die Grundlage für eine saubere Two-Way Binding Lösung. Nachdem das Master-DropDown Element gebunden wurde, muss die zweite DropDown manuell gebunden werden.

Der CodeBehind besteht nur noch aus einer Implementierung für DataBound der Master-DropDown

CodeBehind Master-DropDown

   1: protected void DropDown1DataBound(object sender, EventArgs e)

   2: {

   3:     // Wenn DropDown1 sein Wert ändert muß DropDown2 neu gebunden werden

   4:     var dd1 = sender as DropDownList;

   5:     var dd2 = dd1.NamingContainer.FindControl("ddLevel2");

   6:     dd2.DataBind();

   7: }

Fertig. Mehr muss nicht programmiert werden. Die ASPX Seite sieht wie nachfolgend aus:

Vollständiger Code der ASPX-Seite

   1: <body>

   2:     <form id="form1" runat="server" >

   3:     <div>

   4:     

   5:     <%-- Bearbeitung einer Person durch FormView --%>

   6:     <asp:FormView runat="server" 

   7:         ID="ctlForm" DataSourceID="dsPerson" DefaultMode="Edit" 

   8:         >

   9:         

  10:         <EditItemTemplate>

  11:             <%-- gebundenes Property --%>    

  12:             <asp:TextBox runat="server" ID="txtName" Text='<%#Bind("Name") %>'></asp:TextBox>

  13:     

  14:             <%-- ungebundene DropDownList --%>

  15:             <asp:DropDownList runat="server" ID="ddLevel1" AutoPostBack="true"

  16:              DataSourceId="dsLevel1"

  17:              DataTextField="Level1Name"

  18:              DataValueField="Level1Id"

  19:              SelectedValue='<%#Bind("Level1Id") %>'

  20:              OnDataBound="DropDown1DataBound"

  21:              />

  22:              

  23:             <%-- ungebundene DropDownList --%>

  24:             <asp:DropDownList runat="server" ID="ddLevel2"

  25:              DataSourceId="dsLevel2"

  26:              DataTextField="Level2Name"

  27:              DataValueField="Level2Id"

  28:              SelectedValue='<%#Bind("Level2Id") %>'

  29:              />

  30:  

  31:             <%-- Datenquelle DropDownList 1 --%>

  32:             <asp:ObjectDataSource 

  33:                 ID="dsLevel1" runat="server" TypeName="WebControls.MyDataSource"

  34:                 DataObjectTypeName="WebControls.Level1"

  35:                 SelectMethod="GetLevel1"

  36:                 />

  37:     

  38:             <%-- Datenquelle DropDownList 2 in Abhängigkeit der DropDownList 1 --%>

  39:             <asp:ObjectDataSource 

  40:                 ID="dsLevel2" runat="server" TypeName="WebControls.MyDataSource"

  41:                 DataObjectTypeName="WebControls.Level2"

  42:                 SelectMethod="GetLevel2">

  43:                   <SelectParameters>

  44:                     <asp:ControlParameter Name="level1" ControlID="ddLevel1"  PropertyName="SelectedValue" Type="Int32" />

  45:                   </SelectParameters>

  46:                 </asp:ObjectDataSource>

  47:  

  48:             <%-- CommandName="Update" ruft Update() auf der ObjectDataSource auf --%>

  49:             <asp:Button runat="server" ID="btnSend" Text="Senden" CommandName="Update" />

  50:         </EditItemTemplate>

  51:  

  52:     </asp:FormView>   

  53:  

  54:     <%-- Datenquelle der FormView --%> 

  55:     <asp:ObjectDataSource 

  56:         ID="dsPerson" runat="server" TypeName="WebControls.MyDataSource"

  57:         DataObjectTypeName="WebControls.Person"

  58:         SelectMethod="SelectPerson"

  59:         UpdateMethod="UpdatePerson"

  60:         >

  61:      </asp:ObjectDataSource>

  62:         

  63:     </div>

  64:     </form>

  65:  

  66: </body>

Fazit

Es gibt eine einfache Lösung für Two-Way DataBinding von Cascading DropDownList in einer FormView.

Diese besteht aus zwei exakt abgestimmten Funktionsblöcken. Zum einen muß die Detail-DataSource in Abhängigkeit des Master-DropDown Controls gesetzt werden. Zum anderen muß der DataBound Event der Master-DropDown die Detail-DropDown neu binden.

Jeder kleine Fehler in der Programmierung führt zu z.T. irreführenden Fehlermeldungen mitten aus dem Databinding heraus. Der StackTrace enthält meist kein kein “eigener” Code. Die auftretenden Fehlermeldungen sind:

  • “Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control”
  • ‘DropDownList’ has a SelectedValue which is invalid because it does not exist in the list of items