![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
Controls
Intermediate
License: The Code Project Open License (CPOL)
WPF : Selection made betterBy Sacha BarberA better selection option for users |
C# (C#3.0, C#4.0), .NET (.NET3.5, .NET4.0), WPF, Architect, Dev, Design
|
||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
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}" />
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
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.
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.
Enjoy.
General
News
Question
Answer
Joke
Rant
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 |