WPF ComboBox: Set SelectedItem to item not in ItemsSource -> Binding oddity

11,305

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.

Share:
11,305

Related videos on Youtube

Andreas Duering
Author by

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

Comments

  • Andreas Duering
    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
    Federico Berasategui over 9 years
    important 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.