Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF: Selection made better

0.00/5 (No votes)
3 Jul 2009 1  
A better selection option for users.

Introduction

I have been away for a while exploring the idea of writing a book; for all those of you that left words of encouragement at my blog, thanks a lot. It meant a great deal to me. Unfortunately, the publisher turned out to be very narrow minded and could not see the bigger picture or what our book would be like. So the book project is off.

So this article represents the first of many of my getting back to my usual article tirade here at CodeProject, which is really my true home I feel.

This is a short article, but I can promise a very substantial set of articles on what I consider to be my best work, and most useful work to date. The series of upcoming articles will be on an MVVM framework for working with WPF; it actually answers every question/short coming I have ever had working with WPF and Tests and the MVVM pattern, so stay tuned to that series.

But we are where we are, and this is this article, so what does this article actually do? Well, it is fairly simple. We use a lot of comboboxes on our UIs to allow users to pick values, and show a selected value, which is great, so we have something like:

Which works wonderfully, providing your data is fairly short and not that complicated. Remember, in WinForms and WPF, you can put a list of any object you want as a source for a combobox, so it is not inconceivable that one would have a list of complex classes as an items source. The above selection/display method just may not cut it, so something more may be required and be useful.

What if we could keep the currently selected item as a simple string, and allow the user to see a DataGrid or ListView to select the current item from? Wouldn't that be nice?

Luckily, WPF is so powerful, we can do just that.

Here is what I came up with:

So what we have is, the current item is just a short property representation of the entire object that is selected, but when the user wants to make a new selection, they get shown the entire object in an appropriate display container; in my example, I am using a standard (Styled) WPF ListView, but you could use whatever floats your boat.

How does all this work? If you want to know, please read on.

How it Works

The first thing to understand is what the comboxbox is being used to select. In this simple demo code (attached), I am using an ObservableCollection<Person> for the ComboxBox.ItemsSource, but this could be any IEnumerable, so something like List<Person> would do fine as well.

I setup the ComboBox.ItemSource via a binding on a ViewModel which is used for the DataContext for the demo code's Window. Here is the entire ViewModel code. Though this ViewModel code is really not that important to understand, the only thing you need to get is that the ComboxBox.ItemsSource is being bound to the ViewModel's People property which is a ObservableCollection<Person>.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;

namespace WpfApplication1
{
    public class PeopleViewModel : INotifyPropertyChanged
    {
        private Person currentPerson = null;
        private ObservableCollection<Person> people = 
            new ObservableCollection<Person>();
        private ICollectionView peopleCV = null;

        public PeopleViewModel()
        {
            this.people.Add(new Person 
                { 
                    FirstName = "sacha",
                    MiddleName = "",
                    LastName = "Barber1"
                });
            this.people.Add(new Person
            {
                FirstName = "leanne",
                MiddleName = "riddley",
                LastName = "rymes"
            });
            peopleCV = CollectionViewSource.GetDefaultView(people);
            peopleCV.MoveCurrentToPosition(-1);
        }

        public Person CurrentPerson
        {
            get { return currentPerson; }
            set
            {
                if (currentPerson != value)
                {
                    currentPerson = value;
                    NotifyChanged("CurrentPerson");
                }
                else
                    return;
            }
        }

        public ObservableCollection<Person> People
        {
            get { return people; }
            set
            {
                if (people != value)
                {
                    people = value;
                    peopleCV = CollectionViewSource.GetDefaultView(people);
                    peopleCV.MoveCurrentToPosition(-1);
                    NotifyChanged("People");
                }
                else
                    return;
            }
        }

        #region INotifyPropertyChanged Implementation

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">The names
        ///       of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

Here is what one of the Person objects actually looks like:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace WpfApplication1
{
    public class Person : INotifyPropertyChanged
    {
        private String firstName = String.Empty;
        private String middleName = String.Empty;
        private String lastName = String.Empty;

        public String FormattedName
        {
            get
            {
                return FirstName.Substring(0, 1) + "." +
                       LastName;
            }
        }

        public String FirstName
        {
            get { return firstName; }
            set
            {
                if (firstName != value)
                {
                    firstName = value;
                    NotifyChanged("FirstName");
                }
                else
                    return;
            }
        }

        public String MiddleName
        {
            get { return middleName; }
            set
            {
                if (middleName != value)
                {
                    middleName = value;
                    NotifyChanged("MiddleName");
                }
                else
                    return;
            }
        }

        public String LastName
        {
            get { return lastName; }
            set
            {
                if (lastName != value)
                {
                    lastName = value;
                    NotifyChanged("LastName");
                }
                else
                    return;
            }
        }

        #region INotifyPropertyChanged Implementation

        /// <summary>
        /// Occurs when any properties are changed on this object.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;


        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">
        ///         The names of the properties that changed.</param>
        protected virtual void NotifyChanged(params string[] propertyNames)
        {
            foreach (string name in propertyNames)
            {
                OnPropertyChanged(new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

One important thing to note here is that I have a FormattedName property on the Person class, which represents a short display representation of the entire object. It is this FormattedName property that I use to display the currently selected item within the ComboBox; basically, it is just a short representation of the selected object.

So far, what we have is the ComboBox being bound to a ObservableCollection<Person>; great, so how does all the rest work? Well, to get into that, we need to understand how the ComboBox ControlTemplate works. There is a popup within the ControlTemplate that hosts the items, and there is also a Content property that is used to show the currently selected item. Knowing this, we can set to work getting the ComboBox to do what we want.

Getting the ComboBox to Show a Grid for its ItemPresenter

The first step is to get it to display a DataGrid or ListView for its items. How do we do that? And the answer I came up with was to cheat. We just use the ComboBox as normal, but we place a DataGrid or ListView in for its first item, and give it some negative Margin, so we never see the standard ComboBox selection color around the ComboBox item (which is really our DataGrid or ListView).

Here is what I am doing:

<local:ComboBoxEx >

    .......
    .......
    .......
    .......

    <local:ComboBoxEx.Items>
        <ComboBoxItem>

            <ListView AlternationCount="0"
                      Margin="-5,-2,-5,-2"
                      Background="White"
                      Height="200"
                      ItemsSource="{Binding Path=People}"
                      SelectedValue="{Binding Path=CurrentPerson}"
                      ItemContainerStyle="{DynamicResource ListItemStyle}"
                      BorderBrush="Transparent"
                      VerticalAlignment="Stretch"
                      HorizontalAlignment="Stretch"
                      IsSynchronizedWithCurrentItem="True"
                      local:SortableList.IsGridSortable="True"
                      FontSize="12"
                      SelectionMode="Single">
                <ListView.Resources>

                    <Style x:Key="ListItemStyle"
                           TargetType="{x:Type ListViewItem}">
                        <Setter Property="Template"
                                Value="{StaticResource EntityListViewItemTemplate}" />
                        <Setter Property="HorizontalContentAlignment"
                                Value="Left" />

                    </Style>

                </ListView.Resources>

                <ListView.View>
                    <GridView ColumnHeaderContainerStyle="{StaticResource 
                            GridViewColumnHeaderStyle}">
                        <GridViewColumn Header="FirstName"
                                        DisplayMemberBinding="{Binding FirstName}" />
                        <GridViewColumn Header="Middle Name"
                                        DisplayMemberBinding="{Binding MiddleName}" />
                        <GridViewColumn Header="Last Name"
                                        DisplayMemberBinding="{Binding LastName}" />
                    </GridView>
                </ListView.View>
            </ListView>
        </ComboBoxItem>
    </local:ComboBoxEx.Items>
</local:ComboBoxEx>

The eagle eyed amongst you will actually notice that I am not using the standard WPF ComboBox, but rather a ComboBoxEx; don't worry, I will cover this in just a minute. For now, just understand that we are using the standard WPF ComboBox.Items collection and providing a ComboBoxItem just as you would for a standard WPF ComboBox. It just so happens that the ComboBox only has one item, and that is our DataGrid or ListView.

So that explains how we get a DataGrid or ListView to appear. What about the selected item? As the current item is effectively a DataGrid or ListView, surely the selected item will be a DataGrid or ListView too. Well, yes, under normal circumstances, it would be, and it would look quite odd; it would look like this:

Which is not really what we are after at all. How do we go about fixing that? Well, that does require a little bit more knowledge about how the ComboBox ControlTemplate works. When you look into it, you can see that there is a ContentPresenter that is used to represent the currently selected item's Content, and that by default uses a TemplateBinding to bind to the Content. Which explains what we just saw above; the Content is actually a DataGrid or ListView. That's interesting, so perhaps, we can get it to display something else, if we use another property.

<ContentPresenter x:Name="item"
      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
      Margin="{TemplateBinding Padding}"
      VerticalAlignment="Center"
      Grid.Column="1"
      Content="{TemplateBinding SelectionBoxItem}"
      ContentTemplate="{TemplateBinding Content}"
      ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" />

Changing the Selected Item Content

When I first looked into this, I wanted to be able to swap out the ContentTemplate="{TemplateBinding Content}" to use an Attached DP, but this did not seem to work, as the DP was not actually considered to be part of the standard properties available to use for a TemplateBinding markup extension. What I then thought was well, we will just have to subclass ComboBox and add a property we want to use for Content and use that in the ControlTemplate.

That is exactly what I do; here is the full code for ComboBoxEx:

public class ComboBoxEx : ComboBox
{
    #region SelectedTemplateOverride

    /// <summary>
    /// SelectedTemplateOverride Dependency Property
    /// </summary>
    public static readonly DependencyProperty SelectedTemplateOverrideProperty =
        DependencyProperty.Register("SelectedTemplateOverride", 
        typeof(DataTemplate), typeof(ComboBoxEx),
            new FrameworkPropertyMetadata((DataTemplate)null));

    /// <summary>
    /// Gets or sets the SelectedTemplateOverride property.
    /// </summary>
    public DataTemplate SelectedTemplateOverride
    {
        get { return (DataTemplate)GetValue(SelectedTemplateOverrideProperty); }
        set { SetValue(SelectedTemplateOverrideProperty, value); }
    }

    #endregion
}

As I say, it would have been nice to use an attached DP, but hey ho.

So with this ComboBoxEx class in place, we can then change the standard ControlTemplate applied to use our new SelectedTemplateOverride DP. Let's see that in the relevant part of the ComboBoxEx ControlTemplate.

<ContentPresenter x:Name="item"
          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
          Margin="{TemplateBinding Padding}"
          VerticalAlignment="Center"
          Grid.Column="1"
          Content="{TemplateBinding SelectionBoxItem}"
          ContentTemplate="{TemplateBinding SelectedTemplateOverride}"
          ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" />

Notice that we no longer use ContentTemplate="{TemplateBinding Content}" but rather use ContentTemplate="{TemplateBinding SelectedTemplateOverride}", which is our new DP we introduced in the ComboBoxEx class.

So all that we now need to do is put something in the SelectedTemplateOverride DP; the place to do this is in the XAML where we use the actual instance of a ComboBoxEx object.

Here is the relevant bit of XAML:

<local:ComboBoxEx.SelectedTemplateOverride>
    <DataTemplate>
        <Label DataContext="{Binding ElementName=theView, Path=DataContext}"
               Content="{Binding Path=CurrentPerson.FormattedName, 
                    UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
               VerticalContentAlignment="Center"
               Padding="0"
               Margin="2,0,0,0" />
    </DataTemplate>
</local:ComboBoxEx.SelectedTemplateOverride>

Notice that it is just a DataTemplate, as this is what the SelectedTemplateOverride DP type was. The other two things to note are:

  1. That we are using our special shorted FormattedName property that we saw earlier when we talked about the demo (Person) class.
  2. I had to get the DataContext from somewhere for the label, for the binding to work, so I grab it off the hosting Window, as this is the thing that has the entire ViewModel set as its DataContext anyway. The ViewModel actually knows which is the current item, by the magic of ICollectionView and IsSynchronizedWithCurrentItem="True" which is set on the ListView (CombBox single item) in the demo code. I have not discussed ICollectionView and IsSynchronizedWithCurrentItem="True", but all it does is keep the selection made in the ListView synchronized with the ICollectionView in the ViewModel, which allows me to grab the currently selected item from the DataContext (from the Window, as it has the ViewModel as its DataContext).

So with this last piece of the puzzle solver, we end up with the selected item being the currently selected Persons from the ListView being used as the Contemt for the selected item in the CombBox.

Bonuses

The attached code also demonstrates how to sort the ListView columns using an attached DP called SortableList, which you set on your ListView like this:

<ListView local:SortableList.IsGridSortable="True" SelectionMode="Single">

You can dig into the SortableList to see how it works.

The End

Anyway, I hope that all is clear. If you like it, you can leave a vote and a comment, that would be nice.

Enjoy.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here