通用ASPNET数据分页控件
对于几乎所有的数据表现Web应用来说组织好数据的显示方式避免给用户带来混乱的感觉就是最主 要的目标之一每个页面显示条记录当然是可以接受的但每页显示条记录就很容易给用户带来不便了将数据分成多个页面显示即对数据进行分 页是解决此类问题的最常见的办法
一慨述
ASPNET本身只提供了一个支持数据分页的控件即 DataGrid分页控件不过它比较适合Intranet环境使用对于Internet环境来说DataGrid分页控件提供的功能似乎不足以构造 出灵活的Web应用其中一个原因是DataGrid控件对Web设计者放置分页控件的位置和分页控件的外观都有限制例如DataGrid控件不允 许垂直放置分页控件另一个能够发挥分页技术优势的控件是RepeaterWeb开发者可以利用Repeater控件快速配置数据的显示方式但分页功 能却需要开发者自己实现数据源在不断地变化数据表现方式也千差万别如果针对这些不断变动的条件分别定制分页控件显然太浪费时间了构造一个不限于 特定表现控件的通用分页控件将极大地有利于节省时间
一个优秀的通用数据控件不仅提供常规的分页功能而且还要能够
⑴ 提供首页上一页下一页末页分页导航按钮
⑵ 根据数据显示情况调整自身的状态即具有数据敏感性如果分页控件被设置成每页显示个记录但实际上只有个记录那么分页控件不应该显示出来在数 据分成多页显示的情况下第一个页面的首页上一页按钮不应显示出来最后一个页面的下一页末页按钮也不应该显示出来
⑶ 不能依赖于特定的数据显示控件
⑷ 具有适应各种现有将有数据源的能力
⑸ 应当能够方便地配置显示方式轻松地集成到各种应用之中
⑹ 当分页就绪时提醒其他控件
⑺ 即使是缺乏经验的Web设计者也要能够毫无困难地使用
⑻ 提供有关分页信息的属性数据
目前市场上已经有一些提供上述功能的商业性控件不过都价格不菲对于许多开发者来说自己构造一个通用的分页控件是最理想的选择
图一显示了本文通用分页控件的运行界面其中用于显示的控件是一个Repeater控件分页控件由两类部件构成四个导航按钮一组页面编号链接
用户可以方便地改换显示控件改变分页控件本身的外观例如在图一中和分页控件协作的显示控件可发换成一个DataGrid控件页面编号链接和四个导航按钮分两行显示
ASPNET 支持创建定制Web控件的三种方式用户控件复合控件自定义控件第三种控件即自定义控件的名称很容易引起误解实际上所有这三种控件都应该算是自 定义控件复合控件和微软所谓的自定义控件的不同之处在于前者要用到CreateChildControls()方法 CreateChildControls()方法允许控件根据某些事件重新绘制自身对于本文的通用分页器我们将使用复合控件
下面的UML序列图概括了通用分页控件的一般机制
虽然我们的目标是让通用分页控件不依赖于表现数据的控件但很显然总得有某种方法让分页控件访问数据每一个从Control类继承的控件都提供一个 DataBinding事件我们把分页器本身注册成DataBinding事件的监听器分页器就可以获知数据的情况并修改数据由于所有从 Control类继承的控件都有这个DataBinding事件所以分页器控件达到了不依赖于特定数据表现控件的目标——换句话说分页器控件可以绑定 到所有从Control类派生的控件即它能够绑定到几乎所有的Web控件
二核心功能
当表现控件触发DataBinding事件分页控件就可以获取DataSource属性遗憾的是微软没有提供所 有数据绑定类实现的接口诸如IdataSourceProvider之类而且并非所有从Control或WebControl类继承的控件都有一个 DataSource属性因此向上定型成Control类没有意义唯一可行的办法是通过Reflection API直接操作DataSoruce属性在讨论事件句柄方法之前应该指出的是为了注册事件句柄首先必须获得一个表现控件的引用分页控件显露了一 个简单的字符串属性BindToControl
publicstringBindToControl
{
get
{
if(_bindcontrol==null)
thrownewNullReferenceException(在使用分页控件之前请先通过设置BindToControl属性绑定到一个控件);
return_bindcontrol;}
set{_bindcontrol=value;}
}
这个方法非常重要所以最好能够抛出一个含义更明确的信息而不是抛出标准的NullReferenceException异常在分页控件的 OnInit方法中我们解析了对表现控件的引用本例应当用OnInit事件句柄(而不是构造函数)来确保JIT编译的aspx页面已经设置了 BindToControl
protectedoverridevoidOnInit(EventArgse)
{
_boundcontrol=ParentFindControl(BindToControl);
BoundControlDataBinding+=newEventHandler(BoundControl_DataBound);
baseOnInit(E);
}
搜索表现控件的操作通过搜索分页控件的Parent控件完成在这里Parent就是页面本身按照这种方式使用Parent比较危险举例来说如 果分页控件嵌入到了另一个控件之中例如嵌入到了Table控件之中则Parent引用实际上将是一个对Table控件的引用由于 FindControl方法只搜索当前的控件集合除非表现控件就在该集合之中否则不可能搜索到一种比较安全的方法是递归地搜索各个控件集合直至找 到目标控件为止
找到BoundControl之后我们将分页控件注册成为DataBinding事件的监听器由于分页控件要操作数 据源所以该事件句柄应当是调用链中的最后一个这一点很重要不过只要表现控件在OnInit事件句柄中注册DataBinding的事件句柄(默认 行为)分页控件操作数据源时就不会出现问题
DataBound事件句柄负责获取表现控件的DataSource属性
privatevoidBoundControl_DataBound(objectsenderSystemEventArgse)
{
if(HasParentControlCalledDataBinding)return;
Typetype=senderGetType();
_datasource=typeGetProperty(DataSource);
if(_datasource==null)
thrownewNotSupportedException(分页控件要求表现控件必需包含一个DataSource);
objectdata=_datasourceGetGetMethod()Invoke(sendernull);
_builder=Adapters[dataGetType()];
if(_builder==null)
thrownewNullReferenceException(没有安装适当的适配器来处理下面的数据源类型+dataGetType());
_builderSource=data;
ApplyDataSensitivityRules();
BindParent();
RaiseEvent(DataUpdatethis);
}
在DataBound中我们尝试通过Reflection API获得DataSource属性然后返回实际数据源的一个引用现在虽然已经获知了数据源但分页控件还必须知道如何操作该数据源为了让分页控件 不依赖于特定的表现控件问题复杂了很多不过如果让分页控件依赖于特定的数据源那就背离了设计一个灵活的分页控件的目标我们要通过一个接插式的体 系结构来确保分页控件能够处理各种数据源无论是NET提供的数据源还是自定义的数据源
为了提供一个健壮的可伸缩的接插式体系结构我们将利用[GoF] Builder模式构造出一个解决方案
IDataSourceAdapter接口定义了分页控件操作数据所需的最基本的元素相当于插头
publicinterfaceIDataSourceAdapter
{
intTotalCount{get;}
objectGetPagedData(intstartintend);
}
TotalCount属性返回在处理数据之前数据源所包含元素的总数而GetPagedData方法返回原始数据的一个子集例如假设数据源是一个 包含个元素的数组分页控件将数据显示成每页个元素则第一页的元素子集是数组元素第二页的元素子集是数组元素 DataViewAdapter提供了一个DataView类型的插头
internalclassDataViewAdapter:IDataSourceAdapter
{
privateDataView_view;
internalDataViewAdapter(DataViewview)
{
_view=view;
}
publicintTotalCount
{
get{return(_view==null)?:_viewTableRowsCount;}
}
publicobjectGetPagedData(intstartintend)
{
DataTabletable=_viewTableClone();
for(inti=start;i<=end&&i<=TotalCount;i++)
{
tableImportRow(_view[i]Row);
}
returntable;
}
}
DataViewAdapter实现了IDataSourceAdapter的GetPagedData方法该GetPagedData克隆原始的 DataTable将原始DataTable中的数据导入到新的DataTable该类的可见性有意地设置成internal目的是为了向Web开 发者隐藏实现细节进而通过Builder类提供一个更简单的接口
publicabstractclassAdapterBuilder
{
privateobject_source;
privatevoidCheckForNull()
{
if(_source==null)thrownewNullReferenceException(必须提供一个合法的数据源);
}
publicvirtualobjectSource
{
get
{
CheckForNull();
return_source;}
set
{
_source=value;
CheckForNull();
}
}
publicabstractIDataSourceAdapterAdapter{get;}
}
AdapterBuilder抽象类为IdataSourceAdapter类型提供了一个更容易管理的接口由于提高了抽象程度我们不必再直接使用 IdataSourceAdapter同时AdapterBuilder还提供了在分页数据之前执行预处理的指令另外该Builder还使得实际的 实现类例如DataViewAdapter对分页控件的用户透明
publicclassDataTableAdapterBuilder:AdapterBuilder
{
privateDataViewAdapter_adapter;
privateDataViewAdapterViewAdapter
{
get
{
if(_adapter==null)
{
DataTabletable=(DataTable)Source;
_adapter=newDataViewAdapter(tableDefaultView);
}
return_adapter;
}
}
publicoverrideIDataSourceAdapterAdapter
{
get
{
returnViewAdapter;
}
}
}
publicclassDataViewAdapterBuilder:AdapterBuilder
{
privateDataViewAdapter_adapter;
privateDataViewAdapterViewAdapter
{
get
{//延迟实例化
if(_adapter==null)
{
_adapter=newDataViewAdapter((DataView)Source);
}
return_adapter;
}
}
publicoverrideIDataSourceAdapterAdapter
{
get{returnViewAdapter;}
}
}