Original last user turn
Q: Avoiding conversion errors and associated rendering delay with NullItemSelectorAdapter
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.
A: 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.
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"); } }
}
}
A: 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.
A: 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:
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"); } }
}
}
What are some limitations of the NullableComboBoxBehavior proposed in the second answer?8993 rendered input tokens. Last 192 rendered input tokens retained.