在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开篇:经历了上一篇《aspx与服务器控件探秘》后,我们了解了aspx和服务器控件背后的故事。这篇我们开始走进WebForm状态保持的一大法宝—ViewState,对其刨根究底一下。然后,再对曾经很流行的ASP.Net AJAX方案中的利器—UpdatePanel这个神奇的区域一探究竟。 1.1 从Http的无状态说起Http是一个无状态协议,同一个会话的连续两个请求互相不了解,它们由最新实例化的环境进行解析,除了应用本身可能已经存储在全局对象中的所有信息外,该环境不保存与会话有关的任何信息。另外,因为,浏览器和服务器之间是通过Socket进行通信,Http请求通常请求完毕就会关闭Socket连接,因此Http协议不会保持连接。如果保持连接会降低客户端并发处理请求数,不保持连接又会降低处理速度(建立连接所需的时间会长一点);
基于Http协议的无状态特性,我们在ASP.Net的开发中也会经常碰到这种情况:用户上一次提交的东西,下次再提交时服务器就不记得了。很多时候,我们感到很不解?后来,我们发现原来每一次的请求服务器都开启了不同的线程来处理,也就是说每次都会new一个XXX.aspx.cs中的类对象实例来进行处理(上一次new出来为我们处理的page对象也许早就被服务器销毁了)。比如,我们在xxx.aspx.cs代码中写入了一个int类型的number成员(初始为0),每次请求我们都想让这个number自增一下,然后重新返回给浏览器。但就是这么一个简单的梦想,我们却无法轻易的实现。 那么,到底怎么来破呢?大神们已经为我们想好了策略,我们可以使用隐藏域字段、Cookie、Session等来保存状态。而伟大的Microsoft还在ASP.Net中帮我们封装了ViewState,以至于我们在WebForm中进行PostBack操作时,都感觉不到服务器是无状态的。 1.2 青春四处绽放—无处不在的ViewState(1)类似于Dictionary的一种数据结构 如果你曾经使用过Dictionary或者Session的话,那么你应该了解这种Key/Value的数据结构。这里并没有什么高深的理论,ViewState通过String类型的数据作为索引。ViewState对应项中的值可以存储任何类型的值(参数是Object类型),实施上任何类型的值存储到ViewState中都会被装箱为Object类型。 例如,这里我们可以改写上面那个按钮事件中的代码: 1 protected void btnGetNumber_Click(object sender, EventArgs e) 2 { 3 //number++; 4 //this.lblNumber.Text = number.ToString(); 5 6 object age = this.ViewState["age"]; 7 if (age == null) 8 { 9 age = 1; 10 } 11 else 12 { 13 age = Convert.ToInt32(age) + 1; 14 } 15 this.ViewState["age"] = age; 16 this.lblNumber.Text = age.ToString(); 17 } 这里,我们借助ViewState存储了age的状态值,第一次来我给你返回1,后面再来我就加1再返回给你。于是,在上一节我们所提到的那个问题(无法记住上次的number值,每次都返回1)就解决了。
我们都知道,Dictionary和Session都是存储在服务器端的。那么,我们不禁要问,既然我们在服务器端给ViewState增加了一个Key/Value对,并返回给浏览器端,ViewState又是存储在什么位置的呢? (2)大隐隐于市的“页面级”隐藏字段 跟Session和Dictionary的存储位置不同,ViewState的作用域是页面,也就是说ViewState是存储在浏览器的页面之中的(这里相比Session等,耗费的服务器资源较少,也算是ViewState的优点之一吧),当你关闭某个aspx文件后,那么属于这个aspx的ViewState也就不存在了。或许,这么说来,我们还不是很了解,现在我们来实地看看。 ①首先,如果页面上有一个runat="server"的form,当用户请求这个页面时,服务器会自动添加一个_ViewState的隐藏域返回给浏览器。但是,我们发现这个ViewState的value看起来像一串乱码?这是为什么呢?这是因为服务器在向浏览器返回html之前,对ViewState中的内容进行了Base64的加密编码; ②其次,当用户点击页面中的某个按钮提交表单时,浏览器会将这个_VIEWSTATE的隐藏域也一起提交到服务端;服务器端在解析请求时,会将浏览器提交过来的ViewState进行反序列化后填充到ViewState属性中(比如下图中,我们可以通过一个软件将_VIEWSTATE解码得到一个如下图所示的树形结构);再根据业务处理需要,从这个属性中根据索引找到具体的Value值并对其进行操作;操作完成后,再将ViewState进行Base64编码再次返回给浏览器端; ③因此,我们可以得出一个结论:VIEWSTATE适用于同一个页面在不关闭的情况下多次与服务器交互(PostBack)。这里我们也可以通过下图来温习一下ViewState的流程,ViewState存放着“事故现场”,下次可以方便地“还原现场”,将无状态的Http模拟成了有状态的,也让广大的初学者了解不到无状态的这个特性。 1.3 喜欢就会放肆—又爱又恨的ViewState!事实上,除了我们手动在服务器端向ViewState属性中添加的K/V对数据,我们在aspx.cs代码中为某些服务器控件设置的值(例如:为Repeater设置DataSource中存入的数据集、为Label所设置的Text内容等,但不包括:TextBox、CheckBox、CheckboxList、RadioButtonList)都存入了ViewState中。这样做的话,我们下次再向服务器提交请求时,现有表单中所有的服务器控件状态都会记录在ViewState中提交到服务器,在服务器端可以方便地对这些服务器控件进行有状态的操作并返回,这无疑是让我们欢喜的,因为方便了我们的开发过程,提高了我们的开发效率; 但有人说:“喜欢就会放肆”,ViewState让人又爱又恨啊。例如,在我们使用Repeater的过程中,WebForm会自动将DataSource(数据源,你可以理解为一个集合)存储到ViewState中并返回给浏览器。可以参考下面的例子来实地理解一下: ①含有Repeater的aspx页面: 1 <form id="form1" runat="server"> 2 <div align="center"> 3 <table class="test"> 4 <tr class="first"> 5 <td> 6 ID 7 </td> 8 <td> 9 产品名称 10 </td> 11 <td> 12 产品描述 13 </td> 14 <td> 15 删除 16 </td> 17 </tr> 18 <asp:Repeater ID="repeaterProducts" runat="server"> 19 <ItemTemplate> 20 <tr> 21 <td> 22 <%#Eval("Id") %> 23 </td> 24 <td> 25 <%#Eval("Name") %> 26 </td> 27 <td> 28 <%#Eval("Msg") %> 29 </td> 30 <td> 31 <a href='Product.ashx?Action=Delete&Id=<%#Eval("Id") %>'>删除</a> 32 </td> 33 </tr> 34 </ItemTemplate> 35 </asp:Repeater> 36 </table> 37 </div> 38 </form> ②后台代码模拟从数据库中取得数据集合并绑定到Repeater中: 1 protected void Page_Load(object sender, EventArgs e) 2 { 3 if (!IsPostBack) 4 { 5 this.repeaterProducts.DataSource = this.GetProductList(); 6 this.repeaterProducts.DataBind(); 7 } 8 } 9 10 private IList<Product> GetProductList() 11 { 12 IList<Product> productList = new List<Product>(); 13 productList.Add(new Product() { Id = 1, Name = "康师傅方便面", Msg = "就是这个味儿!" }); 14 productList.Add(new Product() { Id = 2, Name = "统一方便面", Msg = "还是那个味儿!" }); 15 productList.Add(new Product() { Id = 3, Name = "白象方便面", Msg = "大骨浓汤啊!" }); 16 productList.Add(new Product() { Id = 4, Name = "日本方便面", Msg = "不只是爱情动作片!" }); 17 productList.Add(new Product() { Id = 5, Name = " 台@@湾 方便面", Msg = "马英九夸我好吃!" }); 18 19 return productList; 20 } 编译生成后,通过查看此页面的html代码,可以明显看到一长串的_VIEWSTATE隐藏域。将此_VIEWSTATE复制到ViewStateDecoder中进行反编码,可以发现它确实存储了Repeater中的数据集合。这里我们不禁要问:展示数据既然已经渲染成了html,为何还要存储在ViewState隐藏域中?如果我们的数据集合是一百行、一千行数据的话,那ViewState隐藏域岂不很大(100k?200k?)?但不幸的是,这是ViewState的设计机制,要想依靠它来保持状态,它就会将服务器控件的状态包括数据集合都存储到其中,在浏览器和服务器之间来回传递保持状态。 这里就涉及到网站的性能问题的探讨了:由于ViewState存储在页本身,因此如果存储较大的值,用户请求显示页面的速度会减慢(这对于互联网系统来说,就是一个噩梦。你会选择一个1秒内响应的网站浏览还是5秒内响应的网站?)。又因为ViewState会随同Form表单一同回传给服务器,如果ViewState很大的话,Http报文也会很大,网站流量消耗也会增大。 那么,有没有一种方法可以让ViewState克制一下呢?别急,请看下面的介绍。 1.4 但爱就是克制—禁用还是不禁用ViewState?刚刚说到,因为ViewState会一定程度上影响性能,所以我们可以在不需要的时候禁用 ViewState。默认情况下 ViewState 将被启用,并且是由每个控件(而非页面开发人员)来决定存储在 ViewState 中的内容。有时,这一信息对应用程序并没有什么用处(例如上面提到的Repeater的数据集合,已经渲染生成了html显示,还存储了一份副本在ViewState里边)。尽管也没什么害处,但却会明显增加发送到浏览器的页面的大小。因此如果不需要用ViewState,最好还是将它关闭,特别是当 ViewState 很大的时候。当然,ViewState帮我们实现了某些服务器控件状态保持,因此在非必需的情况下,还是可以适度使用的,特别是在开发企业内部信息系统的场景。 那么,怎样来禁用ViewState呢?禁用ViewState又有什么策略呢?下面我们一一来探讨。 ①页面级禁用ViewState:在aspx的首部的Page指令集中添加EnableViewState="false",该页面中所有控件的状态都不会存入ViewState的,页面一下就会清爽许多; <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="RepeaterViewState.aspx.cs" Inherits="WebFormDemo.RepeaterViewState" EnableViewState="false" %> 禁用后,再次查看生成的html代码,我们会发现:咦,_VIEWSTATE还在那儿,但是明显比先前的体积小了不少! 再将这个瘦身后的_VIEWSTATE复制到ViewStateDecoder中进行反编码查看,我们会发现,只保存了一个最基本的信息,Repeater的那些数据集合没有存入进去了。
②控件级禁用ViewState:在某些场景中,我们只希望禁用某个控件(例如Repater)的ViewState,其他控件仍然通过ViewState保持状态。这时,我们可以给指定的控件设置一个属性EnableViewState="false"即可; <asp:Repeater ID="repeaterProducts" runat="server" EnableViewState="false"> </asp:Repeater> ③全局级禁用ViewState:园子里的大神老赵(Jeffrey Zhao)曾经说过,“我如果新建一个WebForm项目,做的第一件事情就是去Web.config中将enableViewState设置为false从而将ViewState全局关闭”。那么,我们如果希望将网站中所有页面的ViewState都禁用,总不可能去一个一个页面得修改Page指令吧?ASP.Net为我们提供了一个配置,我们只需要在Web.config的system.web中增加一句配置即可: <pages enableViewState="false" />
④真正的禁用ViewState:刚刚我们的三种方法实践后,在页面还是出现_VIEWSTATE的隐藏域,尽管它保留了最基本的信息。那么,我们可能会问?怎样才能彻底地真正地禁用ViewState,根本就别给我生成_VIEWSTATE的隐藏域。答案是有的,将<form runat="server"/>的runat="server"去掉,就不会出现了,但那样又会偏离WebForm的开发模式,大部分的服务器控件都无法正常使用,开发效率又会有所损失。 综上所述,在实际开发中应该权衡利弊,特殊情况特殊分析(到底这个场景该不该禁用ViewState),选择是否禁用ViewState,采用何种方式禁用ViewState。对于ViewState的探秘本篇就到此为止,由于我本人理解的也不是很深刻,所以希望各位园友如果有理解,可以回复出来大家探讨共同进步。 二、飞来的利器—UpdatePanel探秘2.1 从一个简单四则运算计算器说起假如有以下一个场景,我们要做一个简单的四则计算器。aspx页面代码和后端逻辑代码如下: (1)aspx页面代码 <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>AJAX计算器</title> </head> <body> <form id="form1" runat="server"> <div align="center"> <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox> <asp:DropDownList ID="ddlFunc" runat="server"> <asp:ListItem Value="0">+</asp:ListItem> <asp:ListItem Value="1">-</asp:ListItem> <asp:ListItem Value="2">*</asp:ListItem> <asp:ListItem Value="3">/</asp:ListItem> </asp:DropDownList> <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox> <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" onclick="btnGetResult_Click" /> <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label> </div> </form> </body> </html> (2)后置逻辑代码 public partial class AjaxCalculator : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnGetResult_Click(object sender, EventArgs e)
{
int number1 = Convert.ToInt32(this.txtNumber1.Text);
int number2 = Convert.ToInt32(this.txtNumber2.Text);
int result = 0;
switch(this.ddlFunc.SelectedValue)
{
case "0":
result = number1 + number2;
break;
case "1":
result = number1 - number2;
break;
case "2":
result = number1 * number2;
break;
case "3":
if(number2 == 0)
{
throw new DivideByZeroException("除数不能为0!");
}
result = number1 / number2;
break;
}
this.lblResult.Text = result.ToString();
}
}
生成后运行该页面,可以达到以下的效果。我们输入两个数字后,选择是加法、减法、还是乘除法后,点击=按钮,即可刷新页面显示运算结果。 在WebForm中,每一次点击runat="server"的按钮都会将调用form.submit将请求提交到服务器,服务器会返回新的页面html进行页面重绘。这是一个整页的刷新操作,不符合AJAX的风格需求。因此,我们想要将其改为AJAX版本的,除了使用基本的XMLHttpRequest外,我们还可以使用基于JQuery的AJAX方案,这些都是轻量级的原生态的AJAX技术方案。但我们伟大的微软(我哭啊,真是为我们考虑啊,连AJAX方案都为我们解决了,而且还提供了AJAX控件供我们使用,我们拖控件的习惯可以用到AJAX方案上了!!!)还为我们提供了一套叫做ASP.Net AJAX的技术方案,通过这套方案,我们可以在ASP.Net很容易地实现AJAX效果,甚至都不需要我们懂JavaScript。因此,也就出现了前些年,很多WebForm开发者陆续使用ASP.Net AJAX Extension进行AJAX开发,纷纷表示:AJAX如此简单,我等岂能不会?但是,虽然它简单易行,由于其性能问题一直被人诟病,而我们这些菜鸟也未能了解其性能问题的原因,本着知其然也知其所以然的目标,现在我们来使用它并剖析它一下。 2.2 天上掉下个林妹妹—使用UpdatePanel控件不得不说,UpdatePanel真的是天上掉下的林妹妹,一个神奇的控件!有了它,我们可以将页面中需要进行局部刷新的内容放到其ContentTemplate中,一个需要整页刷新的操作便可以成为局部刷新。现在,我们首先来使用其改造刚刚的简单四则计算器页面。 (1)加入UpdatePanel,并将计算器html内容拖入ContentTemplate中 <form id="form1" runat="server"> <div align="center"> <asp:ScriptManager ID="scriptManager" runat="server"> </asp:ScriptManager> <asp:UpdatePanel ID="updatePanel" runat="server"> <ContentTemplate> <asp:TextBox ID="txtNumber1" runat="server"></asp:TextBox> <asp:DropDownList ID="ddlFunc" runat="server"> <asp:ListItem Value="0">+</asp:ListItem> <asp:ListItem Value="1">-</asp:ListItem> <asp:ListItem Value="2">*</asp:ListItem> <asp:ListItem Value="3">/</asp:ListItem> </asp:DropDownList> <asp:TextBox ID="txtNumber2" runat="server"></asp:TextBox> <asp:Button ID="btnGetResult" runat="server" Text="=" Width="50" OnClick="btnGetResult_Click" /> <asp:Label ID="lblResult" runat="server" Text="" Font-Bold="true"></asp:Label> </ContentTemplate> </asp:UpdatePanel> </div> </form> (2)运行该页面,通过开发人员工具查看Http请求 通过查看请求报文,我们了解到此次的请求响应不再是返回整页的html内容,而只是我们放在了UpdatePanel里面的html内容,页面也没有再刷新,于是不禁感叹一句:AJAX,So easy!妈妈再也不用担心我的页面了! 2.3 直到看见XmlHttpRequest才是唯一的答案—UpdatePanel原来如此正当我们沉浸在UpdatePanel为我们提供的神奇的AJAX世界里时,我们不禁对UpdatePanel为我们做了哪些工作产生了兴趣。 (1)首先,我们知道AJAX的核心对象是XmlHttpRequest,那么原生态的AJAX请求的JS方法是如何写的呢? function ajax(url, onsuccess) { var xmlhttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); //创建XMLHTTP对象,考虑兼容性。XHR xmlhttp.open("POST", url, true); //“准备”向服务器的xx.ashx发出Post请求(GET可能会有缓存问题)。这里还没有发出请求 //AJAX是异步的,并不是等到服务器端返回才继续执行 xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4) //readyState == 4 表示服务器返回完成数据了。之前可能会经历2(请求已发送,正在处理中)、3(响应中已有部分数据可用了,但是服务器还没有完成响应的生成) { if (xmlhttp.status == 200) //如果Http状态码为200则是成功 { onsuccess(xmlhttp.responseText); } else { alert("AJAX服务器返回错误!"); } } } //不要以为if (xmlhttp.readyState == 4) {在send之前执行!!!! xmlhttp.send(); //这时才开始发送请求。并不等于服务器端返回。请求发出去了,我不等!去监听onreadystatechange吧! } (2)其次,通过查看运行页面的html,我们可以发现加入UpdatePanel后,我们的html中多了这么几个js引用。 (3)既然我们知道要发AJAX请求,必然会涉及到XmlHttpRequest。那么,我们就在这几个js中取看看是否有涉及到XmlHttpRequest。通过查看,我们找到了这样一个似曾相识的js方法: function Sys$Net$XMLHttpExecutor$executeRequest() { /// <summary loc /> if (arguments.length !== 0) throw Error.parameterCount(); this._webRequest = this.get_webRequest(); if (this._started) { throw Error.invalidOperation(String.format(Sys.Res.cannotCallOnceStarted, 'executeRequest')); } if (this._webRequest === null) { throw Error.invalidOperation(Sys.Res.nullWebRequest); } var body = this._webRequest.get_body(); var headers = this._webRequest.get_headers(); this._xmlHttpRequest = new XMLHttpRequest(); this._xmlHttpRequest.onreadystatechange = this._onReadyStateChange; var verb = this._webRequest.get_httpVerb(); this._xmlHttpRequest.open(verb, this._webRequest.getResolvedUrl(), true ); this._xmlHttpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest"); if (headers) { for (var header in headers) { var val = headers[header]; if (typeof(val) !== "function") this._xmlHttpRequest.setRequestHeader(header, val); } } if (verb.toLowerCase() === "post") { if ((headers === null) || !headers['Content-Type']) { this._xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8'); } if (!body) { body = ""; } } var timeout = this._webRequest.get_timeout(); if (timeout > 0) { this._timer = window.setTimeout(Function.createDelegate(this, this._onTimeout), timeout); } this._xmlHttpRequest.send(body); this._started = true; } 由以上的方法名我们可以猜到,此方法是一个执行AJAX请求的方法。在此方法中,创建了XmlHttpRequest对象,也使用了open方法指明以GET还是POST方法向服务器哪个处理程序发送请求,并且也为该请求指定了请求成功后需要执行的回调函数方法(onreadystatechange),最后调用send方法正式发送请求 由此,我们可以初步分析出一个结论:UpdatePanel本质还是帮我们封装了以XmlHttpRequest为核心的一系列方法帮我们将CodeBehind中的同步事件变为了异步操作,并通过DOM更新指定的HTML内容,使得我们可以方便地实现AJAX效果。 但是,我们也不由发出感叹:本来可以很简单地使用XmlHttpRequest来实现的东西,为什么使用UpdatePanel会引入这么多js,并且为我们返回的东西还是那么多(比如上面的例子,我只需要的数据是一个结果,却给我返回一部分无用的html,还有一系列的hiddenId之类的数据)。在对性能要求较高的应用场合,如果使用UpdatePanel来实现AJAX会增加服务器的负载,并且会消耗掉不必要的网络流量(比如每次请求都会来回都会发送ViewState里的数据,在性能和数据量上都会造成损失)。园子里的浪子曾经在他的博文《远离UpdatePanel带给我的噩梦》里边写到:“UpdatePanel在页面小的时候还是很好用的,而当页面控件数不断上升的时候,UpdatePanel就开始直线下降,我们现在页面有4,5百个控件,每做一次PostBack需要长达15秒钟之长,实在让人无法忍受。” 那么,有木有方式可以替换UpdatePanel呢?其实答案很简单,那就是使用基于XmlHttpRequest的js方法,再加上一定的js回调函数即可。这就要求我们掌握javascript,不能只做拖UpdatePanel控件的程序员。现在基于js的JQuery库也早已为我们封装了XmlHttpRequest,提供了ajax开发的一系列方法供我们调用,相当于UpdatePanel的“重量级”来说,可谓是轻了不少,是一个“轻量级”的AJAX开发方式。通过借助jQuery Ajax+ashx可以方便地在.Net中进行Ajax开发,并且具有不错的性能,这也是我实习所在的企业中经常用到的方式。 三、学习总结本篇主要学习了WebForm中的状态保持法宝—ViewState,以及曾经的ASP.Net AJAX方案的利器—UpdatePanel,虽然一直在说这个不好,那个别用。但是,微软之所以为我们提供了这些东西,肯定有它存在的理由,并不一定都是不好的东西。所谓利器在手,没有一点内功心法的人还是使用不好它,无法发挥出其100%的优势。因此,身为.Net学习者的我们,不能满足于微软为我们所提供的便利,要知其然也知其所以然,做一个上进的程序员,加油吧! 校园招聘的大潮就快来临,希望园子里跟我一样即将毕业的菜鸟们能够好好复习基 |
请发表评论