0

In my WPF application I have a DataGrid, and I want the user to be able to filter which rows get displayed. The filtering is implemented like this: on the GUI there is a ComboBox that enumerates the possible values of some property, let's call it SomeProperty. When the user selects a value, say "Value1", the DataGrid will only display items with item.SomeProperty == "Value1". Both the DataGrid and the ComboBox contents come from a database.

I want the user to be able to switch off filtering by SomeProperty, so I looked for a way to add an "all" item to the ComboBox, that returns null and that I can use in my filtering logic. I found this:

http://philondotnet.wordpress.com/2009/09/18/how-to-select-null-none-in-a-combobox-listbox-listview

This is a wrapper class that adds a null item to a ComboBox or similar. As I am also using ComboBox.DisplayMemberPath property, I changed

public static readonly DependencyProperty NullItemProperty = DependencyProperty.Register(
        "NullItem", typeof(object), typeof(NullItemSelectorAdapter), new PropertyMetadata("(None)"));

to

public static readonly DependencyProperty NullItemProperty = DependencyProperty.Register(
        "NullItem", typeof(NullItem), typeof(NullItemSelectorAdapter), new PropertyMetadata(new NullItem()));

and added a class like this:

[TypeConverter(typeof(NullItemConverter))]
class NullItem: DynamicObject
{
    private const string Text = "(all)";

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = Text;
        return true;
    }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        result = null;
        return true;
    }
}

public class NullItemConverter : TypeConverter
{
    public override bool CanConvertTo(ITypeDescriptorContext context, Type sourceType)
    {
        return true;
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        return null;
    }

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return true;
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return NullItem.Instance;
    }
}

in order to be able to use it like this (irrelevant attributes omitted):

<view:NullItemSelectorAdapter ItemsSource="{Binding People}">
    <ComboBox DisplayMemberPath="Name"/>
</view:NullItemSelectorAdapter>

<view:NullItemSelectorAdapter ItemsSource="{Binding Products}">
    <ComboBox DisplayMemberPath="Description"/>
</view:NullItemSelectorAdapter>

etc.

(The objects in the ItemsSource are instances of generated classes, so I cannot override their ToString method.)

When I call Application.MainWindow.Show(), all these ComboBoxes are instantiated, and I get a ton of errors like this:

System.Windows.Data Error: 23 : Cannot convert 'MyNamespace.View.NullItem' from type 'NullItem' to type 'MyModel.Product' for 'hu-HU' culture with default conversions; consider using Converter property of Binding. NotSupportedException:'System.NotSupportedException: TypeConverter cannot convert from MyNamespace.View.NullItem.
   at System.ComponentModel.TypeConverter.GetConvertFromException(Object value)
   at System.ComponentModel.TypeConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)
   at MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfo culture, Boolean isForward)'
System.Windows.Data Error: 7 : ConvertBack cannot convert value 'MyNamespace.View.NullItem' (type 'NullItem'). target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object') NotSupportedException:'System.NotSupportedException: TypeConverter cannot convert from MyNamespace.View.NullItem.
   at MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfo culture, Boolean isForward)
   at MS.Internal.Data.ObjectTargetConverter.ConvertBack(Object o, Type type, Object parameter, CultureInfo culture)
   at System.Windows.Data.BindingExpression.ConvertBackHelper(IValueConverter converter, Object value, Type sourceType, Object parameter, CultureInfo culture)'

The TypeConverter I've specified does not get instantiated, even though it should be according to the reference sources of MS.Internal.Data.DefaultValueConverter.

These errors do not make the program to crash (it runs fine afterwards), but they cause a noticeable delay on when the window contents get rendered, even on fast computers. How can make this delay go away?

I'm mainly interested in a solution that does not involve manually adding a Converter to each and every Binding on usages of NullItemSelectorAdapter, because that's a lot. I hope that this can be solved by hacking around in the NullItemSelectorAdapter and NullItem classes.

Solution: Roel's answer below is the solution I went for, because it's a one-liner trick to make the mentioned errors disappear. However adabyron's accepted answer is the semantically more correct, more elegant solution and you should use that.

Community
  • 1
  • 1
marczellm
  • 1,224
  • 2
  • 18
  • 42
  • If you just want to add an empty item to your `ComboBox`es, then you've found a really long way of doing it. Given that typically, items are data bound to the `ComboBox.ItemsSource` property, you can add an empty item simply using something like: `Items.Add(new YourDataType())`, or perhaps in your case: `Items.Add(new YourDataType("(None)"))`. – Sheridan Aug 04 '14 at 19:53
  • @Sheridan The ComboBoxes are bound to collections not under my control, these come from the database. The ComboBoxes are used to select filtering conditions on a DataGrid, the null item represents no filtering on that specific property whose possible values the ComboBox enumerates. – marczellm Aug 04 '14 at 22:24
  • I didn't have time to look at your requirements , look in BindingBase.TargetNullValue – eran otzap Aug 12 '14 at 10:27
  • @eranotzap Tried it, but the `TargetNullValue` does not appear in the items. – marczellm Aug 12 '14 at 10:43
  • The TypeConverter won't work, since it is intended to make an instance of a class usable as a XAML settable attribute value. You can debug it if you set the NullItem property in Xaml on the NullItemSelectorAdapter to a string value, like "(all)". But I still see the conversion error, since it still tries to convert NullItem to Product (or the other types you use) – Roel van Westerop Aug 12 '14 at 11:28
  • @RoelvanWesterop I looked into the [reference sources of `MS.Internal.Data.DefaultValueConverter`](http://referencesource.microsoft.com/PresentationFramework/src/Framework/MS/Internal/Data/DefaultValueConverter.cs.html) mentioned in the error message, and it's supposed to call `TypeDescriptor.GetConverter(type)` which should return my TypeConverter. In this case it's probably a bug, or the reference sources are not correct. – marczellm Aug 12 '14 at 11:54
  • Well, you could create a `DependencyProperty` on the `NullItemSelectorAdapter` which specifies the type you want to add to the `ComboBox` and use `Activator.CreateInstance` and some other reflection magic to set the property you need to have set to the text "(all)" – Roel van Westerop Aug 12 '14 at 12:43
  • @RoelvanWesterop but then, will it return null to the SelectedItem binding source? I'm using that null value in the filtering logic. – marczellm Aug 12 '14 at 13:31
  • No, it won't return null then. Btw, I only get the error when I bind the SelectedItem of the ComboBox, is that right? If so, how about changing they way you are notified of the change in the selection? – Roel van Westerop Aug 12 '14 at 14:13
  • @RoelvanWesterop I can do that on my own if all else fails. Thank you. – marczellm Aug 12 '14 at 14:52
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/59249/discussion-between-roel-van-westerop-and-marczellm). – Roel van Westerop Aug 13 '14 at 06:33

3 Answers3

2

Second suggestion, after the OP made clear that his client insists on the null-item. I am sorry to say that I again disregard one of your requirements, which is that SelectedItem is null. But (as stated in a different way in my first answer),

  • Having something to add to the ComboBox (the null/all-item)
  • yet actually adding null (or warping it behind the scenes so it seems that way)

just doesn't go together for me.

On the bright side, since the filtering mechanism is definitely under your control, I suppose you should be able to work with the following. If you don't want to add the behavior to every ComboBox, you could use this code to apply it in an implicit style.

I've created a ComboBox Behavior that will generically insert the "- All -" item, and added an IsNullItemSelected property that you would use instead of SelectedItem == null in the filtering. Current (known) limitations: I expect the ItemsSource to be an IList and the contained items should either be strings or have a parameterless constructor.

enter image description here

enter image description here

enter image description here

enter image description here

The behavior:

using System;
using System.Linq;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Reflection;

namespace WpfApplication1.Behaviors
{
    public class NullableComboBoxBehavior : Behavior<ComboBox>
    {
        // IsNullValueSelected 
        public static readonly DependencyProperty IsNullValueSelectedProperty = DependencyProperty.Register("IsNullValueSelected", typeof(bool), typeof(NullableComboBoxBehavior), new PropertyMetadata(false));
        public bool IsNullValueSelected { get { return (bool)GetValue(IsNullValueSelectedProperty); } set { SetValue(IsNullValueSelectedProperty, value); } }

        private const string AllCaption = "- All -";

        protected override void OnAttached()
        {
            DependencyPropertyDescriptor.FromProperty(ComboBox.ItemsSourceProperty, typeof(ComboBox))
                    .AddValueChanged(this.AssociatedObject, OnItemsSourceChanged);

            DependencyPropertyDescriptor.FromProperty(ComboBox.SelectedItemProperty, typeof(ComboBox))
                    .AddValueChanged(this.AssociatedObject, OnSelectedItemChanged);

            // initial call
            OnItemsSourceChanged(this, EventArgs.Empty);
            OnSelectedItemChanged(this, EventArgs.Empty);
        }


        private void OnSelectedItemChanged(object sender, EventArgs e)
        {
            var cbx = this.AssociatedObject;

            // If the caption of the selected item is either "- All -" or no item is selected, 
            // set IsNullValueSelected to true
            if (cbx.SelectedItem != null)
            {
                // get caption directly or by way of DisplayMemberPath
                string caption = cbx.SelectedItem.GetType() == typeof(string) ?
                                    (string)cbx.SelectedItem :
                                    GetDisplayMemberProperty(cbx.SelectedItem).GetValue(cbx.SelectedItem).ToString();

                if (caption == AllCaption || caption == null)
                    this.IsNullValueSelected = true;
                else
                    this.IsNullValueSelected = false;
            }
            else
                this.IsNullValueSelected = true;
        }

        private void OnItemsSourceChanged(object sender, EventArgs e)
        {
            var cbx = this.AssociatedObject;

            // assuming an ItemsSource that implements IList
            if (cbx.ItemsSource != null && (IList)cbx.ItemsSource != null)
            {
                Type T = cbx.ItemsSource.AsQueryable().ElementType;

                object obj;

                if (T == typeof(string))
                    obj = AllCaption; // set AllCaption directly
                else if (T.GetConstructor(Type.EmptyTypes) != null)
                {
                    // set AllCaption by way of DisplayMemberPath
                    obj = Activator.CreateInstance(T);
                    GetDisplayMemberProperty(obj).SetValue(obj, AllCaption);
                }
                else
                    throw new Exception("Only types with parameterless ctors or string are supported.");

                // insert the null item
                ((IList)cbx.ItemsSource).Insert(0, obj);

                // select first item (optional). 
                // If you uncomment this, remove the OnSelectedItemChanged call in OnAttached 
                //cbx.SelectedIndex = 0;
            }
        }

        private PropertyInfo GetDisplayMemberProperty(object obj)
        {
            if (string.IsNullOrEmpty(this.AssociatedObject.DisplayMemberPath))
                throw new Exception("This will only work if DisplayMemberPath is set.");

            // get the property info of the DisplayMemberPath
            return obj.GetType().GetProperty(this.AssociatedObject.DisplayMemberPath);
        }
    }
}

Implementation:

<Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:vm="clr-namespace:WpfApplication1.ViewModels"
            xmlns:beh="clr-namespace:WpfApplication1.Behaviors"
            Title="MainWindow" Height="350" Width="580">

    <Window.DataContext>
        <vm:ComboBoxResetViewModel />
    </Window.DataContext>

    <StackPanel Orientation="Horizontal" VerticalAlignment="Top" >
        <ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" SelectedValue="{Binding SelectedValue}" DisplayMemberPath="Name" Margin="5,2" Width="150" >
            <i:Interaction.Behaviors>
                <beh:NullableComboBoxBehavior IsNullValueSelected="{Binding IsNullValueSelected, Mode=OneWayToSource}" />
            </i:Interaction.Behaviors>
        </ComboBox>
        <TextBlock Text="SelectedItem:" FontWeight="SemiBold"  Margin="50,2,0,2" VerticalAlignment="Center" />
        <TextBlock Text="{Binding SelectedItem.Name, FallbackValue='null'}" Foreground="Blue" Margin="5,2" VerticalAlignment="Center" />
        <TextBlock Text="IsNullValueSelected:" FontWeight="SemiBold"  Margin="30,2,0,2" VerticalAlignment="Center" />
        <TextBlock Text="{Binding IsNullValueSelected}" Foreground="Blue" Margin="5,2" VerticalAlignment="Center" />
    </StackPanel>
</Window>

ViewModel:

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class ComboBoxResetViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private ObservableCollection<ItemViewModel> _items;
        public ObservableCollection<ItemViewModel> Items { get { return _items; } set { _items = value; OnPropertyChanged("Items"); } }

        private ItemViewModel _selectedItem;
        public ItemViewModel SelectedItem { get { return _selectedItem; } set { _selectedItem = value; OnPropertyChanged("SelectedItem"); } }

        private bool _isNullValueSelected;
        public bool IsNullValueSelected { get { return _isNullValueSelected; } set { _isNullValueSelected = value; OnPropertyChanged("IsNullValueSelected"); } }

        public ComboBoxResetViewModel()
        {
            this.Items = new ObservableCollection<ItemViewModel>()
                {
                    new ItemViewModel() { Name = "Item 1" },
                    new ItemViewModel() { Name = "Item 2" },
                    new ItemViewModel() { Name = "Item 3" },
                    new ItemViewModel() { Name = "Item 4" },
                    new ItemViewModel() { Name = "Item 5" }
                };
        }
    }

    public class ItemViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string _name;
        public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } }
    }
}
Community
  • 1
  • 1
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • Now what should I do? Roel solved my problem with a one-liner and I promised him the bounty in chat, yet both of your solutions show great effort and are more elegant. IMO the "semantically correct" solution would be to accept his answer (because I used his solution after all) and give you the bounty. Do you care about the reputation? – marczellm Aug 13 '14 at 22:40
  • @marczellm accept this answer and give him the bounty. This is the most elegant solution without disregarding your requirements. – Roel van Westerop Aug 14 '14 at 06:08
  • 1
    No, I don't care about the reputation. Do whatever you feel is right. I would like to get the accepted answer more than the bounty, because this influences people more for what they will use, and I obviously think that my suggestions are more appealing than the NullItemSelectorAdapter and Converters, but you decide and I will not gripe. – Mike Fuchs Aug 14 '14 at 07:59
1

If it's possible to inherit the NullItem class from MyModel.Product, the conversion will succeed. Or, if inheritance is not possible, wrap the objects and bind to them.

Edit after discussion: If you change the type of the property you bind the selecteditem to to object, the errors will disappear.

Roel van Westerop
  • 1,400
  • 8
  • 17
  • Cannot inherit, because there are different types involved in different ComboBoxes. – marczellm Aug 12 '14 at 10:22
  • Now what should I do? You solved my problem with a one-liner and I promised you the bounty in chat, but adabyron's solutions show great effort and are quite elegant, too. IMO the "semantically correct" solution would be to accept your answer (because I used your solution after all) and give him the bounty. Do you care about the reputation? – marczellm Aug 13 '14 at 22:41
1

While I can understand the null item approach, because it's been used many times, I find it much more clean to make a difference between

  • Selecting an item
  • Removing your selection

thus not "selecting nothing by selecting something", especially if the requirement is for the SelectedItem to be null.

I would suggest creating a custom control, extending the combobox with a reset button:

enter image description here

enter image description here

enter image description here

enter image description here

The custom control:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1.Controls
{
    [TemplatePart(Name = "PART_ResetButton", Type = typeof(Button))]
    public class ComboBoxReset : ComboBox
    {
        private Button _resetButton;

        // reset event (not used in this demo case, but should be provided)
        public static readonly RoutedEvent ResetEvent = EventManager.RegisterRoutedEvent("Reset", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ComboBoxReset));
        public event RoutedEventHandler Reset { add { AddHandler(ResetEvent, value); } remove { RemoveHandler(ResetEvent, value); } }
        private void OnReset()
        {
            RoutedEventArgs args = new RoutedEventArgs(ResetEvent);
            RaiseEvent(args);
        }

        public ComboBoxReset()
        {
            // lookless control, get default style from generic.xaml
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ComboBoxReset), new FrameworkPropertyMetadata(typeof(ComboBoxReset)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (this.Template != null)
            {
                // find reset button in template
                Button btn = this.Template.FindName("PART_ResetButton", this) as Button;
                if (_resetButton != btn)
                {
                    // detach old handler
                    if (_resetButton != null)
                        _resetButton.Click -= ResetButton_Click;

                    _resetButton = btn;

                    // attach new handler
                    if (_resetButton != null)
                        _resetButton.Click += ResetButton_Click;
                }
            }
        }

        private void ResetButton_Click(object sender, RoutedEventArgs e)
        {
            // reset the selected item and raise the event
            this.SelectedItem = null;
            OnReset();
        }
    }
}

For the style, basically just get the default template of a normal ComboBox through VS designer, add the button (look for PART_ResetButton in the code below), change the TargetType (to ComboBoxReset), put it in Themes\generic.xaml. Not much to it. Here's how the style looked for me:

<Style x:Key="ComboBoxFocusVisual">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate>
                <Rectangle Margin="4,4,21,4" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0">
    <GradientStop Color="#F3F3F3" Offset="0"/>
    <GradientStop Color="#EBEBEB" Offset="0.5"/>
    <GradientStop Color="#DDDDDD" Offset="0.5"/>
    <GradientStop Color="#CDCDCD" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/>
<Geometry x:Key="DownArrowGeometry">M 0 0 L 3.5 4 L 7 0 Z</Geometry>
<Style x:Key="ComboBoxReadonlyToggleButton" TargetType="{x:Type ToggleButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="ClickMode" Value="Press"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" SnapsToDevicePixels="true">
                    <Grid HorizontalAlignment="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}">
                        <Path x:Name="Arrow" Data="{StaticResource DownArrowGeometry}" Fill="Black" HorizontalAlignment="Center" Margin="3,1,0,0" VerticalAlignment="Center"/>
                    </Grid>
                </Themes:ButtonChrome>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Fill" TargetName="Arrow" Value="#AFAFAF"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<LinearGradientBrush x:Key="TextBoxBorder" EndPoint="0,20" MappingMode="Absolute" StartPoint="0,0">
    <GradientStop Color="#ABADB3" Offset="0.05"/>
    <GradientStop Color="#E2E3EA" Offset="0.07"/>
    <GradientStop Color="#E3E9EF" Offset="1"/>
</LinearGradientBrush>
<Style x:Key="ComboBoxEditableTextBox" TargetType="{x:Type TextBox}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="MinWidth" Value="0"/>
    <Setter Property="MinHeight" Value="0"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <ScrollViewer x:Name="PART_ContentHost" Background="Transparent" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<Style x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="ClickMode" Value="Press"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Themes:ButtonChrome x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}" RoundCorners="false" SnapsToDevicePixels="true" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}">
                    <Path x:Name="Arrow" Data="{StaticResource DownArrowGeometry}" Fill="Black" HorizontalAlignment="Center" Margin="0,1,0,0" VerticalAlignment="Center"/>
                </Themes:ButtonChrome>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Fill" TargetName="Arrow" Value="#AFAFAF"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<ControlTemplate x:Key="ComboBoxEditableTemplate" TargetType="{x:Type ComboBox}">
    <Grid x:Name="Placement" SnapsToDevicePixels="true">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom">
            <Themes:SystemDropShadowChrome x:Name="Shdw" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=Placement}">
                <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>
                            <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </Grid>
                    </ScrollViewer>
                </Border>
            </Themes:SystemDropShadowChrome>
        </Popup>
        <Themes:ListBoxChrome x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderFocused="{TemplateBinding IsKeyboardFocusWithin}"/>
        <TextBox x:Name="PART_EditableTextBox" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsReadOnly="{Binding IsReadOnly, RelativeSource={RelativeSource TemplatedParent}}" Margin="{TemplateBinding Padding}" Style="{StaticResource ComboBoxEditableTextBox}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
        <ToggleButton Grid.Column="1" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxToggleButton}"/>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="IsKeyboardFocusWithin" Value="true">
            <Setter Property="Foreground" Value="Black"/>
        </Trigger>
        <Trigger Property="IsDropDownOpen" Value="true">
            <Setter Property="RenderFocused" TargetName="Border" Value="true"/>
        </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>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsGrouping" Value="true"/>
                <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
            </MultiTrigger.Conditions>
            <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
        </MultiTrigger>
        <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="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>
<Style TargetType="{x:Type ctrl:ComboBoxReset}">
    <Setter Property="FocusVisualStyle" Value="{StaticResource ComboBoxFocusVisual}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
    <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
    <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="Padding" Value="4,3"/>
    <Setter Property="Height" Value="22"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="ScrollViewer.PanningMode" Value="Both"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ctrl:ComboBoxReset}">
                <Grid x:Name="MainGrid" SnapsToDevicePixels="true">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
                        <ColumnDefinition Width="Auto" />
                    </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">
                        <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>
                                        <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                    </Grid>
                                </ScrollViewer>
                            </Border>
                        </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}"/>
                    <Button x:Name="PART_ResetButton" Grid.Column="2" Margin="2,0,0,0" >
                        <Image Stretch="Uniform" Source="/WpfApplication1;component/Resources/remove.png" />
                    </Button>
                </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>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsGrouping" Value="true"/>
                            <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </MultiTrigger>
                    <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.Triggers>
        <Trigger Property="IsEditable" Value="true">
            <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
            <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="IsTabStop" Value="false"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="Template" Value="{StaticResource ComboBoxEditableTemplate}"/>
        </Trigger>
    </Style.Triggers>
</Style>

The implementation (producing the screenshots above):

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ctrl="clr-namespace:WpfApplication1.Controls"
        xmlns:vm="clr-namespace:WpfApplication1.ViewModels"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <vm:ComboBoxResetViewModel />
    </Window.DataContext>

    <StackPanel Orientation="Horizontal" VerticalAlignment="Top" >
        <ctrl:ComboBoxReset ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" DisplayMemberPath="Name" Margin="5,2" Width="150" />
        <TextBlock Text="SelectedItem:" FontWeight="SemiBold"  Margin="50,2,0,2" VerticalAlignment="Center" />
        <TextBlock Text="{Binding SelectedItem.Name, FallbackValue='null'}" Margin="5,2" VerticalAlignment="Center" />
    </StackPanel>
</Window>

And finally the viewmodel I used for testing:

using System.Collections.Generic;
using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class ComboBoxResetViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private List<ItemViewModel> _items;
        public List<ItemViewModel> Items { get { return _items; } set { _items = value; OnPropertyChanged("Items"); } }

        private ItemViewModel _selectedItem;
        public ItemViewModel SelectedItem { get { return _selectedItem; } set { _selectedItem = value; OnPropertyChanged("SelectedItem"); } }

        public ComboBoxResetViewModel()
        {
            this.Items = new List<ItemViewModel>()
            {
                new ItemViewModel() { Name = "Item 1" },
                new ItemViewModel() { Name = "Item 2" },
                new ItemViewModel() { Name = "Item 3" },
                new ItemViewModel() { Name = "Item 4" },
                new ItemViewModel() { Name = "Item 5" }
            };
        }
    }

    public class ItemViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string _name;
        public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } }
    }
}
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • I'll take a look at your solution. In my case there used to be a checkbox next to the combobox, and by checking that, you could turn off filtering by that value. But the client requested that I turn it into an "(all)" item in the ComboBox. – marczellm Aug 13 '14 at 18:55