The .NET framework is huge, but not so huge that it does everything for everyone; there are things that they in Redmond miss or don’t do for whatever reason but is still generally applicable to many developers. So, dear reader, I present to you a new series of posts on stuff I find missing in .NET, typically where even the Google fails to find the answer. It could be a useful class, a technique, a good practice or documentation that should be in the framework but isn’t.
Well, we’re getting close. Part 1 showed that the standard controls in WPF were all you needed to make autocomplete happen. Part 2 started us on the road to packaging it up into a reusable component; Part 3 went neck deep into advanced WPF territory to finish the hard part of our implementation. In this final segment, I’ll cover some neat features of WPF that we can take advantage of to make our AutoComplete TextBox even better.
There is one more thing to discuss: improvements over the Win32 implementation. The code has a little more in it than what I’ve shown. When creating this, I based it on the ComboBox in the Windows Run dialog. What’s the point of doing this in WPF if we don’t take advantage of the new platform? I touched on Templates earlier when I replaced the TextBox template with my own in Part 3. The ListBox (more accurately, the ItemsControl) allows us to template the UI for our data objects with DataTemplates. What if we exposed that so we could change the look of the autocomplete list? Say we wanted to add an image for each item, as well as text.
Also, what if I wanted to index on more than one property? Currently, my implementation works on the ToString() method for each object, keeping the instances in the list that correspond to what’s typed in the TextBox. Wouldn’t it be cool if we could arbitrarily choose the properties to index on? Then we could get the same behaviour as the Outlook addressee textboxes in the New Mail Window.
I’ll deal with first one, um, first, since it’s the easiest: just expose a DependencyProperty or another Attached property of type DataTemplate. In the PropertyChanged event handler, assign it to the ListBox.ItemTemplate property which you can see in the code.
The second is just a matter of adding a new Dependency Property and changing the CollectionViewSource.Filter event handler like so:
private void CollectionViewSource_Filter(object sender, FilterEventArgs e) { AutoCompleteFilterPathCollection filterPaths = GetAutoCompleteFilterProperty(); if (filterPaths != null) { Type t = e.Item.GetType(); foreach (string autoCompleteProperty in filterPaths) { PropertyInfo info = t.GetProperty(autoCompleteProperty); object value = info.GetValue(e.Item, null); if (TextBoxStartsWith(value)) { e.Accepted = true; return; } } e.Accepted = false; } else { e.Accepted = TextBoxStartsWith(e.Item); } }
where AutoCompleteFilterPathCollection is a custom Collection<string> class with a TypeConverter applied so that we can write the list of properties in a property attribute as a comma-separated list in XAML thusly:
<TextBox ac:AutoComplete.FilterPath="LastName,Email" ac:AutoComplete.Source="{Binding Source={StaticResource people}}" Name="textBox2"></TextBox>
Then we can get an autocomplete textbox that behaves similar to the Outlook To address bar, that indexes on Name and Email. Easy.
API Design Aside: Now we have three Dependency Properties; the client XAML is looking a little cluttered. At this point I may want to make my own AutoComplete object and have one attached DependencyProperty of that type that you set on the TextBox. This has the advantage of enabling sharing AutoComplete data across many controls. But, on the other hand, that design sacrifices the ease of use. There are a couple of ways to present this to the developer, now, is my point. I’ll leave that as an exercise for the reader.
The final thing I wanted to do was show how simple it is to enable this to work on ComboBoxes. It requires just a quick refactoring to parameterize a few things when we set the control in the AutoComplete instance, as shown in Part 3, which I reproduce here:
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { AutoComplete ac = new AutoComplete(); ac.TextBox = (Control)d; ac.ViewSource.Source = e.NewValue; d.SetValue(AutoCompleteInstancePropertyKey, ac); } internal Control TextBox { set { control = value; Style s = (Style)this["autoCompleteTextBoxStyle"]; viewSource = (CollectionViewSource)control.GetViewSource(s); viewSource.Filter += CollectionViewSource_Filter; value.SetValue(Control.StyleProperty, this["autoCompleteTextBoxStyle"]); value.ApplyTemplate(); autoCompletePopup = (Popup) value.Template.FindName("autoCompletePopup", value); value.AddHandler(System.Windows.Controls.TextBox.TextChangedEvent, new TextChangedEventHandler(textBox1_TextChanged)); value.LostFocus += textBox1_LostFocus; value.PreviewKeyUp += textBox1_PreviewKeyUp; } }
As you see, I’ve typed the TextBox property to Control so I can set this to a ComboBox as well, but there are a few things we’ll have to move into a custom type. For starters, the Style for ComboBox is different than TextBox, so we’ll have to repeat the steps that we took when extracting the TextBox style, namely (deep breath) open XamlPadX, extract the ComboBox style, put it in my ResourceDictionary, add the ListBox control to the visual tree and add the CollectionViewSource to the Style’s Resources.
Once we’ve done that, we want to change the TextBox property above. The two things that we need to change are the hard-coded resource key, "autoCompleteTextBoxStyle"; and we have to parameterize where the CollectionViewSource comes from which you can tell from the two Styles. You can see the code for all the details, but the TextBox property (which I should rename) now looks like this:
internal Control TextBox { set { control = AutoCompleteControl.Create(value); Style s = (Style)this[control.StyleKey]; viewSource = control.GetViewSource(s); viewSource.Filter += CollectionViewSource_Filter; value.SetValue(Control.StyleProperty, this[control.StyleKey]); value.ApplyTemplate(); autoCompletePopup = (Popup) value.Template.FindName("autoCompletePopup", value); value.AddHandler(System.Windows.Controls.TextBox.TextChangedEvent, new TextChangedEventHandler(textBox1_TextChanged)); value.LostFocus += textBox1_LostFocus; value.PreviewKeyUp += textBox1_PreviewKeyUp; } }
My parameterization class is slightly more complicated because of other methods in the AutoComplete class. One thing that is truly magic in this case is setting the TextChangedEvent handler on the ComboBox. Notice that I’m adding a handler to the TextBox.TextChangedEvent. Look at the ComboBox API, and you won’t see a TextChangedEvent! Yet it works just as you’d expect it to. That’s some magic that I don’t understand. Magic or no, the ease with which we’ve introduced autocomplete for ComboBox can’t go unnoticed. Note also that the ComboBox’s own Items are unaffected by autocomplete (you can set them both to the same data source, though).
Phew! As you’ve seen over this massive edition of the Missing .NET, creating behaviour in WPF needn’t involve custom controls, but it does require a solid understanding of the underlying design concepts of WPF. There is definitely more we can do with this AutoComplete stuff as well. Hopefully, I’ve shown you the start of what’s possible.
Nice, but the inability to select an option with the mouse is a big deviation from any autocomplete I’ve ever used.
your solution is way too complicated
Quite Helpful!!
But Can you please provide the code for selecting an option from mouse in autocomplete textbox.
Thanks!!!
Hey i got the solution thanks a lot.
private void TextBox1_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
TextBlock tb = e.OriginalSource as TextBlock;
if (tb != null)
{
TextBox1.Text = tb.Text;
e.Handled = true;
}
}
}
Very nice, but I have an issue with the style.
How can I assign a custom style to this control?
Thanks!!!
a little review of the code of Sharath
internal Control TextBox
{
set
{
control = ControlUnderAutoComplete.Create(value);
Style s = (Style)this[control.StyleKey];
viewSource = control.GetViewSource(s);
viewSource.Filter += CollectionViewSource_Filter;
value.SetValue(Control.StyleProperty, this[control.StyleKey]);
value.ApplyTemplate();
autoCompletePopup = (Popup) value.Template.FindName(“autoCompletePopup”, value);
listBox = (ListBox) value.Template.FindName(“autoCompleteListBox”, value);
value.AddHandler(System.Windows.Controls.TextBox.TextChangedEvent, new TextChangedEventHandler(textBox1_TextChanged));
value.LostFocus += textBox1_LostFocus;
value.PreviewKeyUp += textBox1_PreviewKeyUp;
//ADD THIS LINE FOR SELECTING AN OPTION FROM MOUSE
value.PreviewMouseDown += new MouseButtonEventHandler(value_PreviewMouseDown);
}
}
void value_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
TextBlock tb = e.OriginalSource as TextBlock;
if (tb != null)
{
((TextBox )sender).Text = tb.Text;
e.Handled = true;
}
}
}
Hi,
Thanks for sharing your code.
I wanted to bring your attention to this alternative here: http://code.google.com/p/kocontrols/downloads/list