65.9K
CodeProject 正在变化。 阅读更多。
Home

SelectedValuesListBox - 一个实现多个 SelectedValue(SelectedValues)的 ListBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.10/5 (5投票s)

2009年8月20日

CPOL

3分钟阅读

viewsIcon

27415

downloadIcon

395

使用这个,你可以通过将其绑定到 Guid 列表来设置包含语言的 ListBox 的选择。

引言

我的主要工作是编写数据驱动的应用程序,最近,我从 Windows Forms 迁移到了 WPF。 由于 WPF 提供了一个很棒的数据绑定机制,我想 100% 使用它。

背景

在我的例子中,我遇到了以下问题: 想象一下,最终用户可以管理一个语言列表。 他还将有一个患者列表,并且对于每个患者,他将选择一种或多种语言。

示例

  • Sandrino: 荷兰语, 法语, 意大利语, 英语
  • John: 英语
  • Bernard: 法语
  • William: 荷兰语, 法语

这将需要以下类

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>();
    }
}

现在,假设我将一个 ListBox 绑定到一个 List<Language>。 问题来了:我将如何使用数据绑定在 ListBox 上设置选择? 由于我正在使用 Patient 中的 Guid 列表,我无法设置 ListBoxSelectedItems

解决方案是在患者中拥有一个语言列表。 但是由于我在客户端/服务器环境中使用它,这将导致性能问题。 想象一下,用户获得一个包含 1000 个患者的列表,并且对于每个患者,需要下载两种语言。 这是不可接受的。

我希望能够下载一次语言列表并将其绑定到 ListBox。 之后,我想将一个 Guid 列表绑定到 ListBox,然后,选择应该自动发生。

ListBox 和 SelectedValuePath

原始的 ListBox 几乎解决了我的问题。 通过这样设置 SelectedValuePath

<ListBox ItemsSource="{Binding Path=Languages}" 
    SelectedValuePath="ID" SelectionMode="Multiple" />

这使得可以进行 ListBox.SelectedValue 并在代码或通过绑定中使用它。 这里的问题是我只能访问一个选定的项目。

同样,这是不可接受的,因为要求是对于每个患者,用户可以选择多种语言。

输入... SelectedValuesListBox

这就是为什么我创建了一个基于 ListBox 的新控件。 它公开了一个新的依赖属性:SelectedValues

这使得以下成为可能

  1. ListBox.ItemsSource 绑定到语言列表
  2. SelectedValuePath 设置为 ID
  3. ListBox.SelectedValues 绑定到 List<Guid>
  4. ListBox 中在 SelectedValues 列表中具有 ID 的所有项目将被选中

代码

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;
            }
        }
    }
}

首先,我们创建一个从 ListBox 继承的控件。

现在,我们必须使 SelectedValues 功能可用于该控件。 我们首先创建一个名为 SelectedValuesPropertyDependencyProperty(最佳实践命名)。 这也是正确进行绑定所必需的。 之后,我们实现 SelectedValues 属性,该属性实际上从 DependencyProperty 设置和获取值。

使用回调 OnValuesChanged,我们可以跟踪何时将列表绑定到 SelectedValues 属性。 如果发生这种情况,我们将遍历列表中的所有值(例如 Guid),并找到 ListBox 中的所有项目(例如 Language),这些项目在 SelectedValuePath(例如 ID)上具有相同的 Guid

如果我们绑定到 SelectedValues 的列表实现了 INotifyPropertyChanged 接口(可能是 ObservableCollection),我们也希望知道列表中发生的更改。 通过处理 PropertyChanged 事件,如果列表中有任何更改,我们可以更新 ListBox

最后,如果有人在 ListBox 中更改了选择,这将由 SelectionChangeEvent 处理。 这是必需的,以便更新 SelectedValues 属性,从而更新绑定到此属性的列表。

使用控件

<local:SelectedValuesListBox
   SelectedValues="{Binding Path=Me.Languages, Mode=TwoWay}" 
   ItemsSource="{Binding Path=Languages}"
   DisplayMemberPath="Name" SelectedValuePath="ID" 
   x:Name="listLanguages" SelectionMode="Extended">
</local:SelectedValuesListBox>

SelectedValuePath 是您希望与 SelectedValues 匹配的属性。

示例代码

在附件中,您可以找到一个实现此控件的示例项目。 此示例基于 MVVM,因为这就是我偶然发现此问题的方式。

待办事项

使其线程安全。

历史

  • 2009/08/20: 首次发布。
© . All rights reserved.