Click here to Skip to main content
15,895,667 members
Articles / Desktop Programming / WPF

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

Rate me:
Please Sign up or sign in to vote.
4.10/5 (5 votes)
20 Aug 2009CPOL3 min read 27.1K   382   11  
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:

C#
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:

XML
<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:

  1. Bind ListBox.ItemsSource to a list of languages
  2. Set the SelectedValuePath to ID
  3. Bind the ListBox.SelectedValues to a List<Guid>
  4. All the items in the ListBox that have an ID in the SelectedValues list will be selected

Code

C#
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, 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

XML
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead RealDolmen
Belgium Belgium
I'm a Technical Consultant at RealDolmen, one of the largest players on the Belgian IT market: http://www.realdolmen.com

All posts also appear on my blogs: http://blog.sandrinodimattia.net and http://blog.fabriccontroller.net

Comments and Discussions

 
-- There are no messages in this forum --