实现类型化的数据绑定(列表)控件
春鱼
内容提要
本文探讨了以具体而丰富的方式表现集合数据的总体思路,其中某些做法是作者在长期的实际开发工作中逐渐形成的。可能有一定可取之处,也可能是可笑的。希望得到有经验的朋友的指正。
类型化的数据绑定控件,是基于.NET环境下数据绑定概念,是面向“表示层”设计的。所谓“类型化”表现在以下几个特性:
1. 仅用来呈现特定类型的数据
2. 绑定类型化的数据格式
3. 内置的分页逻辑
4. 内置的排序、筛选
5. 列控制
6. 对全体项或选中项的指令机制(command machanism)
类型化的意义在于提高内聚性、重用性,实现同一的编程接口以及外观感受,减少可能出现的错误、不一致性,简化重复编码,以节省开发的时间成本。举例来说,当我们实现类型化的数据帮定控件时,在得到集合数据(通常以类型化的数据实体保持)后,可以仅通过一个命令呈现数据,类型化呈现控件自行解决分页、筛选、排序等操作,同时可以事件(public event)的方式向容器发送针对全体项或者选中项的指令(查看、删除及其他业务相关指令)。
本文将分析类型化的概念及实现方法。
题解
所谓使用强类型化的数据绑定控件,基于将一般性的“列表需求”进一步提取出来,形成更高级和更模式化的工作过程。列表被强类型化之后,将具有分页、排序、列控制、数据的二次筛选,引发事件等特性。
引言
在我们的实际项目中,这样的业务需求肯定是不可避免的:以list(清单)的形式显示一组数据,当迁移到.NET平台后,这样的工作变得极其简单。我们有诸多“控件”可以使用。又有很多数据格式可以用来承载数据。我们需要做的仅仅是:
1. 通过一定过程从数据源获取数据
2. 以一定格式承载该数据
3. 选择一个数据绑定控件
4. 实施绑定
类似以上的绑定过程是标准的MVC模式。实现了数据与格式的完全分离。这样做没有任何问题。也可以很快地完成任务。但是快,简单、快速是不是一定就好呢?但是让我们来多思考一些:
(以下这些问题,大家肯定遇到过,也思考过)
1. 我们的项目中也许会充满了类似的需求,例如需要在不同的情境下列出系统中已经登记的会员表,比如一个是管理员查看所有check in的会员,另一个功能是依据一定准则检索会员。而这两个列表的具体需求是不一样的。“查看在线会员”需要显示用户check in的时间或者当前的状态;而检索会员则也许需要显示会员的活跃程度。但是也有相同的需求,例如都可以通过单击会员的帐户名查看会员的详细资料。这些类似的需求如果没有适当的进行重用,当业务逻辑更改时,我们不得不一遍一遍修改相同的过程。
2. 当列表显示的数据数目过多,就需要进行分页显示。而分页的逻辑大体上都相似。一般的做法是在每一页附加相同的分页过程。如果将大体上逻辑相似的代码在每个数据绑定过程都来回复制粘贴,结果将造成系统臃肿、难以维护和升级。相信没有多少朋友喜欢这样做。
3. 通常数据并不是列出来就完了。而一般都需要对列出的数据进行操作。而通常我们需要一个相当繁琐的过程知道用户目前选中的是第几行的项。这一过程不是特别低级,就需要相当的技巧性。
4. 对已经列出的数据进行筛选,由于Web Form应用的特点,数据已经绑定,后端的数据就释放了。所以我们无法对已经绑定的数据进行二次处理,例如重新排序、进一步筛选等等。
下面就来解释,我们如何设计“类型化”的数据绑定控件,以实现以上提到的目标。
技术要点
优良的设计都是很有技巧性的。如果即不能出现性能问题,也不能不优雅。以下提到的这个方案其实目前的问题还有很多,权做批判之用。做以下几个方面的工作,就可以实现初步的“类型化”,或者说下面几条是一个基本思路:
注:类型化数据绑定控件的前提是已经设计好了一组类型化的结构型数据实体,在这里表现为类型化的DataSet。
1. 建立一个ASCX(Web User Control)控件,使其直接包含一个Repeater或者DataGrid控件
2. 公开一个public property DataSource,类型为结构型数据实体类
3. 重写DataBind方法
4. 公开一组与“分页”相关的API
5. 公开一组“筛选、排序”的API
6. 实现public event Command相关特性
7. 实现“列控制”API
以下仅简要说明。
基础结构与数据缓存
当没有其他特殊要求时,我们的产品在设计视图是非常简单的。甚至可以仅仅包含一个<asp:Repeater .../>或者<asp:DataGrid .../>。因为我们的数据最终还是要通过普通的绑定控件来实现。还有其他基本看见可以使用。具体看需求即可。作者比较喜欢的是Repeater。但Repeater不好实现列控制(通过一些技巧也可以实现)。所以要根据实际需求选用。
这个时候ASCS的Code Behind里还是空的。我们首先需要公开一个最重要的特性:DataSource。这里要注意的是我们需要该控件对已绑定的数据进行缓存。缓存是为了二次处理(排序、筛选)的需要。缓存的方式需要考虑到系统性能需求。可以使用Page ViewState或者Cache。缓存过程可以在public property ... DataSource的set访问器内实现。而get过程返回从缓存中造型(类型转换)出来的数据实体对象。
以下是一段示例代码:
{
get
{
if(this.Context.Cache[System.Web.HttpContext.Current.Session.SessionID + "_DataSource"] != null)
{
return (System.Data.DataTable)this.Context.Cache[System.Web.HttpContext.Current.Session.SessionID + "_DataTable"];
}
else
{
return null;
}
}
set
{
if(value != null)
{
if(this.Context.Cache[System.Web.HttpContext.Current.Session.SessionID + "_DataSource"] != null)
{
this.Context.Cache[System.Web.HttpContext.Current.Session.SessionID + "_DataSource"] = value;
}
else
{
this.Context.Cache.Insert(System.Web.HttpContext.Current.Session.SessionID + "_DataSource", value);
}
}
else
{
this.Context.Cache.Remove(System.Web.HttpContext.Current.Session.SessionID + "_DataSource");
}
}
}
上面的代码使用了缓存。而且每个缓存都用SessionID标识了。也就是说每个用户都会建立一个DataSet缓存。这使非常影响性能的。所以使用要注意,在不使用缓存时及时清空。
另外要重写DataBind方法。DataBind过程中将DataSource设置到子控件的DataSource,并使子控件呈现数据。与DataBind相对的还应有ClearData方法。清空缓存中的数据。
而对于内置其中的Repeater或者DataGrid,我们可以使用HTML定义其外观和样式。对于DataGrid,还设置一些必要的列控制逻辑。而对于同一个应用系统而言,视觉上的外观应该是统一的。最多只是操作方式上的不同。可以针对不同的需求编写不同的表现逻辑,限于篇幅,这里不再赘述。
分页
“分页”曾经是非常热门的话题。可惜那些都过去了。这里要解决的是对特定数据的分页。作者在设计这一方案时将分页逻辑进一步集中在了两个比较通用的组件中。一个是承担分页的系统逻辑,一个承担显示。有关这一话题,可以查阅本文集中的一篇较早的文章:列表和分页器之间的对话。
我们可以确定的是,使用这两个组件,我们可以将分页、显示页码的逻辑交给这两个组件,这两个组件分别以Web Custom Control的方式出现,可以配置每页的项目容量、转到首页、前后页、和末尾页。可以设置以什么样的样式显示页码。
而顶层的控件无需显式实现这些分页API,只需要订阅一个分页器引发的事件,进行重新绑定就行了。
Command机制
Command机制指的是:“将用户选择了列表中的一项后选择对该项的处理指令(查看、删除等)的过程进行封装,以服务器端事件的形式表现出来的过程“。
一个系统或者组件设计得是否良好,在于标准化的程度。这里的“标准化”,可以理解为该组件的用户不用再编写底层的、容易出错的、重复的代码来取得一些数据。
实现Command机制后,用户直接使用鼠标单一某一行或者选中多行后单一一个按钮,我们只需要订阅控件的Command事件就可以了解到发生了什么。通过事件的arguements,可以得到下面的数据:
l CommandName
l 引发该事件的单个项或多个项的标识符
容器(ASPX页)捕获到了包含这两项的事件后,就已经知道了用户的意图。于是就可以调用一定的业务逻辑过程完成该操作。这就是简单意义上的Command机制。
为了实现这一目标,我们的产品需要通过一定的postback-handle过程取得以上值。一般我们在列表绑定数据呈现的时候,将用于识别各项的标识符也输出到前端。例如绑定到check box的value arrtibute上。而选中一行或多行的check box然后单击一个命令按钮(也可以是LinkButton)时,控件前端所附加的脚本将命令标识符(CommandName)以及check box的选中结果一同发送到后端。而后端也通过一定的规则以event arguement的形式将CommandName和选中项返回给容器。
考虑到更高的灵活性,这里的CommandName可以通过配置文件进行配置。而不是各自定义并且硬编码在控件中。例如控件在初始化时读取指定的配置文件,然后通过HTML attribute指定本控件中所需要的command。而对于command组的配置、数据回发等操作,将其逻辑封装在一个Web Custom Control内。这样该Custom Control可以在多个“类型化数据绑定控件”之间重用。
例如以下代码:
我们可以看到该Custom:ListBottomCommand控件有一个Content HTML attribute。其值为delete,create。我们就会理解,这显示两个命令,“删除”和“新建”。显示在控件中可以看到:
内置的筛选和排序
由于列表所绑定的控件是缓存在服务器端的,所以在数据的回发之间还可以取得目前绑定的数据。这就使对数据的重新筛选和排序提供了可能性。筛选以及从现有结果中选择后重新绑定。而排序则是按照一定规则重新排序后重新绑定。
不管是筛选还是排序,都是和特定的列相关的。即控件自己必须知道目前所绑定的数据格式是什么。这也是只有类型化的数据绑定控件可以做到的。当控件进行筛选时,容器需给定筛选的列和关键字;而进行排序时,需给定列和排序准则。
列控制
列控制控制指设置控件每一行显示某列或者不显示某列。可以通过public property HiddenColumnd定义。列的显示和隐藏需要一定系统规则进行。当设计足够精细化时,可以将各列名公开为ENum类型。
结束
至此,大家可以看到,我们构造出了一个构造简单,但逻辑上异常复杂的“类型化”列表。这时,我们只要设法从后端取得一定类型的数据,绑定到“列表”,就不用管其他的事情了。这就使“重用性”成为了可能。