Click here to Skip to main content
6,822,123 members and growing! (18,326 online)
Email Password   helpLost your password?
Platforms, Frameworks & Libraries » Windows Presentation Foundation » Controls     Intermediate License: The Code Project Open License (CPOL)

WPF : Selection made better

By Sacha Barber

A better selection option for users
C# (C#3.0, C#4.0), .NET (.NET3.5, .NET4.0), WPF, Architect, Dev, Design
Posted:3 Jul 2009
Views:15,935
Bookmarked:66 times
Unedited contribution
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
48 votes for this article.
Popularity: 8.01 Rating: 4.76 out of 5
1 vote, 2.1%
1

2
7 votes, 14.6%
3
3 votes, 6.3%
4
37 votes, 77.1%
5

 

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 1st 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 a MVVM framework for working with WPF, it actually answers every question/short coming I have ever had with 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 faily 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. So 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 diplay container, in my example I am using a standard (Styled) WPF ListView, but you could use what ever floats your boat.

So how does all this work, if you want to know please read on.

How It Works

Ok this first thing to understand is what the ComboxBox is being used to select. In this simple demo code (attached) I am using a 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 codes 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 ViewModels 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.

Ok 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. So 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 1st 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 amongt you will actaully 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 1 item and that is our DataGrid or ListView.

So that explains how we get a DataGrid or ListView to appear. So 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 cicumstances it would be, and it would look quite odd, it would look like this:

Which is not really what we are after at all. So 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 items Content, and that by default uses a TemplateBinding to bind to Content. Which explains what we just saw above, the Content is actually a DataGrid or ListView, mmmm. 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. So 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.

So that is exactly what I do, here is the full code for the 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. Lets see that in the relavent 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 2 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 it 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 peice of the puzzle solver we end up with the selected item being the currently selected Persons from the ListView be used as the Contemt for the selected item in the CombBox.

Bonuses

The attached code also demostrates 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 is all clear. If you like it, you can leave a vote and a comment, that would be nice.

Enjoy.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Sacha Barber


Member
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Occupation: Software Developer (Senior)
Location: United Kingdom United Kingdom

Other popular Windows Presentation Foundation articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 25 of 49 (Total in Forum: 49) (Refresh)FirstPrevNext
GeneralCan't do the same with treeview inside comboBox [modified] PinmemberPhanquangtu23:26 30 Jul '09  
GeneralRe: Can't do the same with treeview inside comboBox PinmvpSacha Barber0:24 31 Jul '09  
GeneralRe: Can't do the same with treeview inside comboBox PinmemberPhanquangtu17:02 2 Aug '09  
GeneralRe: Can't do the same with treeview inside comboBox PinmemberYoniP1:56 9 Aug '09  
GeneralRe: Can't do the same with treeview inside comboBox PinmvpSacha Barber9:01 9 Aug '09  
GeneralExcellent but just 4, because of scrollbar trouble PinmemberMikeEasy11:13 27 Jul '09  
GeneralRe: Excellent but just 4, because of scrollbar trouble PinmvpSacha Barber11:28 27 Jul '09  
GeneralNice one ! PinmvpAbhijit Jana9:50 17 Jul '09  
GeneralRe: Nice one ! PinmvpSacha Barber0:56 18 Jul '09  
GeneralRegarding your book... PinmemberMember 33345340:35 14 Jul '09  
GeneralRe: Regarding your book... PinmvpSacha Barber0:36 14 Jul '09  
GeneralAwesome Article Pinmemberazamsharp8:37 12 Jul '09  
GeneralRe: Awesome Article PinmvpSacha Barber22:42 12 Jul '09  
GeneralMy vote of 1 Pinmemberrolphjasm2:30 11 Jul '09  
GeneralRe: My vote of 1 PinmvpSacha Barber22:41 12 Jul '09  
Generalhello, help from you PinmemberSeraph_summer11:52 9 Jul '09  
GeneralTextSearch.TextPath Pinmember88Keys16:42 8 Jul '09  
GeneralRe: TextSearch.TextPath PinmvpSacha Barber22:49 8 Jul '09  
GeneralGreat article PinmemberGDavy20:25 6 Jul '09  
GeneralRe: Great article PinmvpSacha Barber22:31 6 Jul '09  
GeneralAs usual PinmemberDr.Luiji14:07 6 Jul '09  
GeneralRe: As usual PinmvpSacha Barber22:30 6 Jul '09  
GeneralCompliment Mr.Sacha!!! PinmemberVisualLive5:36 6 Jul '09  
GeneralRe: Compliment Mr.Sacha!!! PinmvpSacha Barber6:28 6 Jul '09  
GeneralWelcome back! Pinmemberdisore21:41 5 Jul '09  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.

PermaLink | Privacy | Terms of Use
Last Updated: 3 Jul 2009
Editor:
Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2010
Web22 | Advertise on the Code Project