FineUIPro控件库深度解析

时间:2023-03-09 15:22:32
FineUIPro控件库深度解析

FineUIPro控件库

FineUIPro是一套基于jQuery的专业ASP.NET控件库,始于2008年的开源版FineUI控件库。

当年为了提升项目的开发效率,降低代码复杂度,减少对CSS和JavaScript的依赖,我们提出了"No JavaScript, No CSS, No UpdatePanel,No ViewState,No WebServices"的口号,现在看起来仍然激动人心。

首先,JavaScript灵活性与复杂性使得大型项目的开发备受挑战,FineUIPro尝试使用服务器端的强类型语言(C#,VB.NET)来代替大部分的JavaScript实现,不仅可以利用IDE的强大功能(智能提示,代码重构),而且强类型语言的编译时错误检查也是一个加分项。

其次,FineUIPro提供统一的控件集合和页面主题,使得我们无需在代码中自定义CSS样式,不仅减少编码和调试CSS的工作量,而且能够保持整个项目中页面风格的统一和美观。

最后,FineUIPro内置了AJAX的交互支持,使得我们无需写一行JavaScript代码,就能把整个页面的回发变为AJAX过程。另外,FineUIPro也内置了IFrame支持,有助于在页面层级对代码进行解耦合。

那么,FineUIPro是如何工作的呢?FineUIPro的控件使用和原生的ASP.NET控件有哪些异同点?FineUIPro的AJAX交互过程又是什么样子的呢?

为了回答这些问题,我们将分别使用FineUIPro和ASP.NET控件来实现一个服务器端分页的表格页面。

ASP.NET的表格控件

首先来看下ASP.NET的原生GridView控件定义:

<asp:GridView ID="Grid1" Title="表格" Width="800px" DataKeyNames="Id,Name" ShowBorder="true"
runat="server" EnableCheckBoxSelect="True" AutoGenerateColumns="False">
<Columns>
<asp:BoundField DataField="Name" DataFormatString="{0}" HeaderText="姓名" />
<asp:TemplateField HeaderText="性别">
<ItemTemplate>
<asp:Label ID="Label2" runat="server" Text='<%# GetGender(Eval("Gender")) %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="EntranceYear" HeaderText="入学年份" />
<asp:CheckBoxField DataField="AtSchool" HeaderText="是否在校" />
<asp:HyperLinkField HeaderText="所学专业" DataTextField="Major"
DataTextFormatString="{0}" DataNavigateUrlFields="Major" DataNavigateUrlFormatString="http://gsa.ustc.edu.cn/search?q={0}"
Target="_blank" />
<asp:ImageField DataImageUrlField="Group" DataImageUrlFormatString="~/res/images/16/{0}.png"
HeaderText="分组">
</asp:ImageField>
</Columns>
</asp:GridView>

由于GridView并不支持服务器端分页,因此我们没有设置表格的AllowPaging和PageSize属性,而是自定义了两个按钮来实现服务器端分页:

<asp:Button ID="btnPrevious" CommandName="Previous" runat="server" OnCommand="OnPageButtonClick" Text="Previous" />
<asp:Button ID="btnNext" runat="server" CommandName="Next" OnCommand="OnPageButtonClick" Text="Next" />
Page
<asp:Label runat="server" ID="lblCurrentPage"></asp:Label>
of
<asp:Label runat="server" ID="lblTotalPages"></asp:Label>  

页面第一次打开时需要加载表格数据:

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
BindGrid();
}
} private void BindGrid()
{
// 1.设置总项数
int recordCount = GetTotalCount(); // 2.获取当前分页数据
DataTable table = GetPagedDataTable(CurrentPageIndex, PAGE_SIZE); // 3.绑定到Grid
Grid1.DataSource = table;
Grid1.DataBind(); UpdatePageControls(recordCount);
}  

绑定表格数据分为如下几个步骤:

1. 获取总记录数

2. 获取当前分页数据

3. 绑定分页数据到表格

其实,表格对象对当前分页状态一无所知(第几页,总共有几页),我们需要自己在页面上保存这些数据:

private int CurrentPageIndex
{
get
{
var pageIndexState = ViewState["CurrentPageIndex"];
if (pageIndexState == null)
{
return 0;
}
else
{
return Convert.ToInt32(pageIndexState);
}
}
set
{
ViewState["CurrentPageIndex"] = value;
}
}
private const int PAGE_SIZE = 5; private int CalculatePageCount(int recordCount)
{
int pageCount = recordCount / PAGE_SIZE;
if (recordCount % PAGE_SIZE != 0)
{
pageCount++;
}
return pageCount;
}  

将当前表格分页索引CurrentPageIndex保存到ViewState中,以便在后续的页面回发中获取分页索引。

总页数可以根据当前分页索引和每页记录数计算而来,我们将其逻辑封装到CalculatePageCount方法中。

最后,来看下UpdatePageControls方法:

private void UpdatePageControls(int recordCount)
{
int pageCount = CalculatePageCount(recordCount); lblTotalPages.Text = pageCount.ToString();
lblCurrentPage.Text = (CurrentPageIndex + 1).ToString();
if (CurrentPageIndex == 0)
{
btnPrevious.Enabled = false; if (pageCount > 0)
{
btnNext.Enabled = true;
}
else
{
btnNext.Enabled = false;
}
}
else
{
btnPrevious.Enabled = true; if (CurrentPageIndex == pageCount - 1)
{
btnNext.Enabled = false;
}
else
{
btnNext.Enabled = true;
}
}
}  

根据当前表格分页索引和总页面设置分页按钮的状态。

此时运行页面,显示效果:

FineUIPro控件库深度解析

点击Next按钮时,会发起一个页面回发到后台事件:

protected void OnPageButtonClick(object sender, CommandEventArgs e)
{
switch (e.CommandName)
{
case "Previous":
CurrentPageIndex--;
break;
case "Next":
CurrentPageIndex++;
break;
} BindGrid();
}  

在分页按钮的点击事件中,首先根据e.CommandName来判断点击了哪个按钮,然后从ViewState中读取当前表格分页索引,最后重新绑定表格数据。

点击Next后页面截图如下:

FineUIPro控件库深度解析

此时页面的回发是Form表单的POST过程,因此会导致整个页面的刷新,用户体验并不好。

FineUIPro的表格控件

FineUIPro中的大部分实现代码和GridView的实现代码一样。

不过由于FineUIPro表格默认支持服务器端分页,因此无需在后台通过ViewState保存表格分页索引,也无需自己动手更新分页按钮的状态,因此代码要简单的多。

<f:PageManager ID="PageManager1" AjaxLoadingType="Mask" runat="server" />
<f:Grid ID="Grid1" Title="表格" Width="800px" DataKeyNames="Id,Name" ShowBorder="true" ShowHeader="true"
AllowPaging="true" IsDatabasePaging="true" PageSize="5" runat="server" EnableCheckBoxSelect="True"
OnPageIndexChange="Grid1_PageIndexChange">
<Columns>
<f:RowNumberField />
<f:BoundField DataField="Name" DataFormatString="{0}" HeaderText="姓名" />
<f:TemplateField HeaderText="性别">
<ItemTemplate>
<asp:Label ID="Label2" runat="server" Text='<%# GetGender(Eval("Gender")) %>'></asp:Label>
</ItemTemplate>
</f:TemplateField>
<f:BoundField DataField="EntranceYear" HeaderText="入学年份" />
<f:CheckBoxField RenderAsStaticField="true" DataField="AtSchool" HeaderText="是否在校" />
<f:HyperLinkField HeaderText="所学专业" DataTextField="Major"
DataTextFormatString="{0}" DataNavigateUrlFields="Major" DataNavigateUrlFormatString="http://gsa.ustc.edu.cn/search?q={0}" UrlEncode="true"
Target="_blank" ExpandUnusedSpace="True" />
<f:ImageField DataImageUrlField="Group" DataImageUrlFormatString="~/res/images/16/{0}.png"
HeaderText="分组">
</f:ImageField>
</Columns>
</f:Grid>  

这个表格定义和之前的GridView很类似,有几点不同的地方:

1. PageManager是每一个使用FineUIPro控件的页面都需要的,其中的AjaxLoadingType用来定义AJAX回发的提示类型。

2. 表格控件的AllowPaging,IsDatabasePaging,PageSize用来指定服务器端分页和分页记录大小,这样就无需自己维护分页信息了。

3. 表格控件的PageIndexChanged用来定义服务器端分页事件。

表格列还有一些特定的属性,实现不同的显示效果:

4.1. 表格列定义了RowNumberField,用来显示行序号。

4.2 CheckBoxField的RenderAsStaticField用来指定复选框的显示样式。

4.3 HyperLinkField的ExpandUnusedSpace用来定义本列宽度占据所有未使用空间。

后台数据绑定代码很简单:

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
BindGrid();
}
} private void BindGrid()
{
// 1.设置总项数
Grid1.RecordCount = GetTotalCount(); // 2.获取当前分页数据
DataTable table = GetPagedDataTable(Grid1.PageIndex, Grid1.PageSize); // 3.绑定到Grid
Grid1.DataSource = table;
Grid1.DataBind();
}  

此时页面显示效果:

FineUIPro控件库深度解析

由于FineUIPro内置了很多主题,因此我们可以在Web.config中设置不同的主题,得到不同的显示效果:

FineUIPro控件库深度解析

FineUIPro控件库深度解析

FineUIPro控件库深度解析

分页事件处理函数也很简单:

protected void Grid1_PageIndexChange(object sender, GridPageEventArgs e)
{
BindGrid();
}  

由于FineUIPro表格自行管理分页信息,因此我们只需要重新绑定数据即可。

此时点击下一页,页面截图:

FineUIPro控件库深度解析

此时的回发是AJAX POST过程,整个页面不会刷新,在回发过程中,FineUIPro会显示一个回发提示动画:

FineUIPro控件库深度解析

如果仅从代码和运行效果对比,我们可以看出FineUIPro的表格控件相比ASP.NET原生控件,有如下优点:

1. 代码有90%和原生控件保持一致

2. 代码更少(得益于FineUIPro表格对服务器端分页的内置支持)

3. 页面显示效果更美观大方,并且可以通过全局配置切换不同的显示样式

4. 分页过程是AJAX部分刷新,并内置了提示动画

另外,全部示例代码没有一行JavaScript和CSS代码,但是实际上FineUIPro却是严重依赖JavaScript和CSS来实现页面效果和交互。

下面我们会深入分析两个示例的异同。

页面渲染的对比

虽然两个示例的大部分ASPX和C#代码一模一样,但是从一开始两者的实现方式就完全不同。

ASP.NET的表格控件

首先来看下ASP.NET表格控件生成的页面HTML代码:

FineUIPro控件库深度解析

简化后看的更清楚:

<table>
<tr>
<th scope="col">姓名</th>
<th scope="col">性别</th>
<th scope="col">入学年份</th>
<th scope="col">是否在校</th>
<th scope="col">所学专业</th>
<th scope="col">分组</th>
</tr>
<tr>
<td>陈萍萍</td>
<td><span id="Grid1_ctl02_Label2">女</span></td>
<td>2000</td>
<td><input type="checkbox" checked="checked" disabled="disabled" /></td>
<td><a href="http://gsa.ustc.edu.cn/search?q=计算机应用技术" target="_blank">计算机应用技术</a></td>
<td><img src="../res/images/16/1.png" /></td>
</tr>
</table> <input type="button" name="btnPrevious" value="Previous" id="btnPrevious" disabled="disabled" />
<input type="button" name="btnNext" value="Next" onclick="javascript:__doPostBack('btnNext','')" id="btnNext" />
Page
<span id="lblCurrentPage">1</span>
of
<span id="lblTotalPages">5</span>  

可以看出:

1. ASP.NET表格渲染到页面上是<table>标签,并且包含了当前页的全部数据

2. 分页按钮最终调用的__doPostBack函数,这个函数我们并不陌生,几乎每个页面都包含这样一个默认的定义

<script type="text/javascript">
var theForm = document.forms['form1'];
if (!theForm) {
theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
</script>  

毫无疑问,调用此回发函数,其实就是对页面上全局表单对象的提交(theForm.submit()),这将会是整个页面的刷新。

FineUIPro的表格控件

FineUIPro表格控件生成的页面HTML代码:

FineUIPro控件库深度解析

简化一下:

<div id="Grid1_wrapper">
<div id="Grid1_tpls" class="f-grid-tpls f-hidden">
<div class="f-grid-tpl" id="Grid1_ftpl_frow0_2">
<span id="Grid1_ftpl_frow0_2_Label2">女</span>
</div>
...
</div>
</div> <script type="text/javascript">
F.load(function() { new F.Grid({
renderTo: '#Grid1_wrapper',
title: '表格',
data: [{
"f0": ["", "陈萍萍", "#@TPL@#ftpl_frow0_2", "2000", "<i class=\"f-icon f-iconfont f-grid-static-checkbox f-checked\"></i>", "<a href=\"http://gsa.ustc.edu.cn/search?q=%e8%ae%a1%e7%ae%97%e6%9c%ba%e5%ba%94%e7%94%a8%e6%8a%80%e6%9c%af\" target=\"_blank\">计算机应用技术</a>", "<img src=\"/res/images/16/1.png\" class=\"f-grid-imagefield\"></img>"],
"f1": [101, "陈萍萍"],
"f6": "frow0"
}],
paging: true,
databasePaging: true,
pageSize: 5,
pageIndex: 0,
recordCount: 22,
listeners: {
paging: function(event, pageIndex, oldPageIndex) {
__doPostBack('Grid1', 'Page$' + pageIndex + '$' + oldPageIndex);
}
}
});
});
</script>  

可以看出:

1. 表格数据在JavaScript代码中,并渲染到页面上一个容器(Grid1_wrapper)

2. 分页事件同样触发的是__doPostBack事件

两相对比,我们可以得出如下结论:

1. ASP.NET表格控件直接渲染为table标签(包含数据)

2. FineUIPro表格控件会在页面上生成一个div占位符,然后通过JavaScript来渲染出表格控件

FineUIPro的做法更加灵活,并且可以实现更加复杂的显示效果,看下生成的DOM结构:

FineUIPro控件库深度解析

只所以有这么多的层次结构,是有很多原因的,简单来说:

1. FineUIPro中表格是从面板继承下来的,所以最外层的div节点是面板相关的

div.f-panel
        ->div.f-panel-header
        ->div.f-panel-bodyct
                ->div.f-panel-body

2. f-panel-body里面的层次才是表格的特定结构

div.f-panel-body

->div.f-grid-inner

->div.f-grid-headerct

->div.f-grid-bodyct

->table.f-grid-table

表格的这个特定DOM层次结构在启用列锁定时会变的更加复杂,如下所示:

FineUIPro控件库深度解析

启用列锁定时,f-grid-inner里面会分裂成两部分,分别对应于锁定表格和主表格,FineUIPro会负责这两部分的同步工作。

由此可知,ASP.NET表格控件直接渲染table节点和数据的方式仅适合于简单的形式,而FineUIPro为了更加好看的界面效果和更加复杂的逻辑实现,必须通过JavaScript来渲染界面和数据。而这一切对于开发人员都是透明的,FineUIPro开发人员只需要写ASPX表格和C#代码即可,剩下的交给我们。

页面回发的对比

前面分析可知,ASP.NET表格和FineUIPro的分页回发都是调用的__doPostBack函数,为什么一个是整个页面刷新,而另一个是AJAX部分刷新?

这是因为FineUIPro耍了个小把戏,重写了__doPostBack函数,翻开FineUIPro的客户端JavaScript源代码:

function _fjs_doPostBack(eventTarget, eventArgument, options) {
$.ajax({
type: 'POST',
url: url,
data: formDataBeforeAJAX,
dataType: 'text',
headers: {
'X-FineUI-Ajax': true
},
success: function (data) {
},
error: function (xhr, textStatus) {
},
complete: function (xhr, textStatus) {
ajaxComplete(xhr.responseText, textStatus, xhr);
}
});
} (function() {
if (!isUND(__doPostBack)) {
__originalDoPostBack = __doPostBack;
__doPostBack = _fjs_doPostBack;
}
})();  

这是简化后的代码,可以看到FineUIPro重新赋值:__doPostBack=_fjs_doPostBack;

而在_fjs_doPostBack中,调用了jQuery.ajax来发起AJAX请求,当然实际的实现要复杂的多,FineUIPro让这一切变得透明起来,开发人员甚至不用写一行JavaScript代码就能享受jQuery.ajax的无刷新回发。

ASP.NET表格的回发(整个页面刷新)

浏览器中F12,打开Network选项卡,观察ASP.NET表格的分页回发过程:

FineUIPro控件库深度解析

可以看出:

1. ASP.NET表格页面回发是整个页面刷新,返回的是完整的HTML标签(包含html,head,body....)

2. 由于是页面重新渲染,所以页面资源会重新加载,比如common.css文件

FineUIPro表格的回发(AJAX部分刷新)

浏览器中F12,打开Network选项卡,观察FineUIPro表格的分页回发过程:

FineUIPro控件库深度解析

可以看出,请求参数中包含X-Requested-With=XMLHttpRequest参数,说明这是一个AJAX部分刷新过程

返回的响应正文如下所示:

FineUIPro控件库深度解析

这是一段JavaScript代码,其中包含表格当前页的数据,并通过表格的客户端API函数来重现加载表格数据。

由于是部分刷新,页面资源无需重新加载,整个页面DOM节点也无需重建,而且响应正文的大小也要小很多。

源代码下载

下载后放到FineUIPro官网示例源代码中即可:

https://files.cnblogs.com/files/sanshi/fineuipro_database_paging.zip

小结

经过上述分析,我们可以得知,FineUIPro使用JavaScript来渲染页面,并且使用jQuery.ajax来更新页面控件。

对于开发人员来说这一切都是透明的,开发人员只需要关注ASPX和C#代码,关注自己的业务既可以了,剩下的都丢给FineUIPro来处理。