WPF ComboBox: Set SelectedItem to item not in ItemsSource -> Binding oddity
Solution 1
In short, you can't set SelectedItem
to the value, that is not in ItemsSource
. AFAIK, this is default behavior of all Selector
descendants, which is rather obvious: settings SelectedItem
isn't only a data changing, this also should lead to some visual consequences like generating an item container and re-drawing item (all those things manipulate ItemsSource
). The best you can do here is code like this:
public DemoViewModel()
{
selected = Source.FirstOrDefault(s => s == yourValueFromSettings);
}
Another option is to allow user to enter arbitrary values in ComboBox
by making it editable.
Solution 2
I realize this is a bit late to help you, but I hope that it helps someone at least. I'm sorry if there are some typos, I had to type this in notepad:
ComboBoxAdaptor.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
namespace Adaptors
{
[ContentProperty("ComboBox")]
public class ComboBoxAdaptor : ContentControl
{
#region Protected Properties
protected bool IsChangingSelection
{ get; set; }
protected ICollectionView CollectionView
{ get; set; }
#endregion
#region Dependency Properties
public static readonly DependencyProperty ComboBoxProperty =
DependencyProperty.Register("ComboBox", typeof(ComboBox), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(new PropertyChangedCallback(ComboBox_Changed)));
private static void ComboBox_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var theComboBoxAdaptor = (ComboBoxAdaptor)d;
theComboBoxAdaptor.ComboBox.SelectionChanged += theComboBoxAdaptor.ComboBox_SelectionChanged;
}
public ComboBox ComboBox
{
get { return (ComboBox)GetValue(ComboBoxProperty); }
set { SetValue(ComboBoxProperty, value); }
}
public static readonly DependencyProperty NullItemProperty =
DependencyProperty.Register("NullItem", typeof(object), typeof(ComboBoxAdaptor),
new PropertyMetadata("(None)"));
public object NullItem
{
get { return GetValue(NullItemProperty); }
set { SetValue(NullItemProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(new PropertyChangedCallback(ItemsSource_Changed)));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(SelectedItem_Changed)));
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty AllowNullProperty =
DependencyProperty.Register("AllowNull", typeof(bool), typeof(ComboBoxAdaptor),
new PropertyMetadata(true, AllowNull_Changed));
public bool AllowNull
{
get { return (bool)GetValue(AllowNullProperty); }
set { SetValue(AllowNullProperty, value); }
}
#endregion
#region static PropertyChangedCallbacks
static void ItemsSource_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
adapter.Adapt();
}
static void AllowNull_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
adapter.Adapt();
}
static void SelectedItem_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
if (adapter.ItemsSource != null)
{
//If SelectedItem is changing from the Source (which we can tell by checking if the
//ComboBox.SelectedItem is already set to the new value), trigger Adapt() so that we
//throw out any items that are not in ItemsSource.
object adapterValue = (e.NewValue ?? adapter.NullItem);
object comboboxValue = (adapter.ComboBox.SelectedItem ?? adapter.NullItem);
if (!object.Equals(adapterValue, comboboxValue))
{
adapter.Adapt();
adapter.ComboBox.SelectedItem = e.NewValue;
}
//If the NewValue is not in the CollectionView (and therefore not in the ComboBox)
//trigger an Adapt so that it will be added.
else if (e.NewValue != null && !adapter.CollectionView.Contains(e.NewValue))
{
adapter.Adapt();
}
}
}
#endregion
#region Misc Callbacks
void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ComboBox.SelectedItem == NullItem)
{
if (!IsChangingSelection)
{
IsChangingSelection = true;
try
{
int selectedIndex = ComboBox.SelectedIndex;
ComboBox.SelectedItem = null;
ComboBox.SelectedIndex = -1;
ComboBox.SelectedIndex = selectedIndex;
}
finally
{
IsChangingSelection = false;
}
}
}
object newVal = (ComboBox.SelectedItem == null ? null : ComboBox.SelectedItem);
if (!object.Equals(SelectedItem, newVal))
{
SelectedItem = newVal;
}
}
void CollectionView_CurrentChanged(object sender, EventArgs e)
{
if (AllowNull && (ComboBox != null) && (((ICollectionView)sender).CurrentItem == null) && (ComboBox.Items.Count > 0))
{
ComboBox.SelectedIndex = 0;
}
}
#endregion
#region Methods
protected void Adapt()
{
if (CollectionView != null)
{
CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
CollectionView = null;
}
if (ComboBox != null && ItemsSource != null)
{
CompositeCollection comp = new CompositeCollection();
//If AllowNull == true, add a "NullItem" as the first item in the ComboBox.
if (AllowNull)
{
comp.Add(NullItem);
}
//Now Add the ItemsSource.
comp.Add(new CollectionContainer { Collection = ItemsSource });
//Lastly, If Selected item is not null and does not already exist in the ItemsSource,
//Add it as the last item in the ComboBox
if (SelectedItem != null)
{
List<object> items = ItemsSource.Cast<object>().ToList();
if (!items.Contains(SelectedItem))
{
comp.Add(SelectedItem);
}
}
CollectionView = CollectionViewSource.GetDefaultView(comp);
if (CollectionView != null)
{
CollectionView.CurrentChanged += CollectionView_CurrentChanged;
}
ComboBox.ItemsSource = comp;
}
}
#endregion
}
}
How To Use It In Xaml
<adaptor:ComboBoxAdaptor
NullItem="Please Select an Item.."
ItemsSource="{Binding MyItemsSource}"
SelectedItem="{Binding MySelectedItem}">
<ComboBox Width="100" />
</adaptor:ComboBoxAdaptor>
If you find that the ComboBox
is not showing...
Then do remember to link the ComboBox
styling to the content of the ComboBoxAdaptor
<Style TargetType="Adaptors:ComboBoxAdaptor">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Adaptors:ComboBoxAdaptor">
<ContentPresenter Content="{TemplateBinding ComboBox}"
Margin="{TemplateBinding Padding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Some Notes
If SelectedItem
changes to a value not in the ComboBox
, it will be added to the ComboBox
(but not the ItemsSource). The next time SelectedItem
is changed via Binding
, any items not in ItemsSource
will be removed from the ComboBox
.
Also, the ComboBoxAdaptor
allows you to insert a Null item into the ComboBox
. This is an optional feature that you can turn off by setting AllowNull="False"
in the xaml.
Related videos on Youtube
Andreas Duering
I got in touch with (approx. order of experience) C#, C++, C, Python, Java (and very little Ruby, Perl, PHP). I started with C++ in school, tinkered around with Python (and some other languages) on my own and deal with C (for microcontrollers) and C# (Windows application) at my job.
Updated on June 26, 2022Comments
-
Andreas Duering almost 2 years
I want to achieve the following: I want to have a ComboBox which displays the available COM ports. On Startup (and clicking a "refresh" button) I want to get the available COM ports and set the selection to the last selected value (from the application settings).
If the value from the settings (last com port) is not in the list of values (available COM ports) following happens:
Although the ComboBox doesn't display anything (it's "clever enough" to know that the new SelectedItem is not in ItemsSource), the ViewModel is updated with the "invalid value". I actually expected that the Binding has the same value which the ComboBox displays.
Code for demonstration purposes:
MainWindow.xaml:
<Window x:Class="DemoComboBinding.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:local="clr-namespace:DemoComboBinding"> <Window.Resources> <local:DemoViewModel x:Key="vm" /> </Window.Resources> <StackPanel Orientation="Vertical"> <ComboBox SelectedItem="{Binding Source={StaticResource vm}, Path=Selected}" x:Name="combo" ItemsSource="{Binding Source={StaticResource vm}, Path=Source}"/> <Button Click="Button_Click">Set different</Button> <!-- would be refresh button --> <Label Content="{Binding Source={StaticResource vm}, Path=Selected}"/> <!-- shows the value from the view model --> </StackPanel> </Window>
MainWindow.xaml.cs:
// usings removed namespace DemoComboBinding { public partial class MainWindow : Window { //... private void Button_Click(object sender, RoutedEventArgs e) { combo.SelectedItem = "COM4"; // would be setting from Properties } } }
ViewModel:
namespace DemoComboBinding { class DemoViewModel : INotifyPropertyChanged { string selected; string[] source = { "COM1", "COM2", "COM3" }; public string[] Source { get { return source; } set { source = value; } } public string Selected { get { return selected; } set { if(selected != value) { selected = value; OnpropertyChanged("Selected"); } } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; void OnpropertyChanged(string propertyname) { var handler = PropertyChanged; if(handler != null) { handler(this, new PropertyChangedEventArgs(propertyname)); } } #endregion } }
A solution I initially came up with would be to check inside the Selected setter if the value to set is inside the list of available COM ports (if not, set to empty string and send OPC).
What I wonder: Why does that happen? Is there another solution I didn't see?
-
Federico Berasategui over 9 yearsimportant to mention that WPF uses the GetHashCode() + Equals() stuff to determine whether an object "is part" of the ItemsSource Collection. So objects evaluating as Equals() == true will not have this problem.