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






4.10/5 (5投票s)
使用这个,你可以通过将其绑定到 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
列表,我无法设置 ListBox
的 SelectedItems
。
解决方案是在患者中拥有一个语言列表。 但是由于我在客户端/服务器环境中使用它,这将导致性能问题。 想象一下,用户获得一个包含 1000 个患者的列表,并且对于每个患者,需要下载两种语言。 这是不可接受的。
我希望能够下载一次语言列表并将其绑定到 ListBox
。 之后,我想将一个 Guid
列表绑定到 ListBox
,然后,选择应该自动发生。
ListBox 和 SelectedValuePath
原始的 ListBox
几乎解决了我的问题。 通过这样设置 SelectedValuePath
<ListBox ItemsSource="{Binding Path=Languages}"
SelectedValuePath="ID" SelectionMode="Multiple" />
这使得可以进行 ListBox.SelectedValue
并在代码或通过绑定中使用它。 这里的问题是我只能访问一个选定的项目。
同样,这是不可接受的,因为要求是对于每个患者,用户可以选择多种语言。
输入... SelectedValuesListBox
这就是为什么我创建了一个基于 ListBox
的新控件。 它公开了一个新的依赖属性:SelectedValues
。
这使得以下成为可能
- 将
ListBox.ItemsSource
绑定到语言列表 - 将
SelectedValuePath
设置为ID
- 将
ListBox.SelectedValues
绑定到List<Guid>
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
功能可用于该控件。 我们首先创建一个名为 SelectedValuesProperty
的 DependencyProperty
(最佳实践命名)。 这也是正确进行绑定所必需的。 之后,我们实现 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: 首次发布。