Frage

I want to make a searchable ComboBox, with the search box being a TextBox that appears above the ItemsPanel when the ComboBox dropdown is expanded. I think I need to make a custom control to achieve the search functionality, but first I'm just trying to get the TextBox to show up using a normal ComboBox. Here's my current attempt, which is generating an exception when I try to expand the dropdown:

<Style x:Key="FilteredComboBox" TargetType="ComboBox">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <StackPanel>
                    <TextBox/>
                    <StackPanel IsItemsHost="True"
                                Orientation="Horizontal"
                                VerticalAlignment="Center"
                                HorizontalAlignment="Center"/>
                    </StackPanel>

            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
</Style>

The exception this is generating is:

Cannot explicitly modify Children collection of Panel used as ItemsPanel for ItemsControl. ItemsControl generates child elements for Panel.

I'm pretty sure there's a way to make this do what I want, but after several hours of googling and trial and error my head is now spinning. Any help would be greatly appreciated!

War es hilfreich?

Lösung

While the responses I got to this were all along the right track, I didn't think any of them really answered my question well enough to be marked as the solution. What I ended up doing was deriving a custom control from the ComboBox (right-click on a ComboBox in the designer and choose Edit Template -> Edit a Copy, this generates a whole bunch of XAML code for the ComboBox template which I copied into the Generic.xaml file of the custom control project I made). I then edited the Popup portion of the generated XAML code to add a WatermarkTextBox (from the Extended WPF Toolkit) above the ItemsPresenter like so:

<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">
    <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}}">
        <ScrollViewer x:Name="DropDownScrollViewer">
        <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 Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <xtk:WatermarkTextBox Grid.Row="0"
                          Visibility="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource booleanToVisibilityConverter}}"
                          Watermark="Type here to filter..."
                          Text="{Binding SearchFilter, RelativeSource={RelativeSource TemplatedParent}, UpdateSourceTrigger=PropertyChanged}">
            </xtk:WatermarkTextBox>
            <ItemsPresenter x:Name="ItemsPresenter" Grid.Row="1" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </Grid>
        </Grid>
        </ScrollViewer>
    </Border>
    </Themes:SystemDropShadowChrome>
</Popup>

The SearchFilter property I'm binding the Text to is a property in my custom control's code-behind which then performs the actual filtering of the values in the ComboBox. It's a little beyond the scope of my original question but here's how I'm doing the filtering in case anyone is curious:

public class SearchableComboBox : ComboBox
{
    public const string SearchFilterPropertyName = "SearchFilter";
    public readonly static DependencyProperty SearchFilterProperty;
    public string SearchFilter
    {
        get { return (string)GetValue(SearchFilterProperty); }
        set { SetValue(SearchFilterProperty, value); }
    }

    static SearchableComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchableComboBox), new FrameworkPropertyMetadata(typeof(SearchableComboBox)));

        SearchFilterProperty = DependencyProperty.Register(SearchFilterPropertyName, typeof(string), typeof(SearchableComboBox),
            new PropertyMetadata(string.Empty, new PropertyChangedCallback(SearchFilter_PropertyChanged)));

    }

    private static void SearchFilter_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((SearchableComboBox)d).RefreshFilter();
    }

    private void RefreshFilter()
    {
        if (this.ItemsSource != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
            view.Refresh();
        }
    }

    private bool FilterPredicate(object value)
    {
        if (value == null)
            return false;

        if (string.IsNullOrEmpty(SearchFilter))
            return true;

        return value.ToString().Contains(SearchFilter, StringComparison.CurrentCultureIgnoreCase);
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (newValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
            view.Filter += this.FilterPredicate;
        }

        if (oldValue != null)
        {
            ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
            view.Filter -= this.FilterPredicate;
        }

        base.OnItemsSourceChanged(oldValue, newValue);
    }

Andere Tipps

You can use "ComboBox ControlTemplate" from msdn.

You must edit Popup from this style. I added there Grid control with two rows. In the first row I added TextBox, I think that better option will be here WatermarkTextBox from Extended WPF Toolkit (WatermarkTextBox). In the second row you have ScrollViewer with your items.

The TextBox from first row is always visible, when ComboBox has a lot of items only these elements will be in scroll content.

Below is complete example:

<!-- Fill Brushes -->

<LinearGradientBrush x:Key="NormalBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#CCC" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalNormalBrush" StartPoint="0,0" EndPoint="1,0">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#CCC" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="LightBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#EEE" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalLightBrush" StartPoint="0,0" EndPoint="1,0">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#EEE" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="DarkBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#AAA" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#BBB" Offset="0.0"/>
            <GradientStop Color="#EEE" Offset="0.1"/>
            <GradientStop Color="#EEE" Offset="0.9"/>
            <GradientStop Color="#FFF" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />

<SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />

<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />

<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" />

<!-- Border Brushes -->

<LinearGradientBrush x:Key="NormalBorderBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#CCC" Offset="0.0"/>
            <GradientStop Color="#444" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalNormalBorderBrush" StartPoint="0,0" EndPoint="1,0">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#CCC" Offset="0.0"/>
            <GradientStop Color="#444" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultedBorderBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#777" Offset="0.0"/>
            <GradientStop Color="#000" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBorderBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#444" Offset="0.0"/>
            <GradientStop Color="#888" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="DisabledBorderBrush" Color="#AAA" />

<SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />

<SolidColorBrush x:Key="LightBorderBrush" Color="#AAA" />

<!-- Miscellaneous Brushes -->
<SolidColorBrush x:Key="GlyphBrush" Color="#444" />

<SolidColorBrush x:Key="LightColorBrush" Color="#DDD" />

<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="20" />
        </Grid.ColumnDefinitions>
        <Border
            x:Name="Border" 
            Grid.ColumnSpan="2"
            CornerRadius="2"
            Background="{StaticResource NormalBrush}"
            BorderBrush="{StaticResource NormalBorderBrush}"
            BorderThickness="1" />
        <Border 
            Grid.Column="0"
            CornerRadius="2,0,0,2" 
            Margin="1" 
            Background="{StaticResource WindowBackgroundBrush}" 
            BorderBrush="{StaticResource NormalBorderBrush}"
            BorderThickness="0,0,1,0" />
        <Path 
            x:Name="Arrow"
            Grid.Column="1"     
            Fill="{StaticResource GlyphBrush}"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Data="M 0 0 L 4 4 L 8 0 Z"/>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="ToggleButton.IsMouseOver" Value="true">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource DarkBrush}" />
        </Trigger>
        <Trigger Property="ToggleButton.IsChecked" Value="true">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource PressedBrush}" />
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
            <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" />
            <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBorderBrush}" />
            <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
            <Setter TargetName="Arrow" Property="Fill" Value="{StaticResource DisabledForegroundBrush}" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<ControlTemplate x:Key="ComboBoxTextBox" TargetType="TextBox">
    <Border x:Name="PART_ContentHost" Focusable="False" Background="{TemplateBinding Background}" />
</ControlTemplate>

<Style x:Key="{x:Type ComboBox}" TargetType="ComboBox">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="20"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBox">
                <Grid>
                    <ToggleButton 
                        Name="ToggleButton" 
                        Template="{StaticResource ComboBoxToggleButton}" 
                        Grid.Column="2" 
                        Focusable="false"
                        IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
                        ClickMode="Press">
                    </ToggleButton>
                    <ContentPresenter
                        Name="ContentSite"
                        IsHitTestVisible="False" 
                        Content="{TemplateBinding SelectionBoxItem}"
                        ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                        ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                        Margin="3,3,23,3"
                        VerticalAlignment="Center"
                        HorizontalAlignment="Left" />
                    <TextBox x:Name="PART_EditableTextBox"
                             Style="{x:Null}" 
                             Template="{StaticResource ComboBoxTextBox}" 
                             HorizontalAlignment="Left" 
                             VerticalAlignment="Center" 
                             Margin="3,3,23,3"
                             Focusable="True" 
                             Background="Transparent"
                             Visibility="Hidden"
                             IsReadOnly="{TemplateBinding IsReadOnly}"/>
                    <Popup 
                        Name="Popup"
                        Placement="Bottom"
                        IsOpen="{TemplateBinding IsDropDownOpen}"
                        AllowsTransparency="True" 
                        Focusable="False"
                        PopupAnimation="Slide">
                        <Grid 
                            Name="DropDown"
                            SnapsToDevicePixels="True"                
                            MinWidth="{TemplateBinding ActualWidth}"
                            MaxHeight="{TemplateBinding MaxDropDownHeight}">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="30" />
                                <RowDefinition Height="{TemplateBinding MaxDropDownHeight}" />
                            </Grid.RowDefinitions>

                            <Border 
                                x:Name="DropDownBorder"
                                Background="{StaticResource WindowBackgroundBrush}"
                                BorderThickness="1"
                                BorderBrush="{StaticResource SolidBorderBrush}"
                                Grid.RowSpan="2"/>

                            <TextBox Margin="2" VerticalAlignment="Center" />

                            <ScrollViewer Grid.Row="1" Margin="4,6,4,6" SnapsToDevicePixels="True">
                                <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
                            </ScrollViewer>
                        </Grid>
                    </Popup>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                    <Trigger SourceName="Popup" Property="Popup.AllowsTransparency" Value="true">
                        <Setter TargetName="DropDownBorder" Property="CornerRadius" Value="4"/>
                        <Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0"/>
                    </Trigger>
                    <Trigger Property="IsEditable"
                             Value="true">
                        <Setter Property="IsTabStop" Value="false"/>
                        <Setter TargetName="PART_EditableTextBox" Property="Visibility"    Value="Visible"/>
                        <Setter TargetName="ContentSite" Property="Visibility" Value="Hidden"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
    </Style.Triggers>
</Style>

You are going to have a couple of problems, I think.

  1. If you are relying on a style to show the textbox, where are you going to put the logic for that textbox? you need to extend the combobox to add that logic or make some sort of composite control.

  2. The second problem is that the combobox by its nature closes when it loses focus. So, you have to write a some logic to change that behavior.

It's not impossible to do, I just think you need to do a lot more than create a style.

Yes it is a lot of code but this is when you should start thinking about using resource dictionaries. I do agree with the other posters that this sort of control might cause you headaches as the combobox will contract if a user isn't careful and loses focus.

  <Style x:Key="MyCombo" TargetType="{x:Type ComboBox}">
        <Setter Property="Template">
            <Setter.Value>
            <ControlTemplate TargetType="{x:Type ComboBox}">
                <Grid x:Name="MainGrid" SnapsToDevicePixels="true">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
                    </Grid.ColumnDefinitions>
                    <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}}">
                                <ScrollViewer x:Name="DropDownScrollViewer">
                                    <Grid RenderOptions.ClearTypeHint="Enabled">
                                        <StackPanel>
                                            <TextBox>blah blah</TextBox>


                                            <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>
                                            <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                        </StackPanel>
                                    </Grid>
                                </ScrollViewer>
                            </Border>
                        </Microsoft_Windows_Themes:SystemDropShadowChrome>
                    </Popup>
                    <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/>
                    <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true">
                        <Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
                        <Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
                    </Trigger>
                    <Trigger Property="HasItems" Value="false">
                        <Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        <Setter Property="Background" Value="#FFF4F4F4"/>
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                    <Trigger Property="ScrollViewer.CanContentScroll" SourceName="DropDownScrollViewer" Value="false">
                        <Setter Property="Canvas.Top" TargetName="OpaqueRect" Value="{Binding VerticalOffset, ElementName=DropDownScrollViewer}"/>
                        <Setter Property="Canvas.Left" TargetName="OpaqueRect" Value="{Binding HorizontalOffset, ElementName=DropDownScrollViewer}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top