使用NullItemSelectorAdapter避免转换错误和相关的渲染延迟

时间:2014-08-04 19:30:55

标签: c# wpf combobox type-conversion itemscontrol

在我的WPF应用程序中,我有一个DataGrid,我希望用户能够过滤显示哪些行。过滤是这样实现的:在GUI上有一个ComboBox,它枚举了某些属性的可能值,让我们称之为SomeProperty。当用户选择一个值(例如"Value1")时,DataGrid将仅显示item.SomeProperty == "Value1"的项目。 DataGrid和ComboBox内容都来自数据库。

我希望用户能够通过SomeProperty关闭过滤,所以我找了一种方法将"all"项添加到ComboBox,返回null并且我可以在我的过滤逻辑中使用。我发现了这个:

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

这是一个包装类,它将空项添加到ComboBox或类似项。由于我还在使用ComboBox.DisplayMemberPath属性,因此我更改了

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

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

并添加了这样一个类:

[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;
    }
}

为了能够像这样使用它(省略不相关的属性):

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

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

ItemsSource中的对象是生成的类的实例,因此我无法覆盖其ToString方法。)

当我调用Application.MainWindow.Show()时,所有这些ComboBox都被实例化了,我得到了很多这样的错误:

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)'

我指定的TypeConverter没有实例化,即使它应该符合reference sources of MS.Internal.Data.DefaultValueConverter

这些错误不会导致程序崩溃(之后运行正常),但是即使在快速计算机上,它们也会在窗口内容呈现时引起明显的延迟。怎么能让这种延迟消失?

我主要对一个解决方案感兴趣,该解决方案不涉及在Converter的用法上为每个Binding手动添加NullItemSelectorAdapter,因为这很多。我希望通过在NullItemSelectorAdapter和NullItem类中进行黑客攻击来解决这个问题。

解决方案: 下面的Roel's answer是我采用的解决方案,因为它是一个让所提到的错误消失的单线技巧。但是adabyron's accepted answer是语义上更正确,更优雅的解决方案,您应该使用它。

3 个答案:

答案 0 :(得分:2)

第二个建议,在OP明确表示他的客户坚持无效项目之后。 我很遗憾地说我再次忽略了你的一个要求,即SelectedItem为null。但是(正如我在第一个答案中以不同的方式所述),

  • 有一些东西需要添加到ComboBox(null / all-item)
  • 但实际上是在添加null(或者在幕后翘曲它所以似乎那样)

只是没有为我而去。

从好的方面来说,由于过滤机制肯定在您的控制之下,我想您应该能够使用以下内容。如果您不想将行为添加到每个ComboBox,可以使用this code以隐式样式应用它。

我创建了一个ComboBox行为,它通常会插入“ - All - ”项,并在过滤中添加了一个IsNullItemSelected属性,而不是SelectedItem == null。 当前(已知)限制:我希望ItemsSource是一个IList,包含的项应该是字符串或具有无参数构造函数。

enter image description here

enter image description here

enter image description here

enter image description here

行为:

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);
        }
    }
}

实现:

<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>

视图模型:

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"); } }
    }
}

答案 1 :(得分:1)

如果可以从MyModel.Product继承NullItem类,则转换将成功。或者,如果无法继承,请包装对象并绑定它们。

讨论后编辑: 如果更改将selecteditem绑定到object的属性类型,则错误将消失。

答案 2 :(得分:1)

虽然我可以理解零项目的方法,因为它已被多次使用,我发现它更加干净,以区别

  • 选择项目
  • 删除您的选择

因此不会“通过选择某事来选择任何东西”,特别是如果要求是SelectedItem为null。

我建议创建一个自定义控件,使用重置按钮扩展组合框:

enter image description here

enter image description here

enter image description here

enter image description here

自定义控件:

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();
        }
    }
}

对于样式,基本上只需通过VS设计器获取普通ComboBox的默认模板,添加按钮(在下面的代码中查找PART_ResetButton),更改TargetType(到ComboBoxReset),把它放在Themes \ generic.xaml中。不是很多。这就是风格对我的影响:

<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>

实施(制作上面的截图):

<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>

最后我用于测试的视图模型:

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"); } }
    }
}