Search code examples
asp.netvb.netuser-controlshtml-selectdynamic-usercontrols

Dynamic select element in dynamic user control data loss


After a few days of searching forums, reading docs, and meeting with teammates I have yet to find a solution to this issue.

I have a form in a VB.NET project that dynamically adds user controls with the postback from a click of an asp button. The reason there is a postback is because there are calculations happening between the user control and the main page.

There is a dropdown list that needed to have the <optgroup> tag. To do this, I wrote a list of data into an XML file with a structure that follows:

<AccountListData>
    <account>
        <group>OPTION GROUP NAME HERE</group>
        <value>OPTION VALUE/TEXT HERE</value>
    </account>
    <account>...</account>
</AccountListData>

The XML file is 1117 lines long. It wasn't fun to write out. But, the reason I did was to use it to create the <select> tag in the code behind and assign it to the innerHTML attribute of a div tag in the front end. It looks something like this:

Form.aspx

    <table>
      <tr>
        <td id="DateLabel" runat="server">Date:</td>
        <td id="DescriptionLabel" runat="server">Description:</td>
        <td id="AmountLabel" runat="server">Amount:</td>
        <td id="DropDownLabel" runat="server">Select:</td>
      </tr>
      <asp:Placeholder ID="ph1" runat="server"></asp:Placeholder>
    </table>
    <asp:Button ID="AddItemButton" runat="server" CausesValidation="false" Text="Add Item">
    <asp:Label ID="RowCountLabel" runat="server"></asp:Label>

Form.aspx.vb

    Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
      'Add/Removed User Control
      AddRemoveUserControl()
      ScriptManager.RegisterStartupScript(Me, Me.[GetType](), "CalculateCosts", "Javascript function();"), True)
    End Sub
    ...
    Public Function GetPostbackControlName(ByVal Page As Page) As String
      Dim ctrlName As String
      ctrlName = Page.Request.Params.Get("__EVENTTARGET")
      Return ctrlName
    End Function
    ...
    Private Sub AddRemoveUserControl()
      'Get and check the ID of what caused the postback
      Dim c As Control = GetPostbackControlName(Page)

      If Not IsNothing(c) Then
        If c.ID.ToString = "AddItemButton" Then
          RowCountLabel.Text = CInt(RowCountLabel.Text) + 1
        End If
      End If

      'Get ID for dynamic user controls
      Dim ControlID As Integer = 0
      For i As Integer = 1 To (CInt(RowCountLabel) - 1)
        Dim DynamicControl As ItemTable = LoadControl("usercontrols/ItemTable.ascx")
        DynamicUserControl.ID = "uc" & CStr(ControlID)

        'Add an event handler for the usercontrol
        AddHandler DynamicUserControl.RemoveUserControl, AddressOf Me.HandleRemoveUserControl

        'Add user control to placeholder on front end
        ph1.Controls.Add(DynamicUserControl)

        'Number the amount of user controls
        ControlID += 1
      Next
      NumberItemTable()
    End Sub
    ...
    Private Sub HandleRemoveUserControl(sender As Object, e As EventArgs)
      'Removes usercontrol from placeholder and lowers count #
    End Sub
    ...
    Private Sub NumberItemTable()
      'Gives a number value to a property in the usercontrol
    End Sub

Control.ascx

    <%@ Control Language="vb" AutoEventWireup="false" CodeBehind="ItemTable.ascx.vb" Inherits=".ItemTable" %>
<tr>
  <td>
    <asp:Textbox ID="Date" runat="server" CssClass="datepicker"></asp:Textbox></td>
  <td>
    <asp:Textbox ID="Description" runat="server"></asp:Textbox></td>
  <td>
    <asp:Textbox ID="Amount" runat="server"></asp:Textbox></td>
  <td>
    <div ID="DynamicSelect" runat="server"></div></td>
</tr>

Control.ascx.vb

Dim XMLInfo As XDocument

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
  FillDropDownList()
End Sub

Private Sub FillDrowDownList()
  'Get XML data
  Dim XMLData 'all the XML data is loaded as an XDocument here
  Dim temp As string
  'WHERE THE DROPDOWN IS DYNAMICALLY CREATED WITH XML DATA
  DynamicSelect.InnerHTML += "<select ID='" + Me.ID.ToString + "CreatedSelect'><option value='' selected disabled>- Select -</option>"
  For Each element As XElement In XMLData.Descendants("account")
    Dim group As String = element.<group>.Value.ToString
    Dim value As String = element.<value>.Value.ToString

    If group <> "" Then
      If group <> temp Then
        temp = group
        DynamicSelect.InnerHTML += "</optgroup>"
        DynamicSelect.InnerHTML += "<optgroup label='" + group + "'>"
      End If
    End If
    DynamicSelect.InnerHTML += "<option value='" + value + "'>" + value + "</option>"
  Next
  DynamicSelect.InnerHTML += "</select>"
End Sub

When a value is selected on the dynamic dropdown list, any postback clears that selection. Any insight into why that is happening and what I can do to solve this issue would be GREATLY appreciated.


Solution

  • With two days of no answers and a total of 27 views, I doubt I'll get an answer to this question any time soon. So, I'll share with any of you that are looking for a solution to this issue that I came up with last night.

    I realized that option groups are nothing but a list with a header, right? What is a header but some text that is just for display, and what is some text that is just for display when it comes to input? Disabled!

    With this, I decided I was going to alter my code a bit and display a disabled field for each header using the asp DropDownList rather than try to create a dynamic select in the code behind, foregoing the data loss issue and securing the status of the viewstate.

    My data structure stayed the same.

    data.xml

    <account>
      <group>Group 1</group>
      <value>Value 1</value>
    </account>
    

    My form methods stayed the same, and I'll just leave those above and not copy them down here again.

    Here is where the change lies.

    In my control, the front end changed very simply; replace the <div> with the <asp:DropDownList>.

    Control.ascx

    <%@ Control Language="vb" AutoEventWireup="false" CodeBehind="ItemTable.ascx.vb" Inherits=".ItemTable" %>
    <tr>
      <td>
        <asp:Textbox ID="Date" runat="server" CssClass="datepicker"></asp:Textbox></td>
      <td>
        <asp:Textbox ID="Description" runat="server"></asp:Textbox></td>
      <td>
        <asp:Textbox ID="Amount" runat="server"></asp:Textbox></td>
      <td>
        <div ID="DynamicSelect" runat="server"></div></td>
    </tr>
    

    My control code behind was changed to:

    Control.ascx.vb

    Imports System.Web.UI.WebControls
    

    ...

    Private Sub FillDrowDownList()
    'Get XML data
    Dim XMLData 'all the XML data is loaded as an XDocument here
    Dim temp As string = ""
    Dim i As Integer = 0
    'Check if the control already has the dropdown
    If DynamicSelect.Items.Count < 1 Then
      DynamicSelect.Items.Add(New ListItem("- Select -", ""))
      DynamicSelect.Items(i).Attributes.Add("selected", "true")
      DynamicSelect.Items(i).Attributes.Add("disabled", "true")
      i += 1
      'loop through XML data
      For Each element As XElement In XMLData.Descendants("account")
        Dim group As String = element.<group>.Value.ToString
        Dim value As String = element.<value>.Value.ToString
    
        If temp <> group Then
          temp = group
          DynamicSelect.Items.Add(New ListItem(group, ""))
          DynamicSelect.Items(i).Attributes.Add("disabled", "true")
          i += 1
        End If
    
        DynamicSelect.Items.Add(New ListItem(value, value))
    
        i += 1
      End If
    End Sub
    

    This new code produces a DropDownList with group headers! Rather than having the bold and italic headers, it just has a disabled header. Which you can always seek out with CSS selectors and style to your desire!

    I hope anyone who is still looking for a super simple solution to this issue will come across this and find it helpful.