本文将说明如何创建一个带全选复选框的列表控件。其效果如下图:
这个控件是由一个复选框(CheckBox)与一个 ListView 组合而成。它的操作逻辑:
- 当选中“全选”时,列表中所有的项目都会被选中;反之,取消选中“全选”时,所有项都会被取消勾选。
- 在列表中选中部分数据项目时,“全选”框会呈现不确定状态(Indetermine)。
由此看出,“全选”复选框与列表项中的复选框达到了双向控制的效果。
其设计思路:首先,创建自定义控件(CheckListView),在其 ControlTemplate 中定义 CheckBox 和 ListView,并为 ListView 设置 ItemTemplate,在其中增加 CheckBox 控件,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<ControlTemplate TargetType=
"{x:Type control:CheckListView}"
>
<Grid Background=
"{TemplateBinding Background}"
>
<Grid.RowDefinitions>
<RowDefinition Height=
"Auto"
/>
<RowDefinition Height=
"*"
/>
</Grid.RowDefinitions>
<CheckBox Content=
"全选"
/>
<ListView x:Name=
"list"
Grid.Row=
"1"
>
<ListView.ItemTemplate>
<DataTemplate>
<CheckBox />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ControlTemplate>
|
其次,为控件添加两个依赖属性,其中一个为 ItemsSource,即该控件所要接收的数据源,也即选择列表;本质上,这个数据源会指定给其内的 ListView。另外也需要一个属性 IsSelectAllChecked 表示是否选中全选复选框。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
static
readonly
DependencyProperty IsSelectAllCheckedProperty =
DependencyProperty.Register(
"IsSelectAllChecked"
,
typeof
(
bool
?),
typeof
(CheckListView),
new
PropertyMetadata(
false
));
public
static
readonly
DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource"
,
typeof
(
object
),
typeof
(CheckListView),
new
PropertyMetadata(
null
));
/// <summary>
/// 返回或设置全选复选框的选中状态
/// </summary>
public
bool
? IsSelectAllChecked
{
get
{
return
(
bool
?)GetValue(IsSelectAllCheckedProperty); }
set
{ SetValue(IsSelectAllCheckedProperty, value); }
}
/// <summary>
/// 数据源
/// </summary>
public
object
ItemsSource
{
get
{
return
(
object
)GetValue(ItemsSourceProperty); }
set
{ SetValue(ItemsSourceProperty, value); }
}
|
需要注意的一点是,作为一个自定义控件,我们必须考虑它的通用性,所以为了保证能设置各式各样的数据源(如用户列表、物品列表或 XX名称列表),在这里定义一个数据接口,只要数据源中的数据项实现该接口,即可达到通用的效果。该接口定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
public
interface
ICheckItem
{
/// <summary>
/// 当前项是否选中
/// </summary>
bool
IsSelected {
get
;
set
; }
/// <summary>
/// 名称
/// </summary>
string
Name {
get
;
set
; }
}
|
最后,我们把刚才定的属性绑定的控件上,如下:
1
2
3
4
5
6
7
8
|
<CheckBox Content=
"全选"
IsChecked=
"{Binding IsSelectAllChecked, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
/>
<ListView x:Name=
"list"
Grid.Row=
"1"
ItemsSource=
"{TemplateBinding ItemsSource}"
>
<ListView.ItemTemplate>
<DataTemplate>
<CheckBox Content=
"{Binding Name}"
IsChecked=
"{Binding IsSelected}"
/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
|
接下来,实现具体操作:
首先,通过“全选”复选框来控制所有列表项:这里通过其 Click 事件来执行 CheckAllItems 方法, 在此方法中,会对数据源进行遍历,将其 IsSelected 属性设置为 True 或 False。代码如下:
1
2
3
4
5
6
7
|
<CheckBox Content=
"全选"
IsChecked=
"{Binding IsSelectAllChecked, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName=
"Click"
>
<ei:CallMethodAction MethodName=
"CheckAllItems"
TargetObject=
"{Binding RelativeSource={RelativeSource TemplatedParent}}"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
</CheckBox>
|
1
2
3
4
5
6
7
8
9
10
|
/// <summary>
/// 全选或清空所用选择
/// </summary>
public
void
CheckAllItems()
{
foreach
(ICheckItem item
in
ItemsSource
as
IList<ICheckItem>)
{
item.IsSelected = IsSelectAllChecked.HasValue ? IsSelectAllChecked.Value :
false
;
}
}
|
然后,通过选中或取消选中列表项时,更新“全选”复选框的状态:在 DataTemplate 中,我们也为 CheckBox 的 Click 事件设置了要触发的方法 UpdateSelectAllState,代码如下:
1
2
3
4
5
6
7
8
9
|
<DataTemplate>
<CheckBox Content=
"{Binding Name}"
IsChecked=
"{Binding IsSelected}"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName=
"Click"
>
<ei:CallMethodAction MethodName=
"UpdateSelectAllState"
TargetObject=
"{Binding RelativeSource={RelativeSource AncestorType=control:CheckListView}}"
/>
</i:EventTrigger>
</i:Interaction.Triggers>
</CheckBox>
</DataTemplate>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/// <summary>
/// 根据当前选择的个数来更新全选框的状态
/// </summary>
public
void
UpdateSelectAllState()
{
var items = ItemsSource
as
IList<ICheckItem>;
if
(items ==
null
)
{
return
;
}
// 获取列表项中 IsSelected 值为 True 的个数,并通过该值来确定 IsSelectAllChecked 的值
int
count = items.Where(item => item.IsSelected).Count();
if
(count == items.Count)
{
IsSelectAllChecked =
true
;
}
else
if
(count == 0)
{
IsSelectAllChecked =
false
;
}
else
{
IsSelectAllChecked =
null
;
}
}
|
这里也有两点需要提醒:
我一开始定义属性 IsSelectAllChecked 时,它的类型是 bool 类型,那么,由于 CheckBox 控件的 IsChecked 值为 null 时,它将呈现 Indetermine 状态,所以后来把它改为 bool? 类型。
在XAML 代码中可以看出,对事件以及事件的响应使用了行为,所以,需要添加引用 System.Windows.Interactivity.dll 和 Microsoft.Expression.Interactions.dll 两个库,并在XMAL 头部添加如下命名空间的引用:
这样,这个控件就基本完成了,接下来是如何使用它。
首先,定义将要在列表中展示的数据项,并为它实现之前提到的 ICheckItem 接口,这里定义了一个 User 类,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
User : BindableBase, ICheckItem
{
private
bool
isSelected;
private
string
name;
public
bool
IsSelected
{
get
{
return
isSelected; }
set
{ SetProperty(
ref
isSelected, value); }
}
public
string
Name
{
get
{
return
name; }
set
{ SetProperty(
ref
name, value); }
}
}
|
接下来在 ViewModel 中定义一个列表 List<ICheckItem>,并添加数据,最后在 UI 上为其绑定 ItemsSource 属性即可,在此不再贴代码了,具体请参考源代码。
The TargetObject is the object which has the method to be invoked, if you have the method in your window code-behind then the object is the window itself
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
...etcetera...
x:Name="UserControl">
So your CallMethodAction would be:
<ei:CallMethodAction MethodName="MyMethod"
TargetObject="{Binding ElementName=UserControl, Mode=OneWay}"/>