2
votes

I have a ComboBox I am using in a WPF form that appears to be showing odd behavior, where text is cleared immediately from the ComboBox after selecting a dropdown item, then typing text into the ComboBox.

First for some background information, my ComboBox contains a list of ComboBoxItems, with the Content attributes set to a variety of string values (in my sample application, I am using fruit names). The ComboBox itself has all properties set to default, except for the IsEditable property (set to true) and the IsTextSearchEnabled property (set to false).

<ComboBox x:Name="comboBox" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Margin="121,100,0,0" IsEditable="True" KeyUp="comboBox_KeyUp" IsTextSearchEnabled="False">
    <ComboBoxItem Content="apple"/>
    <ComboBoxItem Content="banana"/>
    <ComboBoxItem Content="grape"/>
    <ComboBoxItem Content="lemon"/>
    <ComboBoxItem Content="strawberry"/>
</ComboBox>

I am also using a basic filter that is triggered during the combobox's KeyUp event:

private void comboBox_KeyUp(object sender, KeyEventArgs e)
{
    var combobox = (ComboBox)sender;
    combobox.IsDropDownOpen = true;

    CollectionView itemsViewOriginal = (CollectionView)CollectionViewSource.GetDefaultView(combobox.Items);
    itemsViewOriginal.Filter = ((o) =>
    {
        if (String.IsNullOrEmpty(combobox.Text))
        {
            return true;
        }
        else
        {
            if (((ComboBoxItem)o).Content.ToString().StartsWith(combobox.Text, true, null))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    });

    itemsViewOriginal.Refresh();
}

The issue occurs when I select an item from the dropdown list, click into the combobox to remove the highlight and put focus at the end of the word, then type. As soon as I enter a single letter, the text is removed immediately, and any new text I type essentially restarts the filter. I would expect the new text to simply append to the existing text in the combobox, but that is not the case here.

Interestingly, I can workaround the issue in 2 ways:

a) Re-enable "IsTextSearchEnabled." However, I do not want this property enabled for a variety of reasons, mainly being the behavior not being consistent with how the client expects the control to behave.

b) Modify the filter to use a static value.

If I modify the filter from being:

if (((ComboBoxItem)o).Content.ToString().StartsWith(combobox.Text, true, null))

to:

if (((ComboBoxItem)o).Content.ToString().StartsWith("ba", true, null))

then the clearing text behavior no longer occurs. However, this obviously does not help me since I want to use the ComboBox's text as the filter for the list.

What can I do to keep the text from being removed from the control in this scenario, while still maintaining the filtering and general control behavior?

1
Filtering the Items collection is asking for trouble. ComboBox attempts to synchronize the selected item with the Text property. When you filter Items such that it's effectively empty, the combo box apparently thinks, "Oh, there's no possible selection, so my current selection cannot possibly be valid. I'll go ahead and clear it out, along with my text." ComboBox probably isn't the best control to use for this scenario. I'd suggest you look for a third-party text control with auto completion that you can use instead. - Mike Strobel
Can I ask why you're not using any bindings? WPF is most commonly used with the MVVM pattern which would also solve a lot of similar problems - Oceans

1 Answers

2
votes

I don't really understand why you can't use the built-in search function. For the posted example IsTextSearchEnabled="True" together with StaysOpenOnEdit="True" should get the required result. But I guess you have your reasons and probably didn't want to share your exact code.

The first time you focus the ComboBox and start typing nothing is selected and you simply filter the list of items. So everything works perfectly until the combobox loses focus or actually commits his selected values.
When ever you select an item, the ComboBox uses the SelectedItem property and binds it to the selected ComboBoxItem. If the text then get's changed and no longer matches that of the currently selected item, the combobox removes the selected item, in reality setting it to nothing and thus clearing the text. This can be avoided by declaring the SelectedValue to be the actual text property of the ComboBox. To have the dropdown-list still affect the text correctly you must also define the SelectedValuePath property.

<ComboBox x:Name="comboBox" HorizontalAlignment="Left" SelectedValuePath="Content" SelectedValue="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Text}" VerticalAlignment="Top" Width="120" Margin="121,100,0,0"  IsEditable="True" KeyUp="comboBox_KeyUp" IsTextSearchEnabled="False">
    <ComboBoxItem Content="apple"/>
    <ComboBoxItem Content="banana"/>
    <ComboBoxItem Content="grape"/>
    <ComboBoxItem Content="lemon"/>
    <ComboBoxItem Content="strawberry"/>
</ComboBox>

Unfortunately this alone doesn't seem to be enough. This seems to solve your exact problem that you mentioned, so when more text gets added at the end of the current selected value, then you will get the desired result. But when the ComboBox gets cleared by backspace, then for the first letter with a single match, the ComboBox still seems to think it is done with picking and selects the entire text, the next key press will then of course remove what you typed before.
I was however able to solve this by placing the cursor at the last index, which undo's the selection and should now work exactly as intended.

private void comboBox_KeyUp(object sender, KeyEventArgs e)
{
    var combobox = (ComboBox)sender;
    var ctb = combobox.Template.FindName("PART_EditableTextBox", combobox) as TextBox;
    if (ctb == null) return;
    var caretPos = ctb.CaretIndex;
    combobox.IsDropDownOpen = true;

    CollectionView itemsViewOriginal = (CollectionView)CollectionViewSource.GetDefaultView(combobox.Items);
    itemsViewOriginal.Filter = ((o) =>
    {
        if (String.IsNullOrEmpty(combobox.Text))
        {
            return true;
        }
        else
        {
            if (((ComboBoxItem)o).Content.ToString().StartsWith(combobox.Text, true, null))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    });

    itemsViewOriginal.Refresh();
    ctb.CaretIndex = caretPos;
}

Hopefully this helps you out with your current issue, I'd just like to add that there are way better solutions for something like this. It does involve actual model binding which is something you should definitely look into if you haven't already.

Here is an example of a custom filtered ComboBox for WPF that has a lot more re usability than what you're currently making.
Building a Filtered ComboBox for WPF


Update: I forgot to check for modifier keys like Ctrl, Shift or Alt

private void comboBox_KeyUp(object sender, KeyEventArgs e)
{
    var combobox = (ComboBox)sender;
    var ctb = combobox.Template.FindName("PART_EditableTextBox", combobox) as TextBox;
    if (ctb == null) return;
    if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) || Keyboard.Modifiers.HasFlag(ModifierKeys.Control) || Keyboard.Modifiers.HasFlag(ModifierKeys.Alt))
    return;
    var caretPos = ctb.CaretIndex;
    combobox.IsDropDownOpen = true;

    CollectionView itemsViewOriginal = (CollectionView)CollectionViewSource.GetDefaultView(combobox.Items);
    itemsViewOriginal.Filter = ((o) =>
    {
        if (String.IsNullOrEmpty(combobox.Text))
        {
            return true;
        }
        else
        {
            if (((ComboBoxItem)o).Content.ToString().StartsWith(combobox.Text, true, null))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    });

    itemsViewOriginal.Refresh();
    ctb.CaretIndex = caretPos;
}