Phil On .NET

A Developer's Blog

  • Categories

  • Archives

How to Select Null/None in a ComboBox/ListBox/ListView

Posted by Phil on September 18, 2009

When using a Selector control like ComboBox, ListBox, or ListView to present a non-required/optional field, you might want a special item like “(None)” to appear in the list, representing null/DBNull. This is a fairly common scenario, particularly with nullable foreign keys, but how to accommodate this requirement in the view may not be straightforward when you’re data-binding the ItemsSource, which is typically the case.

Although you could add a special object representing null (which we’ll call the “None” item) to the source collection itself, for practical and philosophical reasons you probably don’t want to modify the collection just to accommodate a particular presentation of it. Of course, if your collection consists of individual data wrappers or view models, adding the None object to the collection may not be an issue. Regardless, we’ll proceed with the assumption that you don’t want to modify the source collection.

A nice way to include the None item along with the data objects in the same list is to use a CompositeCollection. Binding to a CompositeCollection allows you to combine objects from multiple sources, which in our case would be the None item and the real data objects, so they all appear in a single list. However, this isn’t a complete solution because selection changes and source collection currency aren’t kept synchronized.

To more fully solve the problem I created a NullItemSelectorAdapter (code below), a ContentControl derivative whose purpose is to add a None item to the data items presented in a Selector (internally using a CompositeCollection to accomplish this), while keeping the current selection and the bound collection’s current item in sync.

In XAML just wrap a NullItemSelectorAdapter around your Selector control. In the following example, assume the view model (DataContext) has a CustomerList property and a SelectedCustomer property, and Customer is a data object class with a Name property.

<local:NullItemSelectorAdapter ItemsSource="{Binding CustomerList}">
    <ComboBox Name="ComboBoxCustomer" SelectedItem="{Binding SelectedCustomer}">
        <ComboBox.Resources>
            <DataTemplate DataType="{x:Type model:Customer}">
                <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
        </ComboBox.Resources>
    </ComboBox>
</local:NullItemSelectorAdapter>

Here’s the code for the NullItemSelectorAdapter:

using System;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;

namespace PhilOnDotNet.NullItemSelectionSample.View
{
    /// <summary>
    /// Adapts a <see cref="Selector"/> control to include an item representing null.
    /// This element is a <see cref="ContentControl"/> whose <see cref="ContentControl.Content"/>
    /// should be a Selector, such as a <see cref="ComboBox"/>, <see cref="ListBox"/>,
    /// or <see cref="ListView"/>.
    /// </summary>
    /// <remarks>
    /// In XAML, place this element immediately outside the target Selector, and set the
    /// <see cref="ItemsSource"/> property instead of the Selector's ItemsSource.
    /// </remarks>
    /// <example>
    /// <code>
    /// <local:NullItemSelectorAdapter ItemsSource=&quot;{Binding CustomerList}&quot;>
    ///     <ComboBox .../>
    /// </local:NullItemSelectorAdapter>
    /// </code>
    /// </example>
    [ContentProperty("Selector")]
    public class NullItemSelectorAdapter : ContentControl
    {
        ICollectionView _collectionView;
        /// <summary>
        /// Gets or sets the collection view associated with the internal <see cref="CompositeCollection"/>
        /// that combines the null-representing item and the <see cref="ItemsSource"/>.
        /// </summary>
        protected ICollectionView CollectionView
        {
            get { return _collectionView; }
            set { _collectionView = value; }
        }

        /// <summary>
        /// Identifies the <see cref="Selector"/> property.
        /// </summary>
        public static readonly DependencyProperty SelectorProperty = DependencyProperty.Register(
            "Selector", typeof(Selector), typeof(NullItemSelectorAdapter),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(Selector_Changed)));
        static void Selector_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            NullItemSelectorAdapter adapter = (NullItemSelectorAdapter)sender;
            adapter.Content = e.NewValue;
            Selector selector = (Selector)e.OldValue;
            if (selector != null) selector.SelectionChanged -= adapter.Selector_SelectionChanged;
            selector = (Selector)e.NewValue;
            if (selector != null)
            {
                selector.IsSynchronizedWithCurrentItem = true;
                selector.SelectionChanged += adapter.Selector_SelectionChanged;
            }
            adapter.Adapt();
        }

        /// <summary>
        /// Gets or sets the Selector control.
        /// </summary>
        public Selector Selector
        {
            get { return (Selector)GetValue(SelectorProperty); }
            set { SetValue(SelectorProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="ItemsSource"/> property.
        /// </summary>
        public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
            "ItemsSource", typeof(IEnumerable), typeof(NullItemSelectorAdapter),
            new FrameworkPropertyMetadata(new PropertyChangedCallback(ItemsSource_Changed)));
        static void ItemsSource_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            NullItemSelectorAdapter adapter = (NullItemSelectorAdapter)sender;
            adapter.Adapt();
        }

        /// <summary>
        /// Gets or sets the data items.
        /// </summary>
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        /// <summary>
        /// Identifies the <see cref="NullItem"/> property.
        /// </summary>
        public static readonly DependencyProperty NullItemProperty = DependencyProperty.Register(
            "NullItem", typeof(object), typeof(NullItemSelectorAdapter), new PropertyMetadata("(None)"));

        /// <summary>
        /// Gets or sets the null-representing object to display in the Selector.
        /// (The default is the string &quot;(None)&quot;.)
        /// </summary>
        public object NullItem
        {
            get { return GetValue(NullItemProperty); }
            set { SetValue(NullItemProperty, value); }
        }

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        public NullItemSelectorAdapter()
        {
            IsTabStop = false;
        }

        /// <summary>
        /// Updates the Selector control's <see cref="ItemsControl.ItemsSource"/> to include the
        /// <see cref="NullItem"/> along with the objects in <see cref="ItemsSource"/>.
        /// </summary>
        protected void Adapt()
        {
            if (CollectionView != null)
            {
                CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
                CollectionView = null;
            }
            if (Selector != null && ItemsSource != null)
            {
                CompositeCollection comp = new CompositeCollection();
                comp.Add(NullItem);
                comp.Add(new CollectionContainer { Collection = ItemsSource } );

                CollectionView = CollectionViewSource.GetDefaultView(comp);
                if (CollectionView != null) CollectionView.CurrentChanged += CollectionView_CurrentChanged;

                Selector.ItemsSource = comp;
            }
        }

        bool _isChangingSelection;
        /// <summary>
        /// Triggers binding sources to be updated if the <see cref="NullItem"/> is selected.
        /// </summary>
        /// <param name="sender">sender</param>
        /// <param name="e">event data</param>
        protected void Selector_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (Selector.SelectedItem == NullItem)
            {
                if (!_isChangingSelection)
                {
                    _isChangingSelection = true;
                    try
                    {
                        // Selecting the null item doesn't trigger an update to sources bound to properties
                        // like SelectedItem, so move selection away and then back to force this.
                        int selectedIndex = Selector.SelectedIndex;
                        Selector.SelectedIndex = -1;
                        Selector.SelectedIndex = selectedIndex;
                    }
                    finally
                    {
                        _isChangingSelection = false;
                    }
                }
            }
        }

        /// <summary>
        /// Selects the <see cref="NullItem"/> if the source collection's current item moved to null.
        /// </summary>
        /// <param name="sender">sender</param>
        /// <param name="e">event data</param>
        void CollectionView_CurrentChanged(object sender, EventArgs e)
        {
            if (Selector != null && ((ICollectionView)sender).CurrentItem == null && Selector.Items.Count != 0)
            {
                Selector.SelectedIndex = 0;
            }
        }
    }
}

I hope this helps someone out. If you check it out let me know how it works for you!

Posted in WPF Tips and Tricks | Tagged: , , , , , , , | 25 Comments »