我们都知道,所谓B/S构架是指browser(浏览器)/server(服务器)构架,相比于C/S构架中服务器端各种各样“服务的对象”,B/S构架中服务器端的“服务的对象”就只有浏览器一种(尽管浏览器之间的差异可能较大),与客户进行交互行为的是浏览器,服务器要做的是响应浏览器在与客户进行交互产生的数据请求,服务器与浏览器的关系就好像生产商与经销商的关系。早期的网页是静态的,这里说的静态不是说客户就不能与浏览器进行交互了,而是说这些网页在服务器端是怎样的,那么当它被传输到客户端就是怎样的,服务端只是简单传输静态文本而已。这种静态网页技术在满足客户需求方面是十分有限的,比如客户因为某个操作需要获取更多的数据,而这些数据通过Javascript是不可能产生的,只能从服务器端获取;如果是静态网页表现的话,那么就必须再做另一个网页,这个网页相比之前的页面只是多了这部分额外的数据表现而已,尼玛,要实现这个实在是太麻烦了,太难维护了。因此,静态网页适用于那种仅仅用于展示信息,比如公司介绍什么的,一大堆文字加上精美图片。由于静态网页的局限,动态网页技术就应运而生了,一般在服务器存放的动态网页如果直接拿给浏览器解析是不行的,因为其中很多标签是服务器才“理解”的,如ASP.NET中的服务器控件标签,在服务器根据特定代码转化成标准的html标签后,才变成浏览器的“菜”,所谓动态,就体现在服务端的先行解析行为而导致的最终传输给浏览器的html文本与原始文本不一致。
ASP.NET是现在的主流动态网页技术之一,其他的还有jsp、php等等。微软做动态网页有她的优势,因为她又做操作系统,又做浏览器,那干嘛不再做动态网页呢,搞成一条龙多好。ASP.NET的前身是ASP,相比于ASP,ASP.NET在可维护性、可重用性和执行效率上都要提高很多,同时保留了ASP的直接在页面中用<%%>写文本的便携写法。ASP.NET属于.NET大家族的一员,可以享受到.NET中其他成熟技术的支持:集大成开发语言C#用于后台,.NET Framework作为大哥大提供功能丰富而强大的类库罩着,利用ADO.NET操作数据库,尼玛,原来ASP.NET的爹是李刚啊!
一,form标签
在思考ASP.NET的基本实现机制之前,必须先对html中的form标签有充足的了解。Form标签被设计为向服务器传输数据,传输数据是其内部的所有的input 、select、textarea等标签的值集合。Form标签有两个最重要的属性:action与method。action规定当提交表单时,向何处发送表单数据,即网页转向何处;method:表单发送方式,post还是get。下面演示一个简单的利用form标签进行传值同时跳转的示例:
用于传输form表单的页面,因为无需服务器后台方法,因此这里使用静态页面,0_Form_Send.htm,这里只显示body部分:
<form method="post" action="0_Form_Receive.aspx">
这里是发送页面,提交form页面后将转到0_Form_Receive.aspx页面中,并传输form内的input等标签的值。<br />
<input type="text" name="text_test" value="text_value" /><br />
<input type="password" name="password_test" value="password_test" /><br />
<input type="hidden" name="hidden_test" value="hidden_test" /><br />
<select name="select_test">
<option value="select_value_0">select_value_0</option>
<option value="select_value_1" selected="selected">select_value_1</option>
</select><br />
<textarea name="textarea_test" rows="2" cols="50">textarea_test</textarea><br />
<input type="submit" name="submit_test" value="点击我提交" />
</form>
从代码可以看到,表单的传输方式为post,目标页面是0_Form_Receive.aspx,这是一个aspx动态页面,这里设定为动态页面的目的在于需要在页面的后台代码获取form表单传输的数据。Form标签内的可传输值有text_test、password_test、hidden_test、select_test、textarea_test和提交按钮submit_test本身。这里有个地方需要注意,所有的表单值元素都是按name而不是id识别的,如果没有设定name则该元素将被忽略。
使用Httpwatch来监视当点击提交按钮的数据传递情况,结果如下:
下面是用于接收form表单的页面0_Form_Receive.aspx的前台代码,只显示body部分:
<form runat="server" id="form1">
<!--如果仅仅测试获取传值,该form本无需使用,但是因为想使用DropDownList接收select_test的值,而该服务器控件要求使用form标签,因此本页面使用该form标签-->
这里是接收页面,接收到form表单的数据后将值展现出来<br />
<input type="text" id="text_test" runat="server" />
<asp:DropDownList ID="aspddl_test" runat="server">
<asp:ListItem Value="select_value_0" Text="select_value_0" Selected="True"></asp:ListItem>
<asp:ListItem Value="select_value_1" Text="select_value_1"></asp:ListItem>
</asp:DropDownList>
<textarea id="textarea_test" runat="server" rows="2" cols="50">textarea_test</textarea><br />
</form>
0_Form_Receive.aspx的后台代码,只显示关键方法:
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
//页面后台通过Request[Key]的方式获取form标签的传值
text_test.Value = Request.Form["text_test"];
aspddl_test.SelectedValue = Request.Form["select_test"];
textarea_test.Value = Request.Form["textarea_test"];
}
}
0_Form_Receive.aspx的前台代码中的各个标签都加上了runat="server"属性变成服务器控件,用于在后台代码中获取form表单值后给这些标签赋值。后台使用Request[Key]的方式获取form标签的传值,Key对应的就是0_Form_Send.htm内的各个表单值元素的name。该页面显示如下:
二,ASP.NET如何使用Form标签。
通过上个主题,我想读者你应该大致了解了form的作用,而form标签的这种特性就是ASP.NET的基础。先做一个简单的实验,拖一个Button控件到页面中,但是该页面内没有form标签, 实验1代码如下:<body>
<asp:Button ID="Button1" runat="server" Text="Button" />
</body>
运行该页面,发发现页面出现异常,截图如下:
而如果在Button控件外部加上Form,实验2代码修改为:
<body>
<form runat="server">
<asp:Button ID="Button1" runat="server" Text="Button" />
</form>
</body>
则不会出现异常;再将Button控件替换成Label控件,
实验3代码修改为:
<body>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
</body>
同样也运行正常。我们来分析出现这种现象的原因,Button元素包含Click事件,当用户点击按钮后触发Click事件,页面需要做一次Postback(Postback的详细介绍在下一节,这里只说明为提交服务器),而这种Postback操作来提交服务器就需要Form元素来进行,由于实验1中没有任何Form元素,因此页面抛出异常。与之对比,实验2添加了form标签之后,Button的提交行为能够顺利进行。可能读者会奇怪,为什么实验3的页面也能顺利运行,原因就在于Label控件没有对应的事件来PostBack,它的所有事件都是继承于Control基类,像Init 、Load、PreRender等,这些事件都是页面生命周期(页面生命周期将作为独立的一篇文章介绍)中被动调用的,因此这些事件不会引起Postback操作,因此Label控件不需要Form元素也能正常运行。
如果将实验2中form标签的runat="server"属性去掉,运用页面会抛出与实验1相同的错误,这又是为什么?我们先来看看实验2的页面运行被服务器解析后生成的Html:
<body>
<form method="post" action="WebForm1.aspx" id="Form1">
<!—这里是form内的其他标签-->
</form>
</body>
(本测试页面的文件名是WebForm1.aspx)可以看到,<formrunat="server">被解析为<form method="post" action="WebForm1.aspx"id="Form1">,就是由于runat="server"声明导致的;也就是说,声明了runat="server"之后,服务器在解析过程中自动给form标签加上了method="post" action="WebForm1.aspx"属性,还配置了一个id。其中action="WebForm1.aspx"的设置非常重要,通过将action设置为本身,就实现了在页面利用form提交相应的数据到服务器之后,由服务器根据这些数据重新解析(实质上是进行了一次页面周期过程)该页面,完成解析后再返回给客户端,从而实现了该页面的一次重新载入与更新。
综上可知,有些服务器控件(实际上是大部分)可能会触发Postback操作,而如果该服务器控件外部不存在form标签,则无法进行正常提交,服务器在解析aspx文件的过程中发现这种异常则会抛出错误对开发者进行提示;而如果form标签没有runat="server"标志,则服务器在解析过程中不会添加额外的属性,如method="post" action="WebForm1.aspx",这时虽然有了form标签,但没有指明转向页面本身,也就不能实现本页面的一次生命周期,因此当这种服务器控件存在时,ASP.NET就会要求其外部存在runat="server"标识的form标签。当然,像Label这样不可能进行一次Postback操作的服务器控件,就无需Form标签了。
三,Postback的本质
相信在开发ASP.NET项目过程中,大家在后台用的非常多的一个属性就是Page.IsPostBack,这个属性描述的就是本次请求是否为Postback;如果是首次请求而不是由于服务器控件的Postback事件引起的请求,那么该值就是false。这个属性一般用于只有首次请求才需要执行的一些代码,比如给某些服务器控件进行数据绑定、设置皮肤等等,不需要在Postback过程中重复设置,否则只会白白损耗性能。
在页面中的常见用法如下:protected void Page_Load(object sender, EventArgs e)
{
//这里执行无论是否Postback都需要执行的代码
if (!IsPostBack)
{
//这里执行只有首次请求才需要执行的代码
}
//这里执行无论是否Postback都需要执行的代码
}
当然,这里的Page_Load事件可以换成页面生命周期内的任何事件,因为IsPostBack属性的赋值是在页面生命周期的最开始阶段。那么现在考虑所谓的Postback的实质是什么?这里先给出答案,Postback的本质就是form表单的一次提交并且重定位到本页面的一次过程。
为了对上述结论进行说明,用下面比较简单的示例进行测试,前台代码如下:
<body>
<form id="form1" runat="server">
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label><br />
<asp:Button ID="Button1" runat="server" Text="提交" OnClick="Button1_Click" />
</form>
</body>
后台代码如下:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) { Label1.Text = "这是原始文本"; }
}
protected void Button1_Click(object sender, EventArgs e)
{
Label1.Text += "_" + new Random().Next(100).ToString();
}
上述代码实现的功能是:每次点击一次提交按钮,Label1的文本尾部都会添加一个随机数。运行该页面后查看页面的源代码如下:
<body>
<form method="post" action="WebForm2.aspx" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTI2NTY4ODI3MQ9kFgICAw9kFgICAQ8PFgIeBFRleHQFEui/meaYr+WOn+Wni+aWh+acrGRkZDgp+7Qa5o6BdMbUg3AntZBiu2QnVLAKVPn7gK9QyFzA" />
</div>
<div class="aspNetHidden">
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWAgLAt/aAAgKM54rGBoMXUB/Gz/QF84Ztf+O6JzQOs29SU9ddVE8VfJLmGF53" />
</div>
<span id="Label1">这是原始文本</span>
<br />
<input type="submit" name="Button1" value="Button" id="Button1" />
</form>
</body>
可以看到,Label1控件被解析成<span id="Label1">这是原始文本</span>,变成了一个内联的span标签;Button1控件被解析成<input type="submit" name="Button1"value="Button" id="Button1" />,则变成了一个类型为submit的Input标签,也就是说点击它将引起form的提交,而form的action指向的就是本页面。
这里将前台代码做一些小改动,后台代码不变,将Buton1的属性UseSubmitBehavior设置为false,如下所示:
<body>
<form id="form1" runat="server">
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label><br />
<asp:Button ID="Button1" runat="server" Text="提交" OnClick="Button1_Click" UseSubmitBehavior=”false”/>
</form>
</body>
经过这次改动后被解析的页面源代码变成:
<body>
<form method="post" action="WebForm2.aspx" id="form1">
<div class="aspNetHidden">
<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKMTI2NTY4ODI3MQ9kFgICAw9kFgICAQ8PFgIeBFRleHQFEui/meaYr+WOn+Wni+aWh+acrGRkZDgp+7Qa5o6BdMbUg3AntZBiu2QnVLAKVPn7gK9QyFzA" />
</div>
<script type="text/javascript">
//<![CDATA[
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>
<div class="aspNetHidden">
<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="/wEWAgLAt/aAAgKM54rGBoMXUB/Gz/QF84Ztf+O6JzQOs29SU9ddVE8VfJLmGF53" />
</div>
<span id="Label1">这是原始文本</span>
<br />
<input type="button" name="Button1" value="Button" οnclick="javascript:__doPostBack('Button1','')" id="Button1" />
</form>
</body>
可以看到,经过前台这样一个小小的改动,就迫使服务器对原始页面的解析发生了巨大的变化,最显著的就是添加了__doPostBack的javascript方法,然后还有将Button1解析车呢个类型为button的input标签,添加了__EVENTTARGET和__EVENTARGUMENT的类型为hidden的input标签。
实际上,可以将UseSubmitBehavior看成Button是否被解析成submit类型的input标签,默认为true;当将它设置为false,就会转变为button类型的input标签。Submit与button在外形上是一样的,但是功能完全不同,submit作用在于提交form表单,而button则是普通的按钮。因此,当Button服务器控件的UseSubmitBehavior被设置成false的时候,该怎样对其解析才能保证正常地提交form?前面那些Javascript脚本就是为了实现提交form而产生的。先看“var theForm = document.forms['form1'];if (!theForm) {theForm =document.form1;}”这段,声明了一个全局变量theForm就是为了获取form标签的引用;再看__doPostBack方法,function __doPostBack(eventTarget, eventArgument),参数eventTarget,eventArgument分别给__EVENTTARGET和__EVENTARGUMENT的标签赋值,目的很明显,就是在提交form的时候通过这两个hidden类型的input标签将引起本次提交的元素名称和需要传递的参数都一并传递给服务器,最后通过theForm.submit();执行提交。
因此,一次Postback的过程可以概括为:服务器控件被解析后,对它的某些操作可能会导致form的提交,实现的方式可能是通过将控件解析成类型为submit的input标签(仅限于Button类控件,且UseSubmitBehavior为true),也可能是先布置后__doPostBack方法,然后在元素的事件中执行这个方法即可。四,完整分析一次Postback过程
上个小节分析了一次Postback实质上就是一次提交form表单,然后通过将action设置为本页面实现重新载入。那么,在Postback的过程中,浏览器与服务端是怎样协调运作的,它们之间怎样传递数据的?
还是以上一节示例的源代码(UseSubmitBehavior设置为false)。使用Httpwatch来监测一次Postback过程,结果如下:
该截图显示了本次PostBack过程通过form表单传递的参数集合。__EVENTTARGET和__EVENTARGUMENT的作用已经说过,分别是引起本次提交的元素名称和需要传递的参数;__EVENTVALIDATION是验证客户端请求是否合法,防止未经授权的请求,这是为了安全性考虑而额外添加的;__VIEWSTATE的作用则是为了保存视图状态,视图状态是一个非常重要的知识点,我会在一个独立的文章中做详细的介绍,这里先大致可以说明为:服务器控件(包括页面本身)可以通过视图状态将一些属性值存储起来,在Postback的页面生命周期中通过获取存储在__VIEWSTATE标签内的值来生成这些服务器控件。
服务器在接收到这些表单数据后,会执行页面的生命周期过程,所有的服务器控件都会被依次解析成能够被浏览器识别的html标签和Javascript脚本等内容,然后传送给浏览器,这个过程中服务器的返回数据截图如下:
五,runat="server"标志的作用
简单描述的话就是:ASP.NET将有runat=”server”标志的标签当作服务器控件,这些标签会被初始化成各个服务器控件的实例添加到Page的服务器控件树中,参与页面生命周期直至Render成浏览器可识别的html编码。
VSS的Web项目中专门安排了一个文件存放这些服务器控件的声明,如下图所示,[页面名称].aspx.designer.cs就是VSS自动生成的,它与[页面名称].aspx.cs文件以Partial的形式构成了该页面的后台类定义。
页面WebForm1.aspx的前台代码为:
<body>
<asp:Label ID="asplbl_Test" runat="server" Text="Test"></asp:Label>
<input type="file" id="file_test" runat="server" />
</body>
而WebForm1.aspx.designer.cs内自动生成的代码如下:
public partial class WebForm1 {
protected global::System.Web.UI.WebControls.Label asplbl_Test;
protected global::System.Web.UI.HtmlControls.HtmlInputFile file_test;
}
VS自动为添加有runat="server"标志的标签找到对应的服务器控件类,并在designer.cs文件中声明全局变量,这样一来,所有添加有runat="server"标志就能够通过后台代码进行控制。
服务器控件大体上可以分为两大类,一类是HtmlControls(System.Web.UI.HtmlControls命名控件下的大部分类),一类是WebControls(命名空间System.Web.UI.WebControls下的类是ASP.NET自带的,用户可以自定义该类型服务器控件)。
HtmlControls相对来说理解起来比较简单,他们的基类都是HtmlControl类,而HtmlControl类继承Control类。HtmlControls中的控件都对应HTML标签的某一种,也就是说,这些HtmlControl控件是为各个HTML标签量身定做的,目的是为了在给某个HTML标签添加runat=”server”标志后,页面后台就能够为其自动生成相对应的HtmlControl服务器控件,从而在后台进行操控,如Form会转化为HtmlForm控件、Input转化为HtmlInputButton控件、Span转化为HtmlGenericControl控件等等(顺便提一句,那些没有什么“特色”的标签都会转化为HtmlGenericControl控件,像Span、Label、Div等等,一般只会到InnerHTML、Attributes等所有HTML标签共有的属性)。
WebControls则可看成是自定义控件,与HtmlControl不同的是,WebControl并不会仅限于生成某个特定Html标签,为了达到某种“组合效果”,WebControl最终会Render成多个Html标签,这些Html标签“相互配合”达到特定封装的效果。
总结来说,如果在ASPX页面上的标准Html标签加上runat=”server”标志后,ASP.NET就会自动创建对应HtmlControl控件类型的全局变量,从而能够在后台对其进行控制;如果在非标准Html标签中添加runat=”server”标志(一般都会这么做,否则不会被服务器解析而直接返回,这将导致浏览器服务识别),ASP.NET会试图根据Web.Config中关于服务器控件名称的设定找到对应的类型,并创建一个该类型的全局变量。