一直以来觉得用JSON和JavaScript在客户端绑定数据给一个表格或者Grid是件很麻烦的事情。Microsoft ASP.NET Ajax提供了类似Sys.Date.DataTable和Sys.Dat.DataView这样的类来帮助实现客户端绑定,也可以用for循环来动态构建表格,但这些都显得很麻烦而且很不灵活。jQuery的jTemplates插件实现了一种灵活的方式来控制显示,它允许我们定义好一个显示模板,jQuery在展现数据时根据选择的模板来动态生成。这就类似于ASP.NET中的ItemTemplate,也和XSLT有些类似。通过这样的方式,你可以很容易的在客户端通过自定义模板以很灵活的方式展现列表数据。
jQuery官方网站给jTemplates的定义是:jTemplates is a template engine 100% in JavaScript.更多的信息可以参考http://jtemplates.tpython.com/。 接下来我们实现一个小例子来演示如何使用jTemplate去构建一个RSS阅读器。
创建RSS阅读器
RSS源通常都位于另外的domain中,而在AJAX中浏览器通常禁止cross-domain的访问,在这里,为了避开这个问题我们可以在服务端去取得数据。通常我们可以将这些方法做成WebMethod方法放到WebServices中,但这里我们可以将这些方法放到页面的cs文件中。需要注意的是,这个方法必须被声明为Static方法,而且要以WebMethod标注。这样做的目的是,只有在标注为WebMethod,客户端才能正常访问这个方法。而Static标记标识了这个方法不与任何这个页面的实例相关,而是而这个页面本身相关,对于任何一个实例而言都是相同的。所以在你调用的时候,你不需要与任何页面类的实例相关。
[WebMethod]
public static IEnumerable GetFeeds(int PageSize,int PageNumber)
{
string strFeedUrl = System.Configuration.ConfigurationManager.AppSettings["FeedUrl"];
XDocument feedXML = XDocument.Load(strFeedUrl);
var feeds = from feed in feedXML.Descendants("item")
select new
{
Date = DateTime.Parse(feed.Element("pubDate").Value).ToShortDateString(),
Title = feed.Element("title").Value,
Link = feed.Element("link").Value,
Description = feed.Element("description").Value,
};
//paging... (LINQ)
return feeds.Skip((PageNumber - 1) * PageSize).Take(PageSize);
}
在上边的方法中设定了RSS的地址,并通过LINQ to XML来取得我们想要的属性。Skip和Take函数联合起来实现了一个分页的功能。
通过jQuery调用Page Method
jQuery.Ajax方法实现了用Ajax的方式来请求一个页面并设定回调函数来处理相应状态和结果。在我们的实例中,需要请求上边写的PageMethod并处理返回结果。
function DisplayRSS(page) {
$.ajax({
type: "POST",
url: "Default.aspx/GetFeeds",
data: "{'PageSize':'" + pageSize + "', 'PageNumber':'" + page + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
//TODO:Show the result as a table.
alert(msg);
}
});
}
在success的回调函数中我们什么也没有做,先来看看result到底是个什么东西。在浏览器中设置允许调试脚本,定义一个函数供回调函数中调用,然后设定断点在新的函数中。
我们看到在服务端将数据以IEnumerable返回后实际上在result中包含的是一个JSON表示的数据集合。每个对象含有Date, Description, Link和Title属性,这和前边用LINQ取XML字段时是相符的。因为你已经拥有了这个数据集合,接下来所要做的就是在客户端通过某种方式展现出来。你也许会想到用动态拼接Table的方式来做,但这并不灵活。jTemplates提供了更优雅的方式来实现。
用jTemplate来展现数据
jTemplate就把数据展现的方式和业务逻辑分开来,允许你定义好一个模板,然后再运行时会根据模板的内容来render. 和ASP.NET中的绑定相似,也有一个特定的符号来表示绑定的上下文对象$T。$T就类似于上图中的result.d[n]—某一个业务对象,需要展现哪个属性后边直接跟.PropertyName即可。因为表格的行是可变的,所以这里就类似于XSLT中一样来控制动态生成.
<table id="RSSTable" cellspacing="0">
<thead>
<tr>
<th width="80">Date</th>
<th>Title / Excerpt</th>
</tr>
</thead>
<tbody>
{#foreach $T.d as post}
<tr>
<td rowspan="2">{$T.post.Date}</td>
<td><a href="{$T.post.Link}">{$T.post.Title}</a></td>
</tr>
<tr>
<td>{$T.post.Description}</td>
</tr>
{#/for}
</tbody>
</table>
在页面请求完PageMethod并成功返回后可以来应用模板展现数据了:
$(document).ready(function() {
// On page load, display the first page of results.
DisplayRSS(1);
});
function DisplayRSS(page) {
……
success: function(msg) {
// Render the resulting data, via template.
ApplyTemplate(msg);
// TODO: Update navigation status
}
……
}
function ApplyTemplate(msg) {
$('#Container').setTemplateURL('Template/RSSTable.htm',
null, { filter_data: false });
$('#Container').processTemplate(msg);
}
现在为止我们只取得了数据并展现了特定的条数而无法实现分页。看起来一切都好—除了分页。
增加客户端分页功能
为了实现分页首先需要知道页码总数,这样我们可以简单的通过Previous | Next来实现导航。假设一下在服务端我们需要什么:总页数,页大小,当前页,判断并控制Previous|Next导航的有效性及其动作。ok,明白我们所要做的步骤:
· 获取页总数 – 通过Page Method来返回
· 控制页大小和当前页
· 控制Previous | Next导航的有效性
· 实现Previous | Next导航动作
首先,在Template中增加页面导航:
<div id="Paging">
<a id="PrevPage" class="paging">« Previous Page</a>
<a id="NextPage" class="paging">Next Page »</a>
</div>
其次,声明获取页面总数或者条目总数的Page Method. 和前边获取数据源的方法很类似我们除了不需要返回任何XML的属性值外仅仅调用Count方法即可。
[WebMethod]
public static int GetFeedsCount()
{
string strFeedUrl = System.Configuration.ConfigurationManager.AppSettings["FeedUrl"];
XDocument feedXML = XDocument.Load(strFeedUrl);
return feedXML.Descendants("item").Count();
}
设置默认的页面大小,并在显示数据后更新导航栏状态。在页面中我们需要请求这个PageMethod并来计算总的页数
var currentPage = 1;
var lastPage = 1;
var pageSize = 5;
function GetRSSItemCount() {
$.ajax({
type: "POST",
url: "Default.aspx/GetFeedsCount",
data: "{}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
// msg.d will contain the total number of items, as
// an integer. Divide to find total number of pages.
lastPage = Math.ceil(msg.d / pageSize);
// TODO: Update navigation status
}
});
}
接下来就是实现// TODO: Update navigation status标记的内容了:更新导航栏的状态及其动作。当前页的值存储在currentPage变量中,lastPage告诉我们哪一页是最后页,通过这两个参数可以很容易的控制导航状态。而因为他们是全局变量,所以执行导航时只需要通过简单的++/--操作,最终请求GetFeeds方法时会取得相应页的数据。
function UpdatePaging() {
// If we're not on the first page, enable the "Previous" link.
if (currentPage != 1) {
$('#PrevPage').attr('href', '#');
$('#PrevPage').click(PrevPage);
}
// If we're not on the last page, enable the "Next" link.
if (currentPage != lastPage) {
$('#NextPage').attr('href', '#');
$('#NextPage').click(NextPage);
}
}
function NextPage(evt) {
// Prevent the browser from navigating needlessly to #.
evt.preventDefault();
// Entertain the user while the previous page is loaded.
DisplayProgressIndication();
// Load and render the next page of results, and
// increment the current page number.
DisplayRSS(++currentPage);
}
function PrevPage(evt) {
// Prevent the browser from navigating needlessly to #.
evt.preventDefault();
// Entertain the user while the previous page is loaded.
DisplayProgressIndication();
// Load and render the previous page of results, and
// decrement the current page number.
DisplayRSS(--currentPage);
}
用UpdatePaging函数替换上边标注的TODO部分,完整的分页功能即可实现。当然,这里的分页显得很粗糙,但你可以通过扩展其样式来做出更丰富的导航栏。另外示例中也提供了loading时提示用户,这里不尽具表。一个很好的启示是方便的展现LIST并提供简单的客户端分页。
有话要说