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 Guids 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 Guids 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, Languages) 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.