2
votes

How do you get the SelectedItem of a ComboBox to show even when it is not in the ItemsSource?

just as a simple example...

Suppose I have a "Class" object with a "Teacher" property on it.

public class Class: INotifyPropertyChanged
{
   private Individual _teacher
   public Individual Teacher
   {
      get { return _teacher; }
      set 
      {
         teacher = value;
         RaisePropertyChanged("Teacher");
      }
   }
   ...
}

On the "Maintain Classes" GUI, there is a ComboBox to select a Teacher, and I only want active individuals to show up in the ComboBox. And I don't want users to be able to type free form text into the ComboBox. To achieve this, I bind ItemsSource to a collection in my ViewModel that only includes active individuals and the SelectedItem bound to a Teacher property of my "Class" object.

public class MaintainClasses_ViewModel:INotifyPropertyChanged
{
    private ObservableCollection<Individual> _activeIndividuals
        = GetAllActiveIndividuals();

    public ObservableCollection<Individual> ActiveIndividuals
    {
        get { return _activeIndividuals
    }

    public Class SelectedClass
    {
        get; 
        set;
    }
} 

with the xaml for my ComboBox being...

<ComboBox ItemsSource="{Binding ActiveIndividuals}" 
          SelectedItem="{Binding SelectedClass.Teacher}" />

Now suppose I open the "Maintain Classes" GUI for a class where the teacher that has already been saved with is now inactive. Now... I want only active individuals to show up in the combobox -PLUS the teacher that was previously selected (even though they are now inactive and NOT in the ItemsSource).

Currently, the only way I have found to do this is to add the Inactive Individual to the collection and raise the PropertyChanged event for the collection. However, I would really like to archive this result without adding things to the collection. Preferably some method that uses xaml, selectors, and/or converters.

1
Couldnt understand you question :(WPFUser
I had similar problem a while ago, one of the suggestions that I came across was to make the combobox editable, because that's the only time when ComboBox allows values outside the collection that is bound to. I personally didn't go with that approach due to the requirements of my app. My approach was involving MenuItem instead of ComboBox. HTHXAMlMAX
nice name TheodosiusTheodosius Von Richthofen
To clarify my question ... Since the property that the SelectItem property is bound to is not also in the collection that the ItemsSource is bound to, the combobox doesn't show anything as being selected. I would like it to show the Selected Item in the combobox even if it is NOT in the ItemsSource. Making the combobox editable would achieve this... I think, but I don't want the user to be able to type values in... just select ones from the list (like a dropdownlist in winforms).Theodosius

1 Answers

1
votes

Here is what I've been using, I hope it Helps:

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)));
            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>

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.