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; } } }
根据当前表格分页索引和总页面设置分页按钮的状态。
此时运行页面,显示效果:
点击Next按钮时,会发起一个页面回发到后台事件:
protected void OnPageButtonClick(object sender, CommandEventArgs e) { switch (e.CommandName) { case "Previous": CurrentPageIndex--; break; case "Next": CurrentPageIndex++; break; } BindGrid(); }
在分页按钮的点击事件中,首先根据e.CommandName来判断点击了哪个按钮,然后从ViewState中读取当前表格分页索引,最后重新绑定表格数据。
点击Next后页面截图如下:
此时页面的回发是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内置了很多主题,因此我们可以在Web.config中设置不同的主题,得到不同的显示效果:
分页事件处理函数也很简单:
protected void Grid1_PageIndexChange(object sender, GridPageEventArgs e) { BindGrid(); }
由于FineUIPro表格自行管理分页信息,因此我们只需要重新绑定数据即可。
此时点击下一页,页面截图:
此时的回发是AJAX POST过程,整个页面不会刷新,在回发过程中,FineUIPro会显示一个回发提示动画:
如果仅从代码和运行效果对比,我们可以看出FineUIPro的表格控件相比ASP.NET原生控件,有如下优点:
1. 代码有90%和原生控件保持一致
2. 代码更少(得益于FineUIPro表格对服务器端分页的内置支持)
3. 页面显示效果更美观大方,并且可以通过全局配置切换不同的显示样式
4. 分页过程是AJAX部分刷新,并内置了提示动画
另外,全部示例代码没有一行JavaScript和CSS代码,但是实际上FineUIPro却是严重依赖JavaScript和CSS来实现页面效果和交互。
下面我们会深入分析两个示例的异同。
页面渲染的对比
虽然两个示例的大部分ASPX和C#代码一模一样,但是从一开始两者的实现方式就完全不同。
ASP.NET的表格控件
首先来看下ASP.NET表格控件生成的页面HTML代码:
简化后看的更清楚:
<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代码:
简化一下:
<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结构:
只所以有这么多的层次结构,是有很多原因的,简单来说:
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层次结构在启用列锁定时会变的更加复杂,如下所示:
启用列锁定时,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表格的分页回发过程:
可以看出:
1. ASP.NET表格页面回发是整个页面刷新,返回的是完整的HTML标签(包含html,head,body....)
2. 由于是页面重新渲染,所以页面资源会重新加载,比如common.css文件
FineUIPro表格的回发(AJAX部分刷新)
浏览器中F12,打开Network选项卡,观察FineUIPro表格的分页回发过程:
可以看出,请求参数中包含X-Requested-With=XMLHttpRequest参数,说明这是一个AJAX部分刷新过程
返回的响应正文如下所示:
这是一段JavaScript代码,其中包含表格当前页的数据,并通过表格的客户端API函数来重现加载表格数据。
由于是部分刷新,页面资源无需重新加载,整个页面DOM节点也无需重建,而且响应正文的大小也要小很多。
源代码下载
下载后放到FineUIPro官网示例源代码中即可:
https://files.cnblogs.com/files/sanshi/fineuipro_database_paging.zip
小结
经过上述分析,我们可以得知,FineUIPro使用JavaScript来渲染页面,并且使用jQuery.ajax来更新页面控件。
对于开发人员来说这一切都是透明的,开发人员只需要关注ASPX和C#代码,关注自己的业务既可以了,剩下的都丢给FineUIPro来处理。