menu
more_vert
ASP.Net WebForm温故知新学习笔记:二、ViewState与UpdatePanel探秘

作者:dayu

  那么,到底怎么来破呢?大神们已经为我们想好了策略,我们可以使用隐藏域字段、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)就解决了。

PS:ViewState不能存储所有的数据类型,仅支持以下的这几种: String、Integer、Boolean、Array、ArrayList、Hashtable以及一些自定义类型

  我们都知道,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     <formrunat="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的那些数据集合没有存入进去了。

技术分享

PS:为什么禁用ViewState之后,页面源代码中仍然有_VIEWSTATE的隐藏域

这是因为就算禁用了viewstate,aspx页面中还是会有一个服务器控件在那里使用,这就是<form runat="server">。这时,如果你将form去掉runat="server",将其变为普通html标签,那么页面就干净了,从此_VIEWSTATE这个隐藏域彻底消失在你的页面中。

  ②控件级禁用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" />

PS:开发中也可以采用大神老赵的做法,先禁用,再选择性启用,毕竟没有非要ViewState才能干成的事儿!

  ④真正的禁用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>
    <formrunat="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中

技术分享
技术分享
 <formrunat="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学习者的我们,不能满足于微软为我们所提供的便利,要知其然也知其所以然,做一个上进的程序员,加油吧!

ASP.Net WebForm温故知新学习笔记:二、ViewState与UpdatePanel探秘

原文地址:http://www.cnblogs.com/syr320/p/7243267.html