SelectedValuesListBox - A ListBox that implements more than one SelectedValue (SelectedValues)






4.10/5 (5 votes)
Using this, you could set the selection of a ListBox containing languages by binding it to a list of Guids.
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
toID
- Bind the
ListBox.SelectedValues
to aList<Guid>
- All the items in the
ListBox
that have anID
in theSelectedValues
list will be selected
Code
public class SelectedValuesListBox : ListBox
{
/// <summary>
/// Use the monitor to prevent stack overflow exceptions
/// </summary>
private bool monitor = true;
/// <summary>
/// The dependency property for selected values
/// </summary>
public static readonly DependencyProperty SelectedValuesProperty =
DependencyProperty.RegisterAttached("SelectedValues",
typeof(IList), typeof(SelectedValuesListBox),
new PropertyMetadata(OnValuesChanged));
/// <summary>
/// Property for the control
/// </summary>
public IList SelectedValues
{
get { return (IList)GetValue(SelectedValuesProperty); }
set { SetValue(SelectedValuesProperty, value); }
}
/// <summary>
/// When a list is bound to the control,
/// tell the control to set all items as selected.
/// Also, if this is an ObservableCollection (or any other INotify
/// list for that matter), refresh the selection in the list.
/// </summary>
/// <param name="dependencyObject"></param>
/// <param name="e"></param>
private static void OnValuesChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
// Set the selections in the control the first time the list is bound
SelectedValuesListBox multi = dependencyObject as SelectedValuesListBox;
multi.SetSelected(e.NewValue as IList);
// For each change in the bound list, change the selection in the control
if (e.NewValue is INotifyPropertyChanged)
(e.NewValue as INotifyPropertyChanged).PropertyChanged +=
(dependencyObject as SelectedValuesListBox).MultiList_PropertyChanged;
}
/// <summary>
/// This constructor will make sure the selection changes
/// in the control are reflected in the bound list
/// </summary>
public SelectedValuesListBox()
{
SelectionChanged += new SelectionChangedEventHandler(MultiList_SelectionChanged);
}
/// <summary>
/// This event will send the selection changes from the control to the bound list
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MultiList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (monitor && !String.IsNullOrEmpty(SelectedValuePath))
{
try
{
monitor = false;
// Reset the selected values property
SelectedValues.Clear();
// Loop each selected item
// Add the value to the list based on the selected value path
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;
}
}
}
/// <summary>
/// There was a change in the list that was bound.
/// Change the selection of the items in the control!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void MultiList_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
SetSelected(sender as IList);
}
/// <summary>
/// Set the selection in the control based on a bound list
/// </summary>
/// <param name="list"></param>
public void SetSelected(IList list)
{
if (monitor && !String.IsNullOrEmpty(SelectedValuePath)
&& list != null && list.Count > 0)
{
try
{
monitor = false;
// Loop each item
foreach (object item in Items)
{
// Get the property based on the selected value path
PropertyInfo property = item.GetType().GetProperty(SelectedValuePath);
if (property != null)
{
// Match the value from the bound list to an item in the control
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.