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\
Remove the following line and add a CheckBox, then you can perform your custom operation.
<ComboBoxItem>(None)</ComboBoxItem>
Still not 100% happy with this solution, but the best thing I found so far, you only need to override the ComboBox Style and apply an AttachedBehaviour
.
<ComboBox ItemsSource="{Binding Names}"
ext:ComboBoxHelper.IsNullable="True" />
Source: http://xamlblog.com/PostPage.aspx?postId=16#/Posts/16
Edit: Link to the Internet Archive since the link is broken: https://web.archive.org/web/20160420174905/http://xamlblog.com/PostPage.aspx?postId=16
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>