WPF editable ComboBox and its weird bug

This is a double-purpose post: a simple hack around the WPF ComboBox, and a subtle bug in it.
The short video explains both the problems way better than my words could do.

Most of the times, I use the ComboBox as a simple drop-down list. That works perfectly and that’s why I never bumped against the problems around.
This time I needed a real ComboBox, that is the ability to select from a drop-down list as always, but with the editable box as well. More precisely, I wanted the following behaviors:

  1. the user can drop the list and select any item (that’s the usual feature)
  2. the user can type in an existent item or a new one
  3. as the user type in the selection box, the drop-down should open
  4. (future) the drop-down list should be filtered upon the actual text typed in the selection box

The first two points are already available in the standard ComboBox control, while the fourth is just a wish for the (near) future. The problems arose with the third point, because the simplest yet intuitive way I used yielded a nice exception!

The base for the test.

Nothing fancy here: just a collection of objects, very simple. The classic “Person” with an “ID” (why didn’t I named so?):

    public class MyItem
    {
        public string Code { get; set; }
        public string Description { get; set; }

        public override string ToString()
        {
            return this.Code;
        }
    }

The data-set generation looks as follows:

    public partial class App : Application
    {
        public IEnumerable<MyItem> ItemCollection { get; private set; }


        protected override void OnStartup(StartupEventArgs e)
        {
            var nomi = new[] { "Mario", "Carlo", "Lucia", "Elena", "Giorgio", "Nicoletta" };
            var cognomi = new[] { "Bianchi", "Rossi", "Verdi", "Brambilla", "Scarpa", "Zennaro" };

            var items = new List<MyItem>();

            for (int i = 0; i < cognomi.Length; i++)
            {
                for (int k = 0; k < nomi.Length; k++)
                {
                    var mi = new MyItem();
                    mi.Code = (i * 10 + 11 + k).ToString();
                    mi.Description = nomi[k] + " " + cognomi[i];
                    items.Add(mi);
                }
            }

            this.ItemCollection = items;

            base.OnStartup(e);
        }
    }

The deal is populating the combo-box with such a data-set, then testing for a numeric input so that the drop-down part should arrange accordingly. As a (future) bonus, the list should be filtered upon the actual number. That should facilitate the user when he/she has to enter a non-existent ID.
On the XAML-side the base is something like this:

<Window 
    x:Class="ComboEditableDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ComboEditableDemo"
    Title="Window1 - buggy" 
    Height="300" 
    Width="400"
    x:Name="This"
    >
    
    <Window.Resources>
        
        <DataTemplate x:Key="dtplItem">
            <StackPanel
                Orientation="Horizontal"
                >
                <TextBlock Text="{Binding Path=Code}" Margin="0,0,8,0" />
                <TextBlock Text="{Binding Path=Description}" />
            </StackPanel>
        </DataTemplate>
        
    </Window.Resources>
    
    <Grid>
        <ComboBox
            x:Name="Cbo1"
            Width="250"
            Height="30"
            IsEditable="True"
            StaysOpenOnEdit="True"
            ItemTemplate="{StaticResource dtplItem}"
            FontSize="18"
            SelectedItem="{Binding Path=CurrentItem, ElementName=This}"
            Text="{Binding Path=CurrentText, ElementName=This}"
            . . .
            >
        </ComboBox>
    </Grid>
</Window>

Finally, here is a possible way to link the data-set to a ComboBox:

            this.Cbo1.ItemsSource = ((App)App.Current).ItemCollection;

First attempt: the bug.

The first attempt was done via a simple styling of the ComboBox, since it exposes two interesting properties:

The style was defined as follows:

        <Style x:Key="ComboStyleKey" TargetType="ComboBox">
            <Setter Property="StaysOpenOnEdit" Value="True" />

            <Style.Triggers>
                <Trigger Property="IsSelectionBoxHighlighted" Value="True">
                    <Setter Property="IsDropDownOpen" Value="True" />
                </Trigger>
            </Style.Triggers>
        </Style>

Although everything seemed going fine (no errors), once you click with the left mouse button in the selection box, a nasty “StackOverflow” exception is suddenly raised. No matter whether the application is creates as “Debug” or “Release”: the error seems very stable.

stack-overflow

At first glance it’s not clear why it happens, and not how to solve or even around it. Also because the actual exception looks as raised in a very deep layer, below the managed. However, that’s just what I suppose.
The second attempt gave me a clearer idea.

Second attempt: better, but not solved yet.

The second attempt was focus on find any (decent) workaround to the bug. Internet didn’t give more help, so I had to find something reliable to make the ComboBox working.
The hint was just on the “stack overflow”, because most of the times it’s a recursive problem. Again, many times you generate such a exception when there are circular calls, that fill the stack sooner. So, let’s give the UI a “breath”: a small delay, so that a routine can complete before actually calling the next one.
Here is the trick:

    public class ComboBox2
        : ComboBox
    {
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);

            if (e.Property.Name == "IsSelectionBoxHighlighted" &&
                this.IsEditable &&
                this.IsSelectionBoxHighlighted &&
                this.IsDropDownOpen == false &&
                this.StaysOpenOnEdit)
            {
                this.OpenDropDown();
            }
        }


        private async void OpenDropDown()
        {
            await Task.Delay(200);
            this.IsDropDownOpen = true;
        }

    }

Well, the exception wasn’t raised any more, but…AW!…the interaction result was not the expected.
Since the drop-down part is actually a borderless “Window” object, it tries to capture the mouse, at least until some kind of mouse action is worthwhile to be captured. As soon, the mouse click outside its client area, the down-down window closes, but the click is “lost”. That is, there’s no (easy) way to interact with the “real” window, so even an attempt to close the application looks impossible.

Despite this trick did not solved the problem, at least it clears *why* the exception is raised in the previous scenario. As soon the selection box is highlighted, the trigger starts the opening of the drop-down. However, the opening of this new child-window would captures the user-input, moving the focus off the selection box. However, the focus is restored to the selection box, and the condition will trigger again the drop-down opening.

Third attempt: all right!

The final solution isn’t much different than the previous attempt: simply avoid the “IsSelectionBoxHighlighted” property…

    public class ComboBox3
        : ComboBox
    {

        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

            if (this.IsEditable &&
                this.IsDropDownOpen == false &&
                this.StaysOpenOnEdit)
            {
                this.IsDropDownOpen = true;
            }
        }

    }

Conclusion.

You may download the source for the demo application here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s