I want to make a ComboBox in WPF that has one null
item on the top, when this gets selected, the SelectedItem should be set to null (reset to default state). I've searched like forever, but didn't find a solution that was satisfying.
If possible I would want it to do it only with XAML code or an attached behaviour, because I don't really like changing stuff in the ViewModel for the View, or overriding standard controls.
Here is what I've come up with so far (shortened code):
[...]
<Popup x:Name="PART_Popup" [...]>
<Border x:Name="PopupBorder" [...]>
<ScrollViewer x:Name="DropDownScrollViewer" [...]>
<StackPanel [...]>
<ComboBoxItem>(None)</ComboBoxItem>
<ItemsPresenter x:Name="ItemsPresenter"/>
</StackPanel>
</ScrollViewer>
</Border>
</Popup>
[...]
I think the best way would be to somehow add an event trigger that sets the SelectedIndex
to -1
when the item gets selected, but here is where I've got stuck.
Any ideas how to do this? Or an better way, like an attached behaviour?
Here's the ultimate super-simple solution to this problem:
Instead of having an item with a value of null in your ItemsSource, use DbNull.Value as item or as the item's value property.
That's all. You're done. No value converters, no code-behind, no xaml triggers, no wrappers, no control descendants...
It simply works!
Here's a short example for binding enum values including a "null item":
Create your ItemsSource like this:
var enumValues = new ArrayList(Enum.GetValues(typeof(MyEnum)));
enumValues.Insert(0, DBNull.Value);
return enumValues;
Bind this to the ItemsSource of the ComboBox.
Bind the SelectedValue of your ComboBox to any Property having a Type of MyEnum? (i.e. Nullable<MyEnum>).
Done!
Background: This approach works because DbNull.Value is not the same like a C# null value, while on the other hand the framework includes a number of coercion methods to convert between those two. Eventually, this is similar to the mentioned "Null object pattern", but without the need for creating an individual null object and without the need for any value converters.
A little more elaborate than some answers here, but didn't want to have any code behind or ViewModel changes in mine. I wrote this as a WPF behavior. When attached to the XAML, it will inject a button in the visual. It will set the Default value of -1 (or you can adjust to be something else default). This is a re-usable control that is easy to add on your XAML throughout your project. Hope this helps. Open to feedback if you spot an error.
Resulting Visual:
Item Selected:
Behavior Code:
public class ComboBoxClearBehavior : Behavior<ComboBox>
{
private Button _addedButton;
private ContentPresenter _presenter;
private Thickness _originalPresenterMargins;
protected override void OnAttached()
{
// Attach to the Loaded event. The visual tree at this point is not available until its loaded.
AssociatedObject.Loaded += AssociatedObject_Loaded;
// If the user or code changes the selection, re-evaluate if we should show the clear button
AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
base.OnAttached();
}
protected override void OnDetaching()
{
// Its likely that this is already de-referenced, but just in case the visual was never loaded, we will remove the handler anyways.
AssociatedObject.Loaded -= AssociatedObject_Loaded;
base.OnDetaching();
}
private void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
EvaluateDisplay();
}
/// <summary>
/// Checks to see if the UI should show a Clear button or not based on what is or isn't selected.
/// </summary>
private void EvaluateDisplay()
{
if (_addedButton == null) return;
_addedButton.Visibility = AssociatedObject.SelectedIndex == -1 ? Visibility.Collapsed : Visibility.Visible;
// To prevent the text or content from being overlapped by the button, adjust the margins if we have reference to the presenter.
if (_presenter != null)
{
_presenter.Margin = new Thickness(
_originalPresenterMargins.Left,
_originalPresenterMargins.Top,
_addedButton.Visibility == Visibility.Visible ? ClearButtonSize + 6 : _originalPresenterMargins.Right,
_originalPresenterMargins.Bottom);
}
}
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
// After we have loaded, we will have access to the Children objects. We don't want this running again.
AssociatedObject.Loaded -= AssociatedObject_Loaded;
// The ComboBox primary Grid is named MainGrid. We need this to inject the button control. If missing, you may be using a custom control.
if (!(AssociatedObject.FindChild("MainGrid") is Grid grid)) return;
// Find the content presenter. We need this to adjust the margins if the Clear icon is present.
_presenter = grid.FindChildren<ContentPresenter>().FirstOrDefault();
if (_presenter != null) _originalPresenterMargins = _presenter.Margin;
// Create the new button to put in the view
_addedButton = new Button
{
Height = ClearButtonSize,
Width = ClearButtonSize,
HorizontalAlignment = HorizontalAlignment.Right
};
// Find the resource for the button - In this case, our NoChromeButton Style has no button edges or chrome
if (Application.Current.TryFindResource("NoChromeButton") is Style style)
{
_addedButton.Style = style;
}
// Find the resource you want to put in the button content
if (Application.Current.TryFindResource("RemoveIcon") is FrameworkElement content)
{
_addedButton.Content = content;
}
// Hook into the Click Event to handle clearing
_addedButton.Click += ClearSelectionButtonClick;
// Evaluate if we should display. If there is nothing selected, don't show.
EvaluateDisplay();
// Add the button to the grid - First Column as it will be right justified.
grid.Children.Add(_addedButton);
}
private void ClearSelectionButtonClick(object sender, RoutedEventArgs e)
{
// Sets the selected index to -1 which will set the selected item to null.
AssociatedObject.SelectedIndex = -1;
}
/// <summary>
/// The Button Width and Height. This can be changed in the Xaml if a different size visual is desired.
/// </summary>
public int ClearButtonSize { get; set; } = 15;
}
Usage:
<ComboBox
ItemsSource="{Binding SomeItemsSource, Mode=OneWay}"
SelectedValue="{Binding SomeId, Mode=TwoWay}"
SelectedValuePath="SomeId">
<i:Interaction.Behaviors>
<behaviors:ComboBoxClearBehavior />
</i:Interaction.Behaviors>
</ComboBox>
You will need two things for this Behavior -- you may already have them, but here they are:
1.) The Button Template - The code is looking for a style. In my case, it's called NoChromeButton- If you are looking for a turnkey solution, you can add mine to your resources file:
<Style x:Key="NoChromeButton"
TargetType="{x:Type Button}">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="Foreground"
Value="{DynamicResource WindowText}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="Padding"
Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid x:Name="Chrome"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="true">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="#ADADAD" />
<Setter Property="Opacity"
TargetName="Chrome"
Value="0.5" />
</Trigger>
<Trigger
Property="IsMouseOver"
Value="True">
<Setter
TargetName="Chrome"
Property="Background"
Value="{DynamicResource ButtonBackgroundHover}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Also you will need your icon for the clear. If you have one, just update the code to use that resource (named "RemoveIcon"). Otherwize.. here is mine:
<Viewbox x:Key="RemoveIcon"
x:Shared="False"
Stretch="Uniform">
<Canvas Width="58"
Height="58">
<Path Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Control, Mode=FindAncestor}}">
<Path.Data>
<PathGeometry Figures="M 29 0 C 13 0 0 13 0 29 0 45 13 58 29 58 45 58 58 45 58 29 58 13 45 0 29 0 Z M 43.4 40.6 40.6 43.4 29 31.8 17.4 43.4 14.6 40.6 26.2 29 14.6 17.4 17.4 14.6 29 26.2 40.6 14.6 43.4 17.4 31.8 29 Z"
FillRule="NonZero" />
</Path.Data>
</Path>
</Canvas>
</Viewbox>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With