I was building a WPF application for a client which had a few ComboBoxes with large amounts of items as options in them. The client asked for the users to have the ability to filter the items in the drop down, but still force them to choose an item in the list. A search of the internet yielded some solutions, but each solution required me to set the ComboBox.IsEditable property to “True”. In each solution I found and tried doing this always made the user experience a little weird, allowing them to type in whatever they wanted, but still only allowing them to choose an item within the list.
What I did to finally solve the problem was actually pretty simple and I was surprised none of the solutions I came across didn’t go this route. I created a new UserControl to handle this implementation so it would be easier to reuse throughout the application and others in the future.
The first part of it was creating a new Style for the ComboBox, so that inside the Popup (PART_Popup) portion of the ControlTemplate can include my filtering TextBox.
<Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom"> <Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}"> <Border x:Name="DropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"> <Grid RenderOptions.ClearTypeHint="Enabled"> <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0"> <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=DropDownBorder}" Height="{Binding ActualHeight, ElementName=DropDownBorder}" Width="{Binding ActualWidth, ElementName=DropDownBorder}"/> </Canvas> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <DockPanel LastChildFill="True" Margin="0,5,10,5" Grid.Row="0"> <TextBlock DockPanel.Dock="Left" Text="Filter:" Margin="0,0,5,0" /> <TextBox x:Name="DropDownFilterTextBox" /> </DockPanel> <ScrollViewer x:Name="DropDownScrollViewer" Grid.Row="1"> <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </ScrollViewer> </Grid> </Grid> </Border> </Microsoft_Windows_Themes:SystemDropShadowChrome> </Popup>
The other piece of this is filtering the items in the within the ComboBox as they type in the filter TextBox. To do this, I attach to the TextChanged event on it.
protected void DropDownFilterTextBox_TextChanged(object sender, TextChangedEventArgs e) { TextBox textBox = ((TextBox)sender); ComboBox cbo = ((TextBox)sender).TemplatedParent as ComboBox; cbo.Items.Filter += a => { if (a.ToString().ToLower().StartsWith(textBox.Text.ToLower())) { return true; } return false; }; }
Get the VS 2010 Samples Solution for this blog which includes the WPF ComboBox with an items Filter
by
This works nicely until you scroll the list and try to click back in the search textbox. You can’t until you scroll to the top of the list and it has some weird behaviors.
I tried moving the textbox around, but no luck.
Thanks!
Thanks for letting me know that, nobody has reported that yet. I played with it a little bit and was able to correct that behavior by capturing the GotMouseCapture event on the TextBox.
<TextBox x:Name="DropDownFilterTextBox" GotMouseCapture="DropDownFilterTextBox_GotMouseCapture" />
In the event handler, I give the TextBox focus and mark the event as handled.
private void DropDownFilterTextBox_GotMouseCapture(object sender, MouseEventArgs e)
{
TextBox textBox = ((TextBox)sender);
textBox.Focus();
e.Handled = true;
}
I’ll also update the sample’s source code tonight.
Thanks for the quick fix.
This resolves the focus, but for some reason the list scrolls up once if not at the top every time you click above the list (even on a button).
It doesn’t seem to matter if its above or below the list, it scrolls up or down in the list.
To fix the other scroll issues, set that top dock panel containing the text box to a Transparent background with HitTestVisible to true. Next, move it down *in front* of the grid containing the scroll viewer. Finally attach events for MoseMove and MouseDown and MouseUp with a single line to set e.Handled = true in each handler. This will prevent the mouse events from propagating down to the ScrollViewer which has some custom handling implemented by the Combobox to auto-scroll on these mouse events.
I found another issue. If the control is not visible, the filter fails to work. I beat my head for a while, and found that the code in the ComboBox_Loaded method needs to call ApplyTemplate on cbo before trying to find the filtertextbox.
I’ve also cleaned it up to use only the template, so that styles can be applied. Email with a email address and I can send you my version sofar…
HTH
Pingback: WPF ComboBox with Custom Popup – Focus on filter Textbox
Thanks for this article, and very nice site ! but i still have a question, how can you get the number of visible items in your filtered CollectionView ?