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
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
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
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
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
public static readonly DependencyProperty SelectedTemplateOverrideProperty =
DependencyProperty.Register("SelectedTemplateOverride",
typeof(DataTemplate), typeof(ComboBoxEx),
new FrameworkPropertyMetadata((DataTemplate)null));
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:
- That we are using our special shorted
FormattedName
property that we saw earlier when we talked about the demo (Person
) class.
- 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.