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来处理。

 

posted @ 2018-03-05 15:53  三生石上(FineUI控件)  阅读(6991)  评论(4编辑  收藏  举报