Why does my webform not detect the contents of my listbox until OnSaveStateComplete? How can I detect them earlier?

I have an ASP.NET webform with two dropdownlists and a listbox cascading from each other in this order:

Department (ddl) -> Job Title (ddl) -> Security Template (listbox)

On load, the Department list is populated. Select a department, and the Job Title DDL is populated with jobs that match that department. Likewise, once you select a Job Title option, then the Security Template listbox populates with matching security templates available for that job title.

The UI seems to be working correctly in that, as soon as I pick a job title, the security templates appear in the listbox. However, I know they’re only appearing near the very end of the page lifecycle due to the output of a few Debug.WriteLines; for a job title that has four security templates, those four templates appear in the listbox when I select said job title on my page, but my Output window (debugging in VS 2019) shows only one entry (the default filler entry before it is populated with real options, “Select a Job Title”).

I wrote some additional Debug.WriteLines and created functions for each page lifecycle state, and they didn’t fire off with the correct amount until OnSaveStateComplete(), which is too late in the page life cycle to do anything based off the values in the listbox.

What’s causing the values to “show up” to my webform so late, and how can I change this so they appear earlier in the life cycle where I can do stuff with them?

In other words, as soon as the listbox populates with options (e.g. as soon as I pick a Job Title), I want to be able to do stuff based (conditionally) upon the new contents of the listbox.

If I set autopostback="true" on the listbox, then naturally it updates and shows the right count in the Output window when I select a listbox item, but I need it to detect the right count when the Job Title is selected, not when a Security Template is selected, because I’m relying on the Security Template being populated for another conditional event to occur (depending on the contents of the Security Templates that appear in the listbox).

It seems odd to me that the second DDL works as I expect it to upon selecting something in the first DDL, but the listbox doesn’t work the same way when I select something in the second DDL. I am chalking this up to not understanding the ASP.NET page life cycle well enough.

I’m using C# 7.3 and targeting .NET Framework 4.6.1, if that matters.

Form.aspx:

<asp:Label ID="lblDeptList1" AssociatedControlID="ddlDeptList1" runat="server" Text="Employee's Department: ">
</asp:Label>
<asp:DropDownList ID="ddlDeptList1" runat="server" 
    DataSourceID="SqlDeptList1" 
    DataTextField="DepartmentName" 
    DataValueField="Department" 
    OnDataBound="ddlDeptList1_DataBound" 
    OnSelectedIndexChanged="ddlDeptList1_SelectedIndexChanged" 
    AutoPostBack="True" >
</asp:DropDownList>
<asp:SqlDataSource ID="SqlDeptList1" runat="server" 
    ConnectionString="<%$ ConnectionStrings:ConnectionString1 %>" 
    SelectCommand="SELECT [Department], [DepartmentName] FROM [Departments] ORDER BY [DepartmentName]">
</asp:SqlDataSource>

<asp:Label ID="lblJobTitle1" AssociatedControlID="ddlJobTitle1" runat="server" Text="Job Title: ">
</asp:Label>
<asp:DropDownList ID="ddlJobTitle1" runat="server" 
    DataSourceID="SqlJobList1" 
    DataTextField="JobTitle" 
    DataValueField="JobCode" 
    OnDataBound="ddlJobTitle1_DataBound" 
    OnSelectedIndexChanged="ddlJobTitle1_SelectedIndexChanged" 
    AutoPostBack="True">
</asp:DropDownList>
<asp:SqlDataSource ID="SqlJobList1" runat="server" 
    ConnectionString="<%$ ConnectionStrings:ConnectionString1 %>">
</asp:SqlDataSource>

<asp:Label ID="lblSecurityTemplates1" AssociatedControlID="lstSecurityTemplates1" runat="server" Text="Select Security Template(s): ">
</asp:Label>
<asp:ListBox ID="lstSecurityTemplates1"
    SelectionMode="Multiple" runat="server" 
    DataSourceID="SqlSecurityList1" 
    DataTextField="Template" 
    DataValueField="Template" 
    OnDataBound="lstSecurityTemplates1_DataBound">
</asp:ListBox>
<asp:SqlDataSource ID="SqlSecurityList1" runat="server" 
    ConnectionString="<%$ ConnectionStrings:ConnectionString1 %>">
</asp:SqlDataSource>

Form.aspx.cs relevant bits:

public partial class NewForm : Page {

    protected void ddlDeptList1_DataBound(object sender,EventArgs e) {
        ddlDeptList1.Items.Insert(0,new ListItem("Choose Department",""));
    }

    protected void ddlJobTitle1_DataBound(object sender,EventArgs e) {
        //using this to avoid the Job Title list from having two "Choose Job Title" entries if a user selects a department and then selects "Choose Department" 
        if (!(ddlJobTitle1.Items.Count == 1 && ddlJobTitle1.Items[0].Text == "Choose Job Title")) {
            ddlJobTitle1.Items.Insert(0,new ListItem("Choose Job Title",""));
        }
    }

    protected void lstSecurityTemplates1_DataBound(object sender,EventArgs e) {
        if (!Page.IsPostBack) {
            lstSecurityTemplates1.Items.Insert(0,new ListItem("Choose a Department first",""));
        }
    }

    protected void ddlDeptList1_SelectedIndexChanged(object sender,EventArgs e) {
        SqlJobList1.SelectParameters.Clear();

        if (ddlDeptList1.SelectedIndex == 0) {
            ddlJobTitle1.Items.Clear();
            ddlJobTitle1.Items.Insert(0,new ListItem("Choose Job Title",""));
            //we want to clear the Security Templates listbox when the department changes, not just when the job title changes.
            lstSecurityTemplates1.Items.Clear();
            lstSecurityTemplates1.Items.Insert(0,new ListItem("Choose a Department first",""));
        }
        else {
            SqlJobList1.SelectParameters.Add("chosenDepartment",ddlDeptList1.SelectedItem.Value);
            SqlJobList1.SelectCommand = "SELECT JobCode, Department, JobTitle FROM JobTitles WHERE Department = @chosenDepartment ORDER BY JobTitle ASC";
            lstSecurityTemplates1.Items.Clear();
            lstSecurityTemplates1.Items.Insert(0,new ListItem("Choose a Job Title first",""));
        }
    }

    protected void ddlJobTitle1_SelectedIndexChanged(object sender,EventArgs e) {
        SqlSecurityList1.SelectParameters.Clear();

        if (ddlJobTitle1.SelectedItem.Text == "Choose Job Title") {
            lstSecurityTemplates1.Items.Clear();
            lstSecurityTemplates1.Items.Insert(0,new ListItem("Choose a Job Title first",""));
        }
        else {
            SqlSecurityList1.SelectParameters.Add("chosenJobTitle",ddlJobTitle1.SelectedItem.Text);
            SqlSecurityList1.SelectParameters.Add("chosenDepartment",ddlDeptList1.SelectedValue);
            SqlSecurityList1.SelectCommand = "SELECT DISTINCT SecurityTemplate + ' | ' + SecurityTemplateName As Template FROM SecurityTemplateDB stdb INNER JOIN JobTitles jt ON jt.JobCode = stdb.JobCode WHERE jt.JobTitle = @chosenJobTitle AND jt.Department = @chosenDepartment GROUP BY stdb.SecurityTemplate, stdb.SecurityTemplateName ORDER BY Template";
            //SqlSecurityList1.DataBind(); - I tried adding this here, but it doesn't seem to make a difference.
            
            //these are the debug statements that should report 4 options when 4 are shown, but instead are reporting 1 option (the default option) when 4 are shown.
            Debug.WriteLine("Security List Item Count:");
            Debug.WriteLine(lstSecurityTemplates1.Items.Count);

        }
    }
}

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

The reason you get the wrong number of results in your code behind is rather simple: you check for the number of items BEFORE there are items in the lstSecurityTemplates1, because the actual databinding of items takes place in the .aspx, not the .aspx.cs (the code behind). This causes the code binding to happen later in the life cycle.

To fix this, you’ll need to make the following changes:

  1. Remove or comment out all the SqlSecurityList1 code from your else condition in ddlJobTitle1_SelectedIndexChanged.
  2. Remove the existing DataSourceID="SqlSecurityList" parameter from your lstSecurityTemplates1 listbox in the .aspx page.
  3. Add a new SqlConnection and SqlDataSource in the ddlJobTitle1_SelectedIndexChanged event in code behind:
    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString1"].ConnectionString);
    SqlDataSource source = new SqlDataSource();
    source.ConnectionString = conn.ConnectionString;
    source.SelectParameters.Add("chosenJobTitle",ddlJobTitle1.SelectedItem.Text);
    source.SelectParameters.Add("chosenDepartment",ddlDeptList1.SelectedValue);
    source.SelectCommand = "SELECT DISTINCT SecurityTemplate + ' | ' + SecurityTemplateName As Template FROM SecurityTemplateDB stdb INNER JOIN JobTitles jt ON jt.JobCode = stdb.JobCode WHERE jt.JobTitle = @chosenJobTitle AND jt.Department = @chosenDepartment GROUP BY stdb.SecurityTemplate, stdb.SecurityTemplateName ORDER BY Template";
    
    lstSecurityTemplates1.DataSource = source;
    lstSecurityTemplates1.DataBind();
    
    Response.Write("Items in lstSecurityTemplates1 = " + lstSecurityTemplates1.Items.Count);
    

The response will now show the correct number for the job title you just selected as you expect, rather than the amount previously in the templates listbox.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x