我想要研究此技术最好的办法就是把代码分离出来放在自己的工程中,重现这些功能,在重现这些功能的时候一定会遇到这样或那样的问题,这恰好是我们应该着重研究的地方,所以让我们先来分离代码吧。
就拿CS中的一个Ajax小应用来说,当具有系统管理员权限的用户进入后,可以看到页面的标题和一些新闻内容是可以无刷新改变的,让我们先从此处下手吧。找到相应的控件页面,我们会发现,这些控件引用的一些文件,我现在先把它们列出来,下面的截图是我在分析TitleBar代码后分离出来的:
参看上图,有这么几个文件是很关键的:
AjaxManager.cs主要提供Ajax应用所需要的功能枚举等等,此文件包含三个类见图2,其中AjaxManager里面包含了几个静态方法,这个是关键。
Global.js这个文件主要包含两个重要的脚本函数来处理客户端的Ajax提交,另外还有一些很有用的js函数。
当然,这两个文件只是基础,要使用ajax必需要相应的页面的配合和对js编程的熟悉,现在让我们来了解一下Ajax的机理,Ajax说白了就是使用JS脚本和XMLHttp技术在后台提交数据到服务器,经过服务器相应方法的处理后把数据返回给客户端,由于数据在后台提交的,用户只看到数据的变化却看不到页面的刷新,功能酷酷的,实现起来也不难,这就开始了:
首先让我们看看在CS2中的Ajax的实现原理
按照上图的步骤来分析,并且以一个实例来了解它,这个示例很简单:在客户端提供两个用户输入的数字,提交到服务器进行计算,并把计算结果返回给客户端。
(在看下面的解说之前最好先下载本篇文章的示例程序结合着看。)
首先,我们建立一个aspx的页面,这里命名为Default.aspx,第一步要在提交请求的时候让页面注册Ajax_CallBack脚本(此脚本即是客户端和服务器通讯的关键),这里我们的后台代码很简单你可以在Page_Load方法里写成这样if(!this.IsPostBack) AjaxManager.Register(this,"Counter");可以看出调用了AjaxManager的静态方法,此方法的作用就是分析传进去的参数(这里的参数为this也就是页面的引用),生成客户端Ajax_CallBack脚本。在生成脚本之前自动去找在此页面有多少个带了AjaxMethod属性的方法,一个服务器方法对应一个客户端Ajax_CallBack方法,生成了客户端脚本后我们只需要在客户端写上少量的调用方法即可与服务器通讯了。在这里注册到客户端的脚本为:
"CounterEnter": function(first, second, clientCallBack) {
return Ajax_CallBack('ASP.Default_aspx', null, 'CounterEnter', [first,second], clientCallBack, false, false, false, false,'/cs_ajax/Default.aspx?Ajax_CallBack=true');
}
有了这个脚本,客户端的工作并没有做完,因为还没有让客户端控件响应操作,这里就需要我们手动写一些操作代码了,不过一般不会太复杂,比如这里我们只需要在页面加多这样的js语句快:
function CounterEnter(){
$('txtResult').value= iteSettings.CounterEnter($('txtFirst').value,$('txtSecond').value,null).value; }
在客户端的按钮onclick事件上调用此函数即可,这段客户端的代码也许有人会迷糊,其中“$”函数是Glogal.js文件中的,等同于document.getElementById,iteSettings.CounterEnter则是调用前面注册到客户端的脚本,那么这段代码的意思是传递第一个和第二个输入筐的值到服务器器上的CounterEnter方法,并把返回的结果付给txtResult的文本框,前面已经提到过服务器上的CounterEnter方法此方法带有AjaxMethod属性,程序会自动找到此方法并执行的。至于怎么找到的,在后面详解,服务器方法在执行完此方法后返回结果,返回的值通过AjaxManager的相关方法以js脚本的形式Response给客户端(形式如:{value:’aaa’,error:null}),在得到这个返回对象后我们只需要调用其中的Value属性即可获取返回的值。
那么服务器怎样去分辨是Get请求还是PostBack或者AjaxPostBack呢,只有分清楚了这个才能在该执行的时候执行相应的方法,好的,其实在这里通过Get方法和AjaxPost的数据asp.net都不认为是回发给服务器,这样一来我们在PageLoad代码里的写的if(!this.IsPostBack)对两者请求都视为等效,那么通过后台的AjaxPost请求的页面同样会去执行AjaxManager.Register方法,那么我们看看此方法除了注册客户端JS代码还做了写什么呢。我们注意到在最后一段代码里有一句:control.PreRender += new EventHandler(OnPreRender);这句话就是在当即将Response给客户端页面的时候执行OnPreRender方法先,此方法的作用即是判断客户端提交的参数里是否有Ajax参数,如果有就判断此次Request为Ajax提交,需要执行服务器的相关方法,此处通过一系列的跳转和反射找到了应该调用的方法并执行,返回执行完后的结果给客户端并中止页面继续Responst,这样就完成了一个完整的Ajax调用,当然在AjaxManager里面有很多细节在这里没有详细写出来,如有异议或疑问希望多多指正和探讨,也希望此篇帖子能起到抛砖引玉的作用,给大家一个小小的参考。
上一节用了一个示例说明了Ajax在CS2中的一点简单的应用,这一节里着重探讨一下CS2中的Ajax的高级应用和实现原理,在了解Ajax的实现原理前我觉得有必要先了解一下aspx页面从请求到返回HTML都做了些什么,我想在了解了页面处理机制再来认识Ajax处理原理应该应该会很有帮助的,见下表:
序号 |
阶段 |
页面事件 |
可覆盖的方法 |
1 |
页面初始化 |
Init |
|
2 |
加载视图状态 |
|
LoadViewState |
3 |
处理回发数据 |
|
任意实现 IPostBackDataHandler 接口的控件中的 LoadPostData 方法 |
4 |
加载页面 |
Load |
|
5 |
回发更改通知 |
|
任意实现 IPostBackDataHandler 接口的控件中的 RaisePostDataChangedEvent 方法 |
6 |
处理回发事件 |
由控件定义的任意回发事件 |
任意实现 IPostBackDataHandler 接口的控件中的 RaisePostBackEvent 方法 |
7 |
页面显示前阶段 |
PreRender |
|
8 |
保存视图状态 |
|
SaveViewState |
9 |
显示页面 |
|
Render |
10 |
卸载页面 |
Unload |
|
此表从上到下为处理ASPX页面所经历的过程,现简单解说一下各过程处理的工作:
1. 在页面实例化所有控件之后触发,页面的控件也仅仅是实例化好了,各静态变量也有了初始值,在这个事件里,一般我们可以设置一些事件处理程序等等。
2. 如果是回发那么加载视图状态。
3. 处理回发数据,为控件赋值,这里通过读取回发过来的数据初始化各控件的属性。这样,第一步生成的控件就会有值了。
4. 激发Load事件
5. 处理比如OnChange这样的事件,也就是控件属性和ViewState中的值不同的话,将会激发事件处理程序。
6. 处理页面里的按钮等回发事件。
7. 页面即将展现给用户,也就是即将生成HTML代码前激发的事件,注意这个地方是我们的Ajax关键事件,在这里页面前的初始化和相关事件都处理完了,服务器控件已经具有该有的值了,而Ajax应用只会返回给客户段一小部分HTML或字符串,作为客户端调用AjaxPostBack的返回值,显然此处不下手还等待何时呢,在这里,筛选出我们想要返回给客户端的对象并且只呈现这些对象即可,而执行完这些对象的Render方法之后即可停止Response数据了。
8. 保存视图状态,在aspx中视图状态保存在客户端HTML的隐藏字段__VIEWSTATE中。
9. 依次执行各个控件的Render方法,输出需要呈现的HTML标签,Render方法是asp.net中很关键的方法,每个服务器控件都有相应的Render方法,它的目的就是把自己生成HTML格式的字符串,此字符串最终返回给客户端的浏览器。
10. 最后一步触发卸载页面事件,我们可以在此处理资源回收等操作。
了解了此原理后,我想要理解Ajax的运行原理应该不是大问题了,让我们再了解一下Ajax都有哪些操作,我想不外乎两种方式,一种是不返回值或返回简单字符串形式,客户端通过这点标识操作一些功能,另外一种方式是返回HTML格式内容,比如在网页里的某个区块的动态加载内容,这两种方式大同小意,我们的Ajax在后台都是相似的处理,我想要说明处理过程还是结合示例来说比较通俗,现在就拿在CS2中经常用到的AjaxPager这个控件来说,这个控件在CS2中算是比较高级一点的应用了,与我们的第三方Ajax控件比较类似,使用方法既是在AjaxPager控件里嵌入相应的服务器控件(比如Repeater控件)以后就可以使里面的内容无刷新分页显示数据了,表面上看来是很高深很酷的功能,其实处理过程并不是想象中的那般复杂。
<CS:AjaxPager runat="Server" id="PostsPager" ShowFirstLastLinks="false">
<asp:Repeater id="Posts" runat="Server" >
<ItemTemplate>
<div class="CommonSidebarContentItem">
<asp:HyperLink Runat="server" id="TitleLink" />
</div>
</ItemTemplate>
</asp:Repeater>
</CS:AjaxPager>
大致分析一下此控件的工作流程:此控件为分页控件,在页面加载的时候赋上分页属性,计算页数,并生成相应的Ajax分页连接,当用户点击分页连接的时候,页面在后台进行ajax提交,通过服务器处理后返回HTML形式的提交结果,并通过js脚本呈现到浏览器上。
AjaxPager
public class AjaxPager : Label, IPagedControl
{
Member variables and constructor#region Member variables and constructor
protected CSContext csContext = CSContext.Current;
HyperLink previousLink;
HyperLink nextLink;
HyperLink firstLink;
HyperLink lastLink;
HyperLink[] pagingHyperLinks;
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender (e);
// Add Page buttons
//
AddPageLinks();
// Add Previous Next child controls
//
AddPreviousNextLinks();
// Add First Last child controls
//
AddFirstLastLinks();
}
#endregion
Render functions#region Render functions
protected override void Render(HtmlTextWriter writer)
{
Render(writer, true);
}
protected virtual void Render(HtmlTextWriter writer, bool renderContainer)
{
if (renderContainer)
writer.Write("<div id=\"" + this.ClientID + "_Content\">");
this.RenderChildren(writer);
if (Visible)
{
int totalPages = CalculateTotalPages();
// Do we have data?
//
if (totalPages <= 1)
return;
AddAttributesToRender(writer);
// Render the paging buttons
//
writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass, false);
//writer.RenderBeginTag(HtmlTextWriterTag.Span);
// Render the first button
//
if (this.ShowFirstLastLinks)
RenderFirst(writer);
// Render the previous button
//
RenderPrevious(writer);
// Render the page button(s)
//
RenderPagingButtons(writer);
// Render the next button
//
RenderNext(writer);
// Render the last button
//
if (this.ShowFirstLastLinks)
RenderLast(writer);
//writer.RenderEndTag();
}
if (renderContainer)
writer.Write("</div>");
}
void RenderFirst (HtmlTextWriter writer)
{
int totalPages = CalculateTotalPages();
if ((PageIndex >= 3) && (totalPages > 5))
{
firstLink.RenderControl(writer);
LiteralControl l = new LiteralControl(" ");
l.RenderControl(writer);
}
}
void RenderLast (HtmlTextWriter writer)
{
int totalPages = CalculateTotalPages();
if (((PageIndex + 3) < totalPages) && (totalPages > 5))
{
LiteralControl l = new LiteralControl(" ");
l.RenderControl(writer);
lastLink.RenderControl(writer);
}
}
void RenderPrevious (HtmlTextWriter writer)
{
Literal l;
if (HasPrevious)
{
previousLink.RenderControl(writer);
l = new Literal();
l.Text = " ";
l.RenderControl(writer);
}
}
void RenderNext(HtmlTextWriter writer)
{
Literal l;
if (HasNext)
{
l = new Literal();
l.Text = " ";
l.RenderControl(writer);
nextLink.RenderControl(writer);
}
}
void RenderButtonRange(int start, int end, HtmlTextWriter writer)
{
for (int i = start; i < end; i++)
{
// Are we working with the selected index?
//
if (PageIndex == i)
{
// Render different output
//
Literal l = new Literal();
//l.Text = "<span class=\"currentPage\">[" + (i + 1).ToString() + "]</span>";
l.Text = (i + 1).ToString();
l.RenderControl(writer);
}
else
{
pagingHyperLinks[i].RenderControl(writer);
}
if ( i < (end - 1) )
writer.Write(" ");
}
}
void RenderPagingButtons(HtmlTextWriter writer)
{
int totalPages;
// Get the total pages available
//
totalPages = CalculateTotalPages();
// If we have <= 5 pages display all the pages and exit
//
if ( totalPages <= 5)
{
RenderButtonRange(0, totalPages, writer);
}
else
{
int lowerBound = (PageIndex - 2);
int upperBound = (PageIndex + 3);
if (lowerBound <= 0)
lowerBound = 0;
if (PageIndex == 0)
RenderButtonRange(0, 5, writer);
else if (PageIndex == 1)
RenderButtonRange(0, (PageIndex + 4), writer);
else if (PageIndex < (totalPages - 2))
RenderButtonRange(lowerBound, (PageIndex + 3), writer);
else if (PageIndex == (totalPages - 2))
RenderButtonRange((totalPages - 5), (PageIndex + 2), writer);
else if (PageIndex == (totalPages - 1))
RenderButtonRange((totalPages - 5), (PageIndex + 1), writer);
}
}
#endregion
ControlTree functions#region ControlTree functions
void AddPageLinks()
{
// First add the lower page buttons
//
pagingHyperLinks = new HyperLink[CalculateTotalPages()];
// Create the buttons and add them to
// the Controls collection
//
for (int i = 0; i < pagingHyperLinks.Length; i++)
{
pagingHyperLinks[i] = new HyperLink();
pagingHyperLinks[i].EnableViewState = false;
pagingHyperLinks[i].Text = (i + 1).ToString();
pagingHyperLinks[i].ID = (i + 1).ToString();
pagingHyperLinks[i].NavigateUrl = "#";
pagingHyperLinks[i].Attributes.Add("onclick", CreatePagerJavaScript((i + 1).ToString()));
}
}
void AddFirstLastLinks()
{
int start = 1;
firstLink = new HyperLink();
firstLink.ID = "First";
firstLink.Text = ResourceManager.GetString("Utility_Pager_firstButton");
firstLink.NavigateUrl = "#";
firstLink.Attributes.Add("onclick", CreatePagerJavaScript(start.ToString()));
int last = CalculateTotalPages();
lastLink = new HyperLink();
lastLink.ID = "Last";
lastLink.Text = ResourceManager.GetString("Utility_Pager_lastButton");
lastLink.NavigateUrl = "#";
lastLink.Attributes.Add("onclick", CreatePagerJavaScript(last.ToString()));
}
void AddPreviousNextLinks()
{
int previous;
if (this.PageIndex < 2)
previous = 1;
else
previous = this.PageIndex;
previousLink = new HyperLink();
previousLink.ID = "Prev";
previousLink.Text = ResourceManager.GetString("Utility_Pager_previousButton");
previousLink.NavigateUrl = "#";
previousLink.Attributes.Add("onclick", CreatePagerJavaScript(previous.ToString()));
int next = this.PageIndex + 2;
nextLink = new HyperLink();
nextLink.ID = "Next";
nextLink.Text = ResourceManager.GetString("Utility_Pager_nextButton");
nextLink.NavigateUrl = "#";
nextLink.Attributes.Add("onclick", CreatePagerJavaScript(next.ToString()));
}
#endregion
Private Properties#region Private Properties
private bool HasPrevious
{
get
{
if (PageIndex > 0)
return true;
return false;
}
}
private bool HasNext
{
get
{
if (PageIndex + 1 < CalculateTotalPages() )
return true;
return false;
}
}
#endregion
Helper methods and Public Properties#region Helper methods and Public Properties
string url = "AjaxPager.GetPage('{0}', {1}, new Function('result', 'if (result.error) {{ alert(result.error); }} else {{ document.getElementById(\\'{0}_Content\\').innerHTML = result.value; }}')); return false;";
protected virtual string CreatePagerJavaScript(string pageIndex)
{
return string.Format(url, this.ClientID, pageIndex);
}
// *********************************************************************
// CalculateTotalPages
//
/**//// <summary>
/// Static that caculates the total pages available.
/// </summary>
///
// ********************************************************************/
public virtual int CalculateTotalPages()
{
int totalPagesAvailable;
if (TotalRecords == 0)
return 0;
// First calculate the division
//
totalPagesAvailable = TotalRecords / PageSize;
// Now do a mod for any remainder
//
if ((TotalRecords % PageSize) > 0)
totalPagesAvailable++;
return totalPagesAvailable;
}
int _pageIndex = 0;
public virtual int PageIndex
{
get
{
// Give first try to the ViewState if it was a postback
if (Page.IsPostBack && ViewState["PageIndex"] != null)
{
_pageIndex = (int) ViewState["PageIndex"];
}
else
{
if (csContext.QueryString["pageindex"] != null)
_pageIndex = int.Parse(csContext.QueryString["pageindex"]) - 1;
}
if (_pageIndex < 0)
return 0;
else
return _pageIndex;
}
set
{
ViewState["PageIndex"] = value;
_pageIndex = value;
}
}
public virtual int PageSize
{
get
{
int pageSize = Convert.ToInt32(ViewState["PageSize"]);
if (pageSize == 0)
return 10;
return pageSize;
}
set
{
ViewState["PageSize"] = value;
}
}
public virtual bool ShowFirstLastLinks
{
get
{
object state = ViewState["ShowFirstLastLinks"];
if (state == null)
return true;
else
return (bool) state;
}
set
{
ViewState["ShowFirstLastLinks"] = value;
}
}
private bool _causeValidation = true;
public bool CausesValidation
{
get {return _causeValidation;}
set {_causeValidation = value;}
}
public int TotalRecords
{
get
{
return Convert.ToInt32(ViewState["TotalRecords"]);
}
set
{
ViewState["TotalRecords"] = value;
}
}
/**//// <summary>
/// Making sure to strip the existing pager value from the querystring before appending a new one
/// </summary>
/// <param name="RawURL">Usually CSContext.Current.RawUrl</param>
/// <param name="QueryName">Usually "pageindex"</param>
/// <returns></returns>
public string UrlCleaner(string RawURL, string QueryName)
{
// Configure the Url
if(RawURL.IndexOf("?") != -1)
{
bool hasPart = false;
string queryString = RawURL.Substring( RawURL.IndexOf("?") + 1 );
string[] parts = queryString.Split('&');
for(int i = 0 ; i < parts.Length ; i++)
{
if(parts[i].StartsWith(QueryName))
{
parts[i] = QueryName + "={0}";
hasPart = true;
}
}
if(hasPart == true)
RawURL = RawURL.Replace(queryString, string.Join("&", parts));
else
RawURL = RawURL.Replace(queryString, string.Join("&", parts) + "&" + QueryName + "={0}");
}
else
RawURL = RawURL + "?" + QueryName + "={0}";
return RawURL;
}
#endregion
public override bool Visible
{
get
{
object state = this.ViewState["Visible"];
if (state == null)
return true;
else
return (bool) state;
}
set
{
this.ViewState["Visible"] = value;
}
}
AJAX#region AJAX
protected override void OnInit(EventArgs e)
{
base.OnInit (e);
AjaxManager.Register(this, "AjaxPager");
}
[AjaxMethod(IncludeControlValuesWithCallBack=true)]
public virtual string GetPage(int page)
{
StringWriter stringWriter = new StringWriter();
HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter);
AddPageLinks();
AddPreviousNextLinks();
if (this.ShowFirstLastLinks)
AddFirstLastLinks();
this.Render(htmlWriter, false);
return stringWriter.ToString();
}
#endregion
}
关键一点是让程序注册PreRender处理事件,我们知道,在此事件控件还没有生成HTML代码,也就是Response对象里还没有具体的HTML代码,所有的控件还没有执行Reader方法,这里我们就可以选择想执行Reader方法的控件而忽略其他控件,在此我们只需要执行包含在AjaxPager控件内的控件(也就是AjaxPager的子控件)的Reader方法, 这样就可以保证Response给客户端的是纯净的,只在AjaxPager控件内部的控件的HTML实现。
让我看看此控件重写的OnInit方法,此方法里的AjaxManager.Register(this, "AjaxPager");这一句在前面一章已经介绍过,为注册Ajax的回发脚本,当然它还有另外一个作用那就是注册PreRender事件,当触发PreReader事件 到时候,AjaxManager会去找控件里具有AjaxMethod属性的方法来执行,让我们看看在AjaxPager里的AjaxMethod都干了些什么:
[AjaxMethod(IncludeControlValuesWithCallBack=true)]
public virtual string GetPage(int page)
{
StringWriter stringWriter = new StringWriter();
HtmlTextWriter htmlWriter = new HtmlTextWriter(stringWriter);
AddPageLinks();
AddPreviousNextLinks();
if (this.ShowFirstLastLinks)
AddFirstLastLinks();
this.Render(htmlWriter, false);
return stringWriter.ToString();
}
可以看出,倒数第二句就是Reader方法,输出的HTML保存到HtmlTextWriter中,最后一句即是把控件的HTML呈现字符串对象返回给调用者(这里指的是AjaxManager),AjaxManager获得值后按照指定的格式Response到客户端,并中止其他的Response,客户端通过调用document.getElementById(‘’).innerHTML = result.value来向浏览器呈现指定区域的数据。