Search code examples
c#winforms

Calling Items.Clear() causes a single recursive call to OnCreateControl()?


My class inherits from ComboBox, and it's having a weird issue.

The following code ends up populating the ComboBox dropdown list twice! That is, it ends up with a list twice as long as expected, and includes the expected list of items twice (despite my use of Items.Clear).

protected override void OnCreateControl()
{
    base.OnCreateControl();
    PopulatePayees();
}

// Populate list items
private void PopulatePayees()
{
    Document document = Program.GetDocument();
    Items.Clear();
    foreach (ListItem listItem in document.Payees.GetListItems())
        Items.Add(listItem);
}

Stepping through with the debugger, I see that as soon as I execute Items.Clear() in PopulatePayees(), it steps into OnCreateControl() a second time, which then calls PopulatePayees() a second time.

If I comment out Items.Clear(), it only populates the list once!

Somehow, executing Items.Clear() is causing a single recursive call into OnCreateControl().

Does anyone have any ideas about what might be happening here?

Call Stack:

BankAccounts.dll!BankAccounts.Controls.PayeeComboBox.PopulatePayees() Line 25   C#  
BankAccounts.dll!BankAccounts.Controls.PayeeComboBox.OnCreateControl() Line 19  C# [External Code]  
BankAccounts.dll!BankAccounts.Controls.PayeeComboBox.PopulatePayees() Line 25   C#  
BankAccounts.dll!BankAccounts.Controls.PayeeComboBox.OnCreateControl() Line 19  C# [External Code]  
BankAccounts.dll!BankAccounts.MainForm.AddTransaction_Click(object sender, System.EventArgs e) Line 191 C#  
BankAccounts.dll!BankAccounts.MainForm.Transactions_AddTransaction(object sender, System.EventArgs e) Line 235 C#  
BankAccounts.dll!BankAccounts.Controls.TransactionListBox.AddTransactionMenu_Click(object sender, System.EventArgs e) Line 153 C# [External Code]  
BankAccounts.dll!BankAccounts.Program.Main() Line 16 C#

Solution

  • The custom ComboBox generates a collection of objects, then adds these objects to the Items collection of the ComboBox.
    This operation is performed both as Design-Time and while the Controls is being created at Run-Time. At Design-Time, the Items content is then serialized (either in the designer.cs file or the Resources file, doesn't matter)

    The issue described - the duplication of the content of the Items collection - arises when the AutoCompleteMode and AutoCompleteSource properties are set in the Designer (the latter, given the result, is probably set to AutoCompleteSource.ListItems).

    When AutoComplete is enabled, calling the Items.Clear() method forces the Owner of the ObjectCollection to recreate its Handle (the relevant method call there is _owner.SetAutoComplete(false, true /*recreateHandle*/)).
    This, in turn, generates a call to both OnHandleCreated() and OnCreateControl(). This happens right after Items.Clear() is called, and resumes the operations on the line of code that comes next. Here, the procedure that adds the new items.
    Since the Control is recreated immediately - this while the Form itself is creating its Controls in the meanwhile - you end up executing the foreach loop twice in a row.

    You can define this as a classic race condition.

    You have different ways to solve this. A couple that are simple (and hopefully hassle-free) to implement:

    • If the collection of items is not needed at Design-Time, you can test DesignMode in OnCreateControl() or OnHandleCreated() (the former is probably a better fit in this context, using the latter may generate another race condition related to the Items state), avoid calling PopulatePayees() when DesignMode = true
    • Otherwise, instead of calling Items.Clear() (causing the underlaying code to recreate the Control's handle in this context), we can clear the content using the RemoveAt() method

    So, in OnCreateControl(), just comment out the line of code that doesn't provide the most desired outcome:

    protected override void OnCreateControl()
    {
        base.OnCreateControl();
        // Test DesignMode if the items should not be visible (and serialized) at Design-Time
        // if (!DesignMode) PopulatePayees();
    
        // Otherwise just call the method
        PopulatePayees();
    }
    
    
    private void PopulatePayees()
    {
        // Instead of calling Items.Clear(), remove the items one by one manually
        // Required because this method can be called from somewhere else
        for (int i = Items.Count - 1; i >= 0; i--) {
            Items.RemoveAt(i);
        }
    
        Document document = Program.GetDocument();
    
        foreach (ListItem listItem in document.Payees.GetListItems())
            Items.Add(listItem);
    }