本文转自:http://www.cnblogs.com/ldp615/archive/2009/11/29/winform_fastselect_component.html
用户界面中,需要用户进行多项选择时,我们通常会提供一组快速选择(以下简称速选)按钮:全选、反选、清空,以方便用户操作。本文章将会构建一个通用速选组件来简化操作,使用之后,您不需要编写任何代码,只需设置两个属性便可让一个控件拥有速选的功能。
常见的几种多选形式
WinForm中常见的几种多选形式如下图:
图1
我们暂且将用于显示选项的控件叫做“选项控件”,全选、反选、清空叫做“速选按钮”。
四种形式采用了不同的控件用作选项控件、速选按钮:
|
选项控件 |
速选按钮 |
形式一 |
CheckedListBox |
Label |
形式二 |
CheckBox |
LinkLabel |
形式三 |
TreeView |
Button |
形式四 |
DataGridView |
PictureBox |
编程中的麻烦
实际使用中还可能会有其它形式的选项控件和速选按钮,根据使用场合不同,选项控件和速选按钮可以任意组合。这给我们编程带来了麻烦,选项控件没有统一的访问接口,换一个选项控件就要编写不同的选择代码。
想要将全选、反选、清空的选择逻辑提取出来还真不容易。于是很多程序员就选择了针对实际应用的组合(如形式一,CheckedListBox + Label)进行直接编码。这样导致大量相似的代码充斥在项目之中,会带来以下问题:
- 重复的编码、调试、测试(主要是界面)工作;
- 一旦选项控件发生改变,就必须修改代码,修改后还要测试;(可能性比较大,可能客户不喜欢形式一,非要你修改成形式二)
这样的直接编码实际上已经违反了DRY原则,我们应该纠正。
面向对象要求我们封装变化,我们这里不变的是速选的逻辑,变化的是选项控件和速选按钮,变化是两个方向的,有点接近桥模式的应用场景了。不过我对设计模式了解不深,不敢冒用,而且感觉这里速选按钮的变化是比较简单的,至少它们都有一个Click事件,也可以认为是不变的。
使用 IExtenderProvider 扩展控件功能
我们采用另外一种途径,.Net其实已经给我们提供了一种扩展控件功能的方式(也正体现了面向对象的OCP原则),可以让我们给控件赋予额外的功能。我们来看一个重要的接口:IExtenderProvider 接口,位于System.ComponentModel命名空间下。实现了这个接口的组件有ToolTip、ErrorProvider等,ToolTip、ErrorProvider这两个组件可以向其它组件(控件是组件的一种)提供额外的功能,ToolStip能让其它控件在用户鼠标悬停时弹出一个小框显示一些提示信息,ErrorProvider则能让控件显式错误信息。
我们要做的就是创建一个新的组件,实现IExtenderProvider接口,向控件(Button、Label等)提供速选的功能。这个组件我已经完成了,名字叫FastSelect,先不考虑实现原理、如何实现,我们先看下如何使用吧:
FastSelect组件
FastSelect是一个组件,会自动显示在工具箱中,将其拖入窗体,将会显示在设计器下方。
图2
选中全选按钮(label1),属性显示窗口如下图:
图3
分组中出现了一个新的分组:速选,其中有如上图两个属性,第一个属性用于选择选项控件,第二个属性用于确定选择方式(全选还是清空)。设置两个属性的值如上图,完成后label1就可以全选分类中的所有选项了。
简单说来,只需要向窗体置入一个控件(Button、Label、LinkLabel、Picture),简单设置两个属性,这个控件就自动具有了速选功能,不需要任何代码。当然得借助FastSelect组件。
IExtenderProvider接口
这么神奇,是如何实现的呢?要从IExtenderProvider接口说起,这个接口可以向其他组件提供属性,如上图中的两个属性。有这里有两点要说明一下:
-
向其它组件提供属性,这个可以限定,比如仅只向Button提供。
-
提供属性并不是真正给其他组件加上新的属性,只是在WinForm设计时,在PropertyGrid中显示额外属性(后面会详说)。
我们来看下FastSelect是如何实现的:
实现IExtenderProvider
1
public
enum
SelectionType
2
{
3
清空,
4
反选,
5
全选,
6
}
7
8
[ProvideProperty(
"
SelectionSource
"
,
typeof
(Control))]
9
[ProvideProperty(
"
SelectionType
"
,
typeof
(Control))]
10
public
partial
class
FastSelect : Component, IExtenderProvider
11
{
12
13
public
bool
CanExtend(
object
extendee)
14
{
15
if
(extendee
==
null
)
return
false
;
16
if
(extendee
is
Button
||
extendee
is
Label
||
extendee
is
PictureBox)
return
true
;
17
return
false
;
18
}
19
20
[Category(
"
速选
"
), Description(
"
速选源控件
"
), Localizable(
true
)]
21
public
Control GetSelectionSource(Control control)
22
{
23
24
}
25
26
public
void
SetSelectionSource(Control control, Control selectionSource)
27
{
28
29
}
30
31
32
[Category(
"
速选
"
), Description(
"
速选方式
"
), DefaultValue(SelectionType.清空), Localizable(
true
)]
33
public
SelectionType GetSelectionType(Control control)
34
{
35
36
}
37
38
public
void
SetSelectionType(Control control, SelectionType selectionType)
39
{
40
41
}
42
43
}
SelectionType枚举不必多说,我们来看FastSelect,它继承至Component,实现了IExtenderProvider接口。
CanExtend是IExtenderProvider接口的唯一成员,它用来标识可以给那些组件进行扩展,上面的代码中我们限定了只可以给Button、Label、PictureBox进行扩展,也就是说其它类型的控件在属性窗口中是看不到速选属性的。CanExtend中没有LinkLabel,是因为LinkLabel是Label的子类,Label有的它也会自动拥有。
Get(Set) SelectionSource、Get(Set)SelectionType有点类似属性的get/set吧,只不过多了一个参数(第一个参数control),传入这个参数的是要进行属性扩展的控件,还是让我们看一下WinForm生成的代码吧(在Form1.generated.cs中)
1
this
.fastSelect.SetSelectionSource(
this
.label1,
this
.categoriesListBox);
2
this
.fastSelect.SetSelectionType(
this
.label1, SelectionType.全选);
对照图三,应该明白“提供属性”的真正含义了吧。
我们再来看是如何实现选择的,先看代码片段:
1
public
partial
class
FastSelect : Component, IExtenderProvider
2
{
3
private
Dictionary
<
Control, Control
>
sourceControlsDict;
4
private
Dictionary
<
Control, SelectionType
>
typeDict;
5
6
public
void
SetSelectionSource(Control control, Control selectionSource)
7
{
8
9
sourceControlsDict.Add(control, selectionSource);
10
control.Click
+=
new
EventHandler(control_Click);
11
12
}
13
14
public
void
SetSelectionType(Control control, SelectionType selectionType)
15
{
16
17
typeDict.Add(control, selectionType);
18
19
}
20
21
void
control_Click(
object
sender, EventArgs e)
22
{
23
Control control
=
sender
as
Control;
24
Control selectionSource
=
sourceControlsDict[control];
25
SelectionType selectType
=
typeDict[control];
26
27
if
(selectionSource
is
DataGridView)
28
{
29
DataGridView dataGridView
=
selectionSource
as
DataGridView;
30
foreach
(DataGridViewRow row
in
dataGridView.Rows)
31
row.Selected
=
ChangeSelected(row.Selected, selectType);
32
}
33
34
}
35
36
private
bool
ChangeSelected(
bool
isSelected, SelectionType type)
37
{
38
if
(type
==
SelectionType.清空)
return
false
;
39
else
if
(type
==
SelectionType.全选)
return
true
;
40
else
if
(type
==
SelectionType.反选)
return
!
isSelected;
41
else
throw
new
NotImplementedException();
42
}
43
44
}
一个窗体上只需要一个FastSelect组件,但它要为其它多个组件提供属性,我们这里使用Dictionary保存这些属性。
设置控件SelectionSource属性时,其实是调用了SetSelectionSource方法,其中我们为控件注册了Click事件,control_Click中根据不同的选项控件的类型(DataGridView、CheckedListBox等)和选择类型进行相应的处理。这样应该明白了吧。
源码
代码比较长,就不发在文章中了,此处下载。(大部分时间花在文章上了,代码没太测试,可能存在问题,如发现请告知,谢谢了)。