Phil On .NET

A Developer's Blog

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!

About these ads

25 Responses to “How to Select Null/None in a ComboBox/ListBox/ListView”

  1. Hans said

    Hi Phil,

    very helpful solution.

    Thanks for sharing it!

    Hans

  2. Ethan Brown said

    This an incredibly elegant solution, and displays some serious WPF kung-fu. Nice work! The only downside appears to be that you can’t use the convenience of the DisplayMemberPath property and must instead resort to using a DataTemplate (as in your example). A minor quibble, though. Thanks again!

    • Andre Luus said

      If you just change the NullItem to a dummy object that exposes “(none)” under the same property name as your normal DisplayMemberPath it works fine! In my case I made the default NullItem a KeyValuePair.

  3. I agree, very nice solution. The one tweak I had to make to it was that I had to change the ICollection to IEnumerable so that it would work with a CollectionViewSource. I don’t think its an issue since that’s what ItemsControl takes for ItemsSource This is much better than all the other ways I was trying to do it. Plus the complete reusable is outstanding. Many thanks for saving me some huge headache or having to figure out how to do this myself.

    • Phil said

      I finally got around to making that change in the post. Thanks!

      • I’m trying to use what you’ve done here, and I see Craig’s modification, but I’m wondering, Phil, about the code. For example, I see a CollectionView is an ICollectionView object, and the ItemsSource is an IEnumerable. Since I didn’t see this blog post until today, when you say you made the change to your post, by that do you mean you changed the ItemsSource from an ICollection to an IEnumerable?

      • Phil said

        Yes, at first I had ItemsSource as an ICollection, but changed it to an IEnumerable.

  4. Avid said

    Great stuff!

    … Now how do I get this to work when using DataSet as my ItemsSource?

  5. rok said

    This is awesome! I’m just learning WPF and couldn’t have done anything like this myself yet. I was still hoping for some detailed explanation about the class (it isn’t obvious for a beginner).
    But still, very elegant solution, “the way it should be” :)

  6. Luis Neves said

    This is the best and cleanest solution ive seen so far, works like a charm. Just added the little tweak that Craig Suchanec refered and also had to define NullItem in XAML because of validation issues. “<utils:NullItemSelectorAdapter NullItem="{StaticResource NullOperationType}" …".
    I've been struggling for hours on this "simple" problem, thank you very much for this briliant solution!

  7. [...] I’m working on a WPF app. In some cases, I have combo boxes that I want to cope with null values. I’ve already found a fantastic solution to this here: http://philondotnet.wordpress.com/2009/09/18/how-to-select-null-none-in-a-combobox-listbox-listview/ [...]

  8. Steve Ballantine said

    Excellent solution, thanks very much for posting it.
    I’ve made a change to move the DisplayMemberPath property to the adapter as well. This allows us to build an object with the correct display member property if needed.

    First the property:

    public static readonly DependencyProperty DisplayMemberPathProperty = DependencyProperty.Register(
    “DisplayMemberPath”, typeof(string), typeof(NullItemSelectorAdapter));

    public string DisplayMemberPath
    {
    get { return (string)GetValue(DisplayMemberPathProperty); }
    set { SetValue(DisplayMemberPathProperty, value); }
    }

    Then this is added just before comp.Add(nullItem):

    object nullItem = NullItemText;
    if (DisplayMemberPath != null)
    {
    IDictionary nullItemObj = new ExpandoObject();
    nullItemObj.Add(DisplayMemberPath, (string)NullItemText);
    nullItem = nullItemObj;
    }

    Finally, add this in the Adapt method to pass the value of the DisplayMemberPath property on to the selector.

    if (Selector != null && DisplayMemberPath != null)
    {
    Selector.DisplayMemberPath = DisplayMemberPath;
    }

    • Steve Ballantine said

      Oh, and I changed NullItem to NullItemText and made it a string for clarity that step isn’t 100% necessary though.

    • Steve Ballantine said

      Found a little problem with my tweak in that the null value no longer sets the value of the bound property. To resolve this, add NullItem property back in and set it with NullItem = nullItem just before comp.Add(nullItem). Then make sure Selector_SelectionChanged uses if(Selector.SelectedItem == NullItem) – (i.e. not NullItemText).

      Finally, I found that the combo boxes were displaying a red border when set to the null value. To correct this, I just added Validation.ErrorTemplate=”{x:Null}” to the combobox XAML.

    • Joe said

      I am trying to solve the same problem as you but I am having compile issues on the line
      IDictionary nullItemObj = new ExpandoObject();
      Error is that I cannot implicity convert from one to the other. Any ideas?

    • Steve, what is ExpandoObject?

      • Sorry Steve, I need I needed to reference System.Dynamic namespace. However, I’m still having a problem because when I resolve that using I get an error message that says, “Cannot implicitly convert type ‘System.Dynamic.ExpandoObject’ to ‘System.Collections.IDictionary'”. I’ve tried casting new ExpandoObject() to ‘System.Collections.IDictionary, and tried using an “as” keyword, but that didn’t help.

      • The live should read:
        IDictionary nullItemObj = new ExpandoObject();

      • Gah! The site is eating the important part because it’s angle brackets. Replace the square brackets in this with angle brackets:
        IDictionary[string, Object] nullItemObj = new ExpandoObject();

  9. Ilse said

    Usually I do not read post on blogs, however I would like to say that this write-up very forced me to check out and do it!
    Your writing taste has been surprised me. Thanks, very great post.

  10. Reblogged this on Rod's space.

  11. Kimberley said

    Have you ever thought about adding a little bit more than just your articles?

    I mean, what you say is important and everything. Nevertheless imagine if you added some great graphics or video clips to
    give your posts more, “pop”! Your content is excellent but with images and videos, this website could definitely be one of the very best in its niche.

    Awesome blog!

  12. Isidro said

    This design is wicked! You most certainly know how to keep a
    reader amused. Between your wit and your videos, I was almost moved to start my
    own blog (well, almost…HaHa!) Great job. I really enjoyed what you had to say,
    and more than that, how you presented it. Too cool!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: