多项选择控件
一个用于选择项目的自定义控件。选择是通过可用项目列表和当前已选项目列表进行的。

引言
在开发 Whitebox Security 的客户端软件时,我们遇到了一个需要控件来允许从可用列表中选择项目的需求。更具体地说,需求包括:
- 显示当前已选项目
- 显示可供选择的项目,即尚未被选中的可用项目。
- 允许将项目添加到已选项目中。
- 允许将项目从已选项目中移除。
- 用户将能够方便地查找可用项目。
- 如果一个项目存在于当前项目列表中但不在可用列表中,这是一个逻辑错误。在这种情况下,会给出视觉指示。
- 使用该控件的结果是,可用项目列表不会改变。另一方面,当前(已选)项目列表会改变。
我曾期望存在这样一个控件,但惊讶地发现它并不存在。
背景
要使用该控件,您应该具备 WPF 基础知识。
如果您想了解控件的编写方式或对其进行自定义,您应该熟悉:
- 自定义控件,请参阅 MSDN 控件创作概述
- 触发器、样式和模板,请参阅 MSDN 上的样式和模板
- 数据绑定,请参阅 MSDN 上的数据绑定概述
要求
该项目是用 Visual Studio 2008 编写的,并基于 .NET 3.5 SP1 构建。
控件
主要部分是可用项目列表和当前项目列表。每个列表都有一个标题。可用项目列表有一个文本过滤器。箭头按钮 pretty much self-explanatory,用于添加项目、移除项目、添加所有项目和移除所有项目。
使用控件
以下是演示控件的 XAML 定义
<local:MultiSelectControl
x:Name="ListControl"
Style="{StaticResource MultiSelectControlStyle}"
CurrentTitle="Current test objects"
AvailableTitle="Available test objects"
AvailableItems="{Binding MyTestAvailableItems}"
CurrentItems="{Binding MyTestCurrentItems, Mode=TwoWay}"
FilterMemberPath="Data"
>
<local:MultiSelectControl.ObjectsTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Data}"
/>
</DataTemplate>
</local:MultiSelectControl.ObjectsTemplate>
</local:MultiSelectControl>
MyTestAvailableItems
和 MyTestCurrentItems
是 <TestObjectModel>
类型的可观察集合。此类有一个 "Data
" 属性并实现了 Equals
方法。
如您所见,定义相当直接。应该在控件上设置的自定义依赖属性有:
CurrentTitle
:显示在当前项目列表上方的标题文本。AvailableTitle
:同上,用于可用项目列表。CurrentItems
:用作当前ListBox
的ItemsSource
的项目。建议使用某个 [Object] 的ObservableCollection
,其中 [Object] 实现INotifyPropertyChanged
和Equals
。当前项目应该是可用项目的子集。请注意,绑定模式设置为 TwoWay。这允许控件的当前项目列表中的更改传播到用户提供的当前项目列表中。AvailableItems
:同上,用于可用项目。可用项目列表应该是当前项目的超集。FilterMemberPath
:设置源对象上某个值的路径,用于作为对象的视觉表示以进行过滤。ObjectsTemplate
:一个DataTemplate
,用于在listbox
中显示可用项目和当前项目。
控件的结构
<Style x:Key="MultiSelectControlStyle" TargetType="{x:Type local:MultiSelectControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MultiSelectControl}">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="3"
SnapsToDevicePixels="true">
<Grid
Margin="3"
Name="TemplateGridPanel"
DataContext="{Binding RelativeSource=
{RelativeSource TemplatedParent}}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="136"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel
Orientation="Horizontal"
Grid.Row="0"
Grid.Column="0"
>
<Label HorizontalAlignment="Left">
Filter:
</Label>
<TextBox
Name="FilterTextBox"
Height="Auto"
VerticalAlignment="Center"
TextChanged="FilterTextBox_TextChanged"
MinWidth="80"
/>
</StackPanel>
<Label
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Left"
Content="{TemplateBinding AvailableTitle}"
/>
<Label
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Left"
Content="{TemplateBinding CurrentTitle}"
/>
<ListBox
Grid.Row="2"
Grid.Column="0"
SelectionMode="Extended"
x:Name="PART_AvailableListBox"
ItemsSource="{Binding AvailableItems}"
ItemTemplate="{TemplateBinding ObjectsTemplate}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="MouseDoubleClick"
Handler="AvailableListBoxItem_DoubleClick" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<ListBox
Grid.Row="2"
Grid.Column="2"
SelectionMode="Extended"
x:Name="PART_CurrentListBox"
ItemsSource="{Binding CurrentItems, Mode=TwoWay}"
ItemTemplate="{TemplateBinding ObjectsTemplate}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<EventSetter Event="MouseDoubleClick"
Handler="CurrentListBoxItem_DoubleClick" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<StackPanel
Orientation="Vertical"
VerticalAlignment="Center"
Grid.Row="2"
Grid.Column="1">
<Button
Style="{DynamicResource Button_General_UI}"
Click="RightArrow_Click"
Margin="5,0,5,3">
<Image HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Right.png"
Stretch="None"/>
</Button>
<Button
Style="{DynamicResource Button_General_UI}"
Click="LeftArrow_Click"
Margin="5,0,5,3">
<Image Margin="-2,0,0,0"
HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Left.png"
Stretch="None"/>
</Button>
<Button Style="{DynamicResource Button_General_UI}"
Click="DoubleRightArrow_Click"
Margin="5,0,5,0">
<Grid Margin="-1,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Right.png"
Stretch="None"/>
<Image Grid.Column="1"
HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Right.png"
Stretch="None"/>
</Grid>
</Button>
<Button
Style="{DynamicResource Button_General_UI}"
Click="DoubleLeftArrow_Click"
Margin="5,0,5,3">
<Grid Margin="-3,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Left.png"
Stretch="None"/>
<Image Grid.Column="1"
HorizontalAlignment="Center"
Source="/Graphics/ButtonArrow_Left.png"
Stretch="None"/>
</Grid>
</Button>
</StackPanel>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
控件的布局非常基础。有趣的点在于当前和可用 Listbox
的 ItemsSource
属性,它们绑定到用户提供的 CurrentItems
和 AvailableItems
DP。ItemsTemplate
绑定到用户在 ObjectsTemplate
DP 中提供的模板。在 Listbox
的双击事件上设置了 EventSetter
,这样双击一个项目就会导致它被移动到另一个列表中。
后台代码
这是处理左箭头按钮点击事件的代码
void LeftArrow_Click(object sender, RoutedEventArgs e){
//A copy is used, because the collection is changed in the iteration
IList currentSelectedItems =
new List<object>((IEnumerable<object>)this.CurrentListBox.SelectedItems);
IList currentListItems = this.CurrentItems as IList;
if (null != currentListItems){
foreach (object obj in currentSelectedItems) {
currentListItems.Remove(obj);
}
}
//updates the available collection
this.AvailableItemsCollectionView.Refresh();
}
左箭头将选定的项目从当前项目列表中移除。
这通过遍历选定的项目并从选定的项目列表中删除它们来实现。
请注意,它们将显示在可用项目列表中。这并不是因为它们被添加到了该列表中,而是因为一个过滤器仅在可用项目 listbox
中显示不在当前项目列表中的项目。
这就是调用可用项目集合视图的 refresh
方法的原因。
双箭头点击 pretty much 做同样的事情,只是针对列表中的所有项目。
同样地,右箭头按钮将项目添加到当前项目列表中,并刷新可用项目列表。当前项目列表会自动刷新,因为假定使用了可观察集合。
双击其中一个列表中的任何项目等同于选择该项目并按下右/左箭头按钮。因此,事件处理程序只是将处理委托给相应的方法。
private void AvailableListBoxItem_DoubleClick(object sender, MouseButtonEventArgs e) {
this.RightArrow_Click(sender, e);
e.Handled = true;
}
代码中使用了两个过滤器。第一个是用于可用项目列表的文本过滤器。
private bool FilterOutText(object item) {
if (String.IsNullOrEmpty(this.FilterText))
return true;
if (null == item){
return false;
}
//This str represents the object. It is determined according to the
//FilterMemberPath DP
string str = "";
//if FilterMemberPath DP not defined, use ToString() result.
if (String.IsNullOrEmpty(this.FilterMemberPath)) {
str = item.ToString();
}
else {
//use reflection to get the value of the string
object value =
this.getPropertyValue(item, this.FilterMemberPath, BindingFlags.Public);
if (null != value) {
str = value.ToString();
}
}
if (String.IsNullOrEmpty(str))
return false;
int index = str.IndexOf(
FilterText,
0,
StringComparison.InvariantCultureIgnoreCase
);
return index > -1;
}
代表项目的文本根据 FilterMemberPath
DP,通过反射来确定。如果未定义此 DP,则使用对象的 ToString()
方法的结果。过滤器文本取自过滤器 TextBox
的文本。
用于反射的方法非常直接。
private object getPropertyValue(
object source,
string propertyName,
BindingFlags flags
) {
object value = null;
// Get the specific field info
PropertyInfo pInfo =
source.GetType().GetProperty(
propertyName,
flags |
BindingFlags.Instance
);
// Make sure the property info is not null
if (pInfo != null) {
// Retrieve the value from the field.
value =
pInfo.GetValue(
source,
null
);
}
return value;
}
第二个过滤器也应用于可用项目列表。
public bool FilterOutCurrentItems(object item){
ICollection currentItems = this.CurrentItems as ICollection;
if (null != currentItems) {
//check if object is contained in current items
foreach (object obj in currentItems) {
if (obj.Equals(item)) {
return false;
}
}
return true;
}
//current Items is null
else{
return false;
}
}
如果该项目存在于当前项目集合中,则过滤器会将其过滤掉。请注意,给定的对象必须以有意义的方式实现 Equals
方法。
由于在 WPF 中对一个列表使用两个过滤器并不直观,我将这两个过滤器合并成一个过滤器,这也是实际使用的过滤器。
private bool FilterOutTextAndCurrentItems(object item) {
bool ans;
//if any of the two are false return false.
ans = this.FilterOutText(item);
//if first is true, return true only if the second one is true as well
return (ans && this.FilterOutCurrentItems(item));
}
控件加载后,我会检查当前项目列表中是否存在不在可用项目列表中的项目。如果存在,我会给出视觉指示 - 红色背景和工具提示。
private void CheckCurrentItemsError() {
IList availableListItems = this.AvailableItems as IList;
IList currentListItems = this.CurrentItems as IList;
foreach (object obj in currentListItems) {
if (!availableListItems.Contains(obj)) {
ItemContainerGenerator ig =
this.CurrentListBox.ItemContainerGenerator;
ListBoxItem lbi = ig.ContainerFromItem(obj) as ListBoxItem;
if (null != lbi) {
lbi.Background = Brushes.Red;
lbi.ToolTip = "Item does not appear in Available list";
}
}
}
}
您觉得怎么样?
请让我知道这篇文章是否对您有所帮助,以及您的想法。
历史
- 2009 年 5 月 21 日:首次发布。
- 2009 年 5 月 24 日:文章更新。