Introduction
My main occupation is writing data driven applications, and recently, I have moved from Windows Forms to WPF. Since WPF provides a great databinding mechanism, I want to use it 100%.
Background
In my case, I had the following problem: imagine that the end user can manage a list of languages. He would also have a list of patients, and for each of these patients, he would select one or more languages.
Example:
- Sandrino: Dutch, French, Italian, English
- John: English
- Bernard: French
- William: Dutch, French
This would require the following classes:
public class Language
{
public Guid ID { get; set; }
public string Name { get; set; }
}
public class Patient
{
public string Name { get; set; }
public List<Guid> LanguageIDs { get; set; }
public Patient(string name)
{
Name = name;
LanguageIDs = new List<Guid>();
}
}
Now, let's say I bind a ListBox
with a List<Language>
. Here comes the problem: how will I set the selection on the ListBox
using databinding? Since I'm using a list of Guid
s in Patient
, I cannot set the SelectedItems
of the ListBox
.
The solution would be to have a list of languages in the patient. But since I'm using this in a client/server environment, this will have performance issue. Imagine that the user gets a list of 1000 patients, and for each patient, two languages need to be downloaded. This is not acceptable.
I want to be able to download the list of languages once and bind it to a ListBox
. After that, I want to bind a list of Guid
s to the ListBox
, and poof, the selection should happen automatically.
ListBox and SelectedValuePath
The original ListBox
almost solved my problem. By setting the SelectedValuePath
like this:
<ListBox ItemsSource="{Binding Path=Languages}"
SelectedValuePath="ID" SelectionMode="Multiple" />
This made it possible to do ListBox.SelectedValue
and use it in code or through binding. The problem here is that I only have access to one selected item.
Again, this is not acceptable since the requirement is that for each patient, a user could select multiple languages.
Enters... SelectedValuesListBox
That's why I created a new control based on the ListBox
. It exposes a new Dependency Property: SelectedValues
.
This makes the following possible:
- Bind
ListBox.ItemsSource
to a list of languages
- Set the
SelectedValuePath
to ID
- Bind the
ListBox.SelectedValues
to a List<Guid>
- All the items in the
ListBox
that have an ID
in the SelectedValues
list will be selected
Code
public class SelectedValuesListBox : ListBox
{
private bool monitor = true;
public static readonly DependencyProperty SelectedValuesProperty =
DependencyProperty.RegisterAttached("SelectedValues",
typeof(IList), typeof(SelectedValuesListBox),
new PropertyMetadata(OnValuesChanged));
public IList SelectedValues
{
get { return (IList)GetValue(SelectedValuesProperty); }
set { SetValue(SelectedValuesProperty, value); }
}
private static void OnValuesChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
SelectedValuesListBox multi = dependencyObject as SelectedValuesListBox;
multi.SetSelected(e.NewValue as IList);
if (e.NewValue is INotifyPropertyChanged)
(e.NewValue as INotifyPropertyChanged).PropertyChanged +=
(dependencyObject as SelectedValuesListBox).MultiList_PropertyChanged;
}
public SelectedValuesListBox()
{
SelectionChanged += new SelectionChangedEventHandler(MultiList_SelectionChanged);
}
private void MultiList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (monitor && !String.IsNullOrEmpty(SelectedValuePath))
{
try
{
monitor = false;
SelectedValues.Clear();
foreach (object item in SelectedItems)
{
PropertyInfo property = item.GetType().GetProperty(SelectedValuePath);
if (property != null)
SelectedValues.Add(property.GetValue(item, null));
}
}
catch
{
throw;
}
finally
{
monitor = true;
}
}
}
protected void MultiList_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
SetSelected(sender as IList);
}
public void SetSelected(IList list)
{
if (monitor && !String.IsNullOrEmpty(SelectedValuePath)
&& list != null && list.Count > 0)
{
try
{
monitor = false;
foreach (object item in Items)
{
PropertyInfo property = item.GetType().GetProperty(SelectedValuePath);
if (property != null)
{
if (list.Contains(property.GetValue(item, null)))
SelectedItems.Add(item);
}
}
}
catch
{
throw;
}
finally
{
monitor = true;
}
}
}
}
First, we create a control that inherits from ListBox
.
Now, we have to make the SelectedValues
functionality available to the control. We first create a DependencyProperty
that we name SelectedValuesProperty
(best practices naming). This is also required for binding to work correctly. After that, we implement the SelectedValues
property that actually sets and gets values from the DependencyProperty
.
Using the callback OnValuesChanged
, we can trace when a list gets bound to the SelectedValues
property. If this happens, we will go through all values in the list (for example, Guid
) and find all items in the ListBox
(for example, Language
s) that have the same Guid
set on the SelectedValuePath
(for example, ID
).
If the list we bound to SelectedValues
implements the INotifyPropertyChanged
interface (probably an ObservableCollection
), we also want to be aware that there are changes in the list. By handling the PropertyChanged
event, we can update the ListBox
if something in the list changes.
And finally, if someone changes the selection in the ListBox
, this will be handled by SelectionChangeEvent
. This is required to update the SelectedValues
property and thus updating the list that is bound to this property.
Using the control
<local:SelectedValuesListBox
SelectedValues="{Binding Path=Me.Languages, Mode=TwoWay}"
ItemsSource="{Binding Path=Languages}"
DisplayMemberPath="Name" SelectedValuePath="ID"
x:Name="listLanguages" SelectionMode="Extended">
</local:SelectedValuesListBox>
The SelectedValuePath
is the property you want to have matched with SelectedValues
.
Sample code
In the attachment, you can find a sample project implementing this control. This sample is based on MVVM since this is how I stumbled upon the problem.
To-do
Make thread safe.
History
- 20/08/2009: First release.