Enhancing AutoCompleteBox part 1: distinguishing similar items

Among other novelties Silverlight 3 also included new and very useful AutoCompleteBox allowing to make smart suggestions to a user during text input. Unfortunately, as it always happens with new and pretty complex controls like that, there are some scenarios when you can't avoid changing or enhancing its default behavior to get the functionality you want.

The first thing I'd like to discuss is how to make AutoCompleteBox work like combo: not only selecting the text you want, but also selecting the object you want. As it turns out, AutoCompleteBox is not a perfect candidate for implementing this functionality: you'll need to extend the code a little to make it work smoothly.

Problems start when you need to distinguish objects which are represented by the same text in AutoCompleteBox's text box. For example, you are implementing an AutoCompleteBox for editing persons LastName. In a dropdown list, you display person's FullName (FirstName+LastName), but in textbox - only LastName. Than, you bind your Person property to AutoCompleteBox.SelectedItem and expect that you'll get as SelectedItem the object you selected in list. But no: selecting second person with the same name
sel.PNG
makes AutoCompleteBox thinks that the first item is selected!
sel1.PNG
This is actually a well-known problem discussed on Silverlight forums. AutoCompleteBox creators recommend to avoid this scenario completely, but actually, not all is that bad, this problem is fixable in a rather simple way. Unfortunately, custom ItemFilters and overriding FormatValue won't help here: it all goes to the same string displayed in AutoCompleteBox text box. When you do a selection in popup, AutoCompleteBox remembers only this string. To get a selected item, it searches again in the whole list and, surely, is having troubles with duplicates, as the information about the actual selection is lost. But can fix it - just make it remember. In your custom class inherited from AutoCompleteBox, define a new dependency property (name it, for example, SelectionBoxItem like it was named in ComboBox), than override OnDropDownClosing:

        protected override void OnDropDownClosing(RoutedPropertyChangingEventArgs<bool> e)
        {
            base.OnDropDownClosing(e);
            SelectionBoxItem = SelectionAdapter.SelectedItem;
        }

You'll need to bind to SelectionBoxItem instead of SelectedItem to get the proper item from selection. Nothing spectacular, but it works, as you may see in the demo attached.

AttachmentSize
autocompleteboxduplicates.zip152.08 KB

Comments

This fix worked like a charm!!

Thanks for posting this fix. It was really annoying and had eaten up a day's effort to fix it.

This works for me

This isn't pretty but seems to work for keys, duplicates, etc...

Written in VB...

Private Sub AutoCompleteBox1_SelectionChanged(ByVal sender As Object, ByVal e As System.Windows.Controls.SelectionChangedEventArgs) Handles AutoCompleteBox1.SelectionChanged

Static LastItem As Object '= e.AddedItems(0)

Dim Current As Object = Nothing
If e.AddedItems.Count > 0 Then Current = e.AddedItems(0)

If AutoCompleteBox1.IsDropDownOpen = False Then
If Not AutoCompleteBox1.SelectedItem Is AutoCompleteBox1.Tag Then AutoCompleteBox1.SelectedItem = AutoCompleteBox1.Tag
ElseIf Not LastItem Is Current Then
If Not AutoCompleteBox1.SelectedItem Is Current Then AutoCompleteBox1.SelectedItem = Current
AutoCompleteBox1.Tag = AutoCompleteBox1.SelectedItem
End If

LastItem = Current

End Sub

Making this work with a DataTemplate

Hi Alex,
This is a great fix for the problem, but how do we get this to work with a DataTemplate? I really appreciate your help.

Thanks very much in advance.

Re: making this work with DataTemplate

Thanks, glad this workaround was helpful. About DataTemplate: I'm not sure what's causing your problem, just write any valid ItemTemplate to AutoCompleteBoxEx and it should work. We're actually using this workaround with DataTemplate without problems. You may check it by specifying an ItemTemplate to AutoCompleteBoxEx in the sample, for example:

...
                        <AutoCompleteBoxExDemo:AutoCompleteBoxEx.ItemTemplate>
                            <DataTemplate>
                                <StackPanel>
                                    <TextBlock Text="my template"/>
                                </StackPanel>
                            </DataTemplate>
                        </AutoCompleteBoxExDemo:AutoCompleteBoxEx.ItemTemplate>
                    </AutoCompleteBoxExDemo:AutoCompleteBoxEx>

dint work for me ?

Hello

I am trying to fix the problem like this :

I have created the helper class for autocomplete and derived it from 'Autocomplete'. Everything is same and I am usinh DataTemplate in autocomplete box. But it still goes in selectionchanged method twice and get the wrong id in second time . Its so annoying , please help me ...

Thanks

code dint show up

Re: dint work for me ?

It works perfectly in the attached project. Please try to localize this problem and reproduce it by modifying the attached sample project. Most likely you are filling the autocomplete box twice, and it causes problems with synchronization.

ToString

A simpler solution would be to override the ToString method of whatever object you are listing in the popup. This worked great for me.

Re: ToString

No, just overriding ToString() will not help here. As you see in the attached project, Person.ToString() method is already overriden (just to display readable text in combo), however it doesn't solve the duplication problem.

And even if it'll help, it's not a good solution, as it requires overriding ToString() in every class displayed in AutoCompleteBox, and different ToString() functionality may be needed somewhere else.

thanks, great solution! two

thanks, great solution!
two things i noticed when handling change events:
a) SelectionChanged fires when you select an item using the cursor keys while the textbox is open, but SelectionBoxItem doesn't change (not surprisingly)
b) SelectionChanged takes place before OnDropDownClosing, so you'll have an old value in SelectionBoxItem when handling this event (need to declare a custom RoutedEvent?)

Re: two things i noticed

Thank you, very interesting observations indeed. So you are saying that on SelectionChanged it's impossible to distinguish similar items with this fix? Agreed, this may be a problem for some scenarios. But just setting SelectionBoxItem before base.OnDropDownClosing may cause a problem with delayed loading of popup contents. Hmm, it seems that we've fixed similar problem before, just thought it isn't worthy of a separate article. Very well, we'll post the solution later - stay tuned for "Enhancing AutoCompleteBox part 2"!

it's still causing pain

how about part 2?

i tried the aproach of

i tried the aproach of implementing my own change-event (BoxItemChanged) but couldn't find an apropriate place to raise it. you can't do it in OnSelectionBoxItemPropertyChanged() because it's static and RaiseEvent() is not. raising it in the setter of SelectionBoxItem is tempting - but the setter is not called when the value is assigned via binding!

Re: i tried the aproach of

First of all, I'm very sorry that I've forgotten about this comment about SelectionChanged. I looked at the sources, and no, we never needed to treat actual value on SelectionChanged, so no hint about it there. But the problem seems to be solvable: on SelectionChanged, SelectionBoxItem seems to show correct value in all cases except the case when the first item in list is selected, in this case SelectionBoxItem is incorrect (shows old value), SelectionAdapter.SelectedItem == null, but SelectionItem shows correct value! So it can be fixed at least by adding another property SelectedItemForSelectionChanged which = (SelectionAdapter.SelectedItem == null)?SelectionItem:SelectionBoxItem. Does it help in your case?

PS > SelectionChanged takes place before OnDropDownClosing
no, it occurs twice, once before and once after OnDropDownClosing, except the case when the first item is selected. So the fix I've described in this comment should work.

Not able to fix in my case

In my solution I have created AutocompleteBoxEx inherited from AutoCompleteBox as per your code.
I am using this control in another xaml where on lloading binding this control with staff names and in droprdownclosing event set dependancy
{property base.OnDropDownClosing(e);
SelectionBoxItem = SelectionAdapter.SelectedItem;
}
In xaml.cs I have raised event selectionchanged..In this I am populating employee ID in another textbox..but not able to set it to proper ID ..it always set to first employeeID.
Please suggest solution for me..NOTE:I am not using Dependancyproperty in xaml(i.e.

Got exception in XAML but it still working

After using your fix in my project, it works properly but my XAML page becomes the following message therefore I cannot view the design mode of my XAML. How to resolve this ?

Exception: A 'Binding' cannot be set on the 'SelectionBoxItem' property of type 'AutoCompleteBoxEx'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
StackTrace: Empty

at MS.Internal.Helper.CheckCanReceiveMarkupExtension(MarkupExtension markupExtension, IServiceProvider serviceProvider, DependencyObject& targetDependencyObject, DependencyProperty& targetDependencyProperty)
at System.Windows.Data.BindingBase.ProvideValue(IServiceProvider serviceProvider)

Here is my XML code that use

Here is my XML code that use your fix.

DependencyProperty

Here's how you should implement a bindable property.
Note that PropertyChangedCallback is need to properly get TwoWay binding with SelectedItem base property.


public static readonly DependencyProperty SelectionBoxItemProperty = DependencyProperty.Register(
"SelectionBoxItem", typeof(object), typeof(CustomAutoCompleteBox), new PropertyMetadata(new PropertyChangedCallback(SelectionBoxItemChanged)));

public object SelectionBoxItem
{
get { return (string)GetValue(SelectionBoxItemProperty); }
set { SetValue(SelectionBoxItemProperty, value); }
}

public static void SelectionBoxItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox control = d as AutoCompleteBox;
if (control != null)
control.SetValue(SelectedItemProperty, e.NewValue);
}