Dino Esposito
Form(表单)是ASP.NET开发中重要的组成部分--没有Form就没有ASP.NET Web编程模型。Form不局限于纯粹的HTML,但是在ASP.NET中会受到一些限制。对于ASP.NET页面,Form可以提交自身,并且ASP.NET模型提供了控件状态管理和postback事件。由于ASP.NET的单一Form模型使得编写ASP.NET应用程序简单又便捷.
ASP.NET中窗体上的限制可能听起来怪异而武断,但实际上跟ASP.NET模型有直接的作用。然而,有一种实际情况是ASP.NET 1.x form 模型所不支持的:在同一个页面拥有多个,互不干涉的form。比如,您不能在某个页面上添加一个搜索文本框并将结果提交到另一个页面.
在msdn杂志2003年5月刊上,我写了一个专栏关于ASP.NET 1.x的Form编程(参阅 Cutting Edge: Form-based Programming in ASP.NET),对于ASP.NET 2.0的介绍里, 关于提交给不同的页面这一主题的地址有一些变动。本文我将讨论ASP.NET 2.0下的Form编程.
窗体呈现
让我们探索一下ASP.NET的窗体世界,去了解窗体(包括控件)是如何实际呈现的。在ASP.NET页面上,<form>标记可以是几种容器控件像<table>, <div>, 或 <body>的子控件; 然而,在大多数页面中,<form>只是简单作为<body>的子节点。如果一个非容器控件(比如TextBox)被放置在form标记之外,将会抛出一个运行时错误(编译时不会对此进行检查)。请看如下代码,节选自TextBox的 AddAttributesToRender 方法:
protected override void AddAttributesToRender(HtmlTextWriter writer) { if (this.Page != null) this.Page.VerifyRenderingInServerForm(this); ... } |
调用页面的VerifyRenderingInServerForm方法将会处理此工作。(当您自己编写自定义服务器控件时应避免这种行为)
HtmlForm类
HtmlForm继承自HtmlContainerControl,使得窗体具有包含子控件的能力。HtmlForm提供了对HTML<form>元素在服务器端的编程访问能力,其properties列表见图1。从表中可以看到ASP.NET 1.x 和 ASP.NET 2.0的变化主要限于几个properties。
一个表单必须拥有唯一的名字,未指定名字时ASP.NET会自动分配一个。可以通过ID或Name属性给Form设置标识,同时设置时以ID属性优先。可是有一点要注意,一些编程接口使用Name属性来兼容xhtml。在xhtml中,elements通过ID标识而不是Name,因此通常来讲,最好以ID属性为准。
表单的父对象是一个具有runat属性的总容器控件。如果这样的控件不存在,页面对象被作为父对象。典型的服务器端form的容器是标记为服务器端对象的<table>或<div>。
图2 列出了HtmlForm类中最常用的到一些方法。这些方法继承于System.Web.UI.Control类。注意FindControl方法只搜索form的直接子控件。内部容器中的子控件和表单子控件的子控件将不能被找到。
多表单管理
通常来讲,投入单表单模型的怀抱而放弃对多表单系统的支持并不算是很大的牺牲。尽管一些页面如果能使用multiple forms将会获得更加致一致和自然的设计--至少对于那些包含有一定逻辑关系的输入控件组的表单 例如,一个页面除了要给用户提供信息,还需要支持一个搜索表单或者登录框表单。
您可以将搜索和登录功能合并到ad hoc类并通过显示信息的同一页面调用,然而这并不是构造代码的最佳方法。
如果您正在将老的代码移植到ASP.NET, 您可能觉得将登录和搜索代码置于另一个专门的页面更容易些。可是您如何才能将那些页面的数据提交到本页呢?
在单表单模型里,页面总是提交自身而且也没有给开发者提供设置回传目标的钩子。对于HTML和ASP编程,表单的Action属性是单一值,在ASP.NET的HtmlForm类中也不暴露此属性。单表单模型跟ASP.NET平台整合的太紧密,您要么采用要么放弃——或者,做为一个附加的选择就是可以用ASP的方式编写代码不使用服务器表单。象下面要讲到的,在ASP.NET 2.0里,可以将数据提交到另一个页面,但是这个特性是通过一些按钮控件的新增功能得以实现的。现在,我们先看看使用HTML的非服务器端表单时有什么棘手问题。
在ASP.NET中,当有多个HtmlForm控件需要呈现时将会抛出异常。页面中的第一个HtmlForm控件被呈现后,会有一个布尔标记被设为true,此标记指示了是否有HtmlForm已被呈现,当另一个HtmlForm试图呈现时,由于此标记已经被设置为true因而引发一个异常。
如果一个Web Form中包含了一个服务器form和任意数量的不含有runat属性的<form>标记不会导致任何错误。没有runat属性,任何标记都成为纯粹单一的HTML而直接呈现(见图3).
此页面包含2个表单,第二个是没有ruan="server"属性的HTML表单,因此被ASP.NET完全忽略。提供给浏览器的html中合法包含了两个<form>元素,它们指向两个不同的action URL。
然而从功能上来讲此代码有一个大的缺陷:不能使用ASP.NET编程模型来检索客户端表单action页面上的提交数据。当编写search.aspx时,对于客户端表单的action页面,不能对页面的控件借助视图状态和提交数据来读取和更新它们的状态。(The apparent statefulness of ASP.NET server controls is obtained by making pages post to themselves)为了知道提交给search.aspx的数据,您必须采取针对ASP模型的直接在回传数据中检索的传统风格:
protected void Page_Load(object sender, EventArgs e) { // Use the Request to retrieve posted data string textToSearch = Request.Form["Keyword"].ToString(); ... // Use standard ASP.NET to populate the page UI KeywordBeingUsed.Text = textToSearch; } |
可以使用 HttpRequest 对象协议规范中的集合(Page.Request等同于HttpContext.Current.Request)来检索回传的数据——对于POST方式时使用Form,GET方式时使用QueryString,或者想要兼容对Form、QueryString、 ServerVariables 和 Cookies的访问时使用Params。HttpRequest 对象会在页面创建前将数据封装,因此,页面的任何事件都可以随意调用Page.Request.对于自提交的ASP.NET页面,不需要使用Request是由于可以借助于一个强类型的编程模型,但是对于以前,可靠的 HttpRequest 对象依然是需要时为您而备的。
还有一件有趣的事要注意,当用户点击Search按钮时,search.aspx被调用,它只接受那些Html 表单上发送回来的数据,不会有视图状态被回传,也没有额外的数据传递。如果必须提交数据到另一个页面,使用传统风格仍然是多数高效性能的明智之选。如随后本专栏所述,ASP.NET 2.0 交叉页提交特性传递了相当大的,类视图状态的数据域。
多 <form> 标记
如果多个服务器端form出现在同一个web form上,将抛出异常。不易发现并不为人知的是,事实上web form可以包含任意数量的服务器端form,只要同一时刻仅有一个可见并呈现。例如,一个页面包含有3个带有runat="server"标记的<form>是允许的,但是仅有一个form的Visible属性可以设置为true.通过激活HtmlForm类的Visible属性,您可以在页面的生命周期改变活动的服务器端form。这个小窍门不能解决同时有多个活动form的问题,但是有时还是有所帮助的。
让我们考虑一下图4中的页面,所有的<form>被标记为runat="server",但是只有第一个是可见的,互斥的form在ASP.NET 1.x中很顺利的实现了一个向导。通过在按钮事件中转换各form的可见性,您可以获得一个类向导的行为,参看图5:
![图5 类向导页面的运行效果](http://msdn.microsoft.com/msdnmag/issues/05/09/CuttingEdge/fig05.gif)
图5 类向导页面的运行效果
这个技巧在ASP.NET 2.0中基本没用,因为您会发现有2个新控件:MultiView 和 Wizard。MultiView控件使用逻辑等同的互斥表单,可惜它使用panel而没用正统的form。MultiView允许你定义多个互斥的HTML panel,并提供了API来切换这几个panel的可见性,并确保同一时刻只有一个被激活并可见。MultiView控件没有提供内建的用户接口。Wizard控件仅是MultiView加上一个类向导的预定义的UI块,我在MSDN杂志2004年11期上讲解过它(参阅 Cutting Edge: The ASP.NET 2.0 Wizard Control )。
Cross-Page Posting(交叉页提交)
ASP.NET 2.0 提供了一个新的内建进制以覆盖常规处理周期并允许页面提交到另一个页面。通常,postback发生在下面两种方式之一:Submit按钮激发或通过script激发。典型的按钮提交自动指向form指定的提交地址,而如果提交是通过script时则更加灵活机动。在ASP.NET 2.0中,您可以配置某个控件(尤其那些实现了新的IButtonControl接口的)使其可以提交给其他目标页面,具体可以查阅cross-page posting。
实现IButtonControl的核心控件是Button, ImageButton, 和 LinkButton。通常,通过实现IButtonControl,所有的自定义控件都可以有表单中的按钮同样的效果。IButtonControl接口正是一个ASP.NET从1.0到2.0迁移时代码重构的一个典型例子。IButtonControl接口聚合了ASP.NET 1.x支持的多数按钮控件(包括一些html按钮控件)的一些属性。另外,一些新的属性公布了新增的功能,象PostBackUrl 和 ValidationGroup,图6详细描述了IButtonControl接口。接下来的代码片断演示了如何使用:
<form runat="server"> <ASP:textbox runat="server" id="Data" /> <ASP:button runat="server" id="buttonPost" Text="Click" PostBackUrl="target.aspx" /> </form> |
当PostBackUrl属性被设置,ASP.NET运行时为按钮控件的相应的html元素绑定一个新的JavaScript功能。将会使用新的WebForm_DoPostBackWithOptions函数取代常规我们使用的__doPostback函数,客户端呈现效果如下:
<input type="submit" name="buttonPost" id="buttonPost" value="Click" onclick="javascript:WebForm_DoPostBackWithOptions( new WebForm_PostBackOptions("buttonPost", "", false, "", "target.aspx", false, false))" /> |
结果是,当用户点击按钮时,当前的表单提交内容给指定的目标页。那么视图状态的情况呢?当含有可以cross-page posting的控件时,页面会创建一个name为__PREVIOUSPAGE的隐藏域,此域包含了提交页的信息。目标页使用此信息来创建一个完整状态的引用来调用页对象。
在目标页,您可以使用Page类的新增的一个属性PreviousPage来引用提交页和页面上所有的控件。下面是目标页面对form中某TextBox内容检索的后台代码:
protected void Page_Load(object sender, EventArgs e) { // Retrieves some posted data TextBox txt = (TextBox) PreviousPage.FindControl("TextBox1"); ... } |
通过使用Page类的PreviousPage属性,可以访问提交页上声明的任意输入控件.对输入控件的访问是弱类型的并间接使用FindControl方法.摆在事实面前的问题是目标页面并不知道关于提交页类型的任何信息.同样地,它也不能提供对源页面类的指定成员的访问.
此外,注意FindControl仅仅查找当前container中的控件,如果你要找的控件是在另一个控件内部(比如模板),您必须首先取得这个container的引用然后再搜索container来查找那个控件.为了避免完全借助FindControl,还需要另一种途径.
为了检索提交页面上的的值,FindControl将仅提供安全的选项当您预先不知道将会调用哪个目标页时.然而,当在应用程序的上下文中使用cross-page posting时,将有机会确切知道的是谁将调用和如何调用这个页面.这种情况下,您可以利用PreviousPageType指令使目标页的PreviousPageType属性强类型为源页面类.在目标页,添加下面的指令:
指令可以为两个互斥属性之一:VirtualPath 或 TypeName.VirtualPath指提交页的URL,TypeName则指明调用页的类型.PreviousPageType指令使目标页PreviousPage属性返回给定路径上的页面相同类型(或者TypeName属性指定的类型)的一个对象引用,而事实本身是您不能直接访问输入控件.在ASP.NET中,每个页面类包含了子控件对应的protected成员.不幸的是,您不能调用外部类的受保护成员.事实上,只有派生类可以访问父类的受保护成员.
为了达到这个目的,您必须在调用页上在一个public属性来进行提交页信息的访问.例如,假象crosspostpage.aspx页上包含一个名字为_textBox1的TextBox,为了使它能够在目标页访问,必须在后台类增加如下代码:
public TextBox TextBox1 { get { return _textBox1; } } |
作为cross-page调用潜在可能的目标,不会自动将目标页成为别的类型.通常目标页总是被自身调用,例如通过一个超链接.这种情况发生时PreviousPage属性返回null并且别的回传相关的属性(象IsPostBack)采用常规值.对于双重功能的页面,好的办法是添加额外的代码来分辨页面的行为.下面的Page_Load事件中的代码使页面只工作于cross-page调用方式:
if (PreviousPage == null) { Response.Write("Sorry, that's not the right way to invoke me."); Response.End(); return; } |
页面重定向
除了按钮控件的PostBackUrl属性,ASP.NET提供了另一个页面间传递控件和值的机制:Server.Transfer方法.当你调用此方法,新页面的URL不会反映到浏览器的地址栏,因为这种页面转向发生服务器端--在客户端不会有任何间接事件发生.下面的代码演示了如何使用此方法来进行页面定向:
protected void Button1_Click(object sender, EventArgs e) { Server.Transfer("targetpage.aspx"); } |
注意下面对页面中进行重定向的调用代码不会被执行.最后,Transfer只是页转向的方法.可是,却对两种情况十分有效:第一,客户端的请求.例如,使用Response.Redirect.第二,同一个应用程序中某请求要在新的页面请求被重用.
对于ASP.NET 1.x,可以通过使用http上下文的Handler属性获取调用对象,象下面所示:
Page caller = (Page) Context.Handler;
由于Handler属性返回了一个有效的对象引用,主体页可以访问它的public成员,但是象我们上面讨论过的不能直接访问页面上受保护级别的控件.
本编程模型也使用于ASP.NET 2.0, 不过,在 ASP.NET 2.0中,变得更简单了,不再需要使用Handler.你可以使用与cross-page postings相同的编程模型并借助一个非空的PreviousPage属性和强类型访问输入域的@PreviousPageType指令来处理.那么页面如何才能检测它是被server transfer调用还是cross-page postback?两种方式下PreviousPage都是非空的,但是PreviousPage对象的Page.IsCrossPagePostBack在cross-page posting方式是为true,而erver transfer则为false.
小结
从一个页面传值到另一个页面有很多种方法可以达成--cross-page posting,server transfer,HTML forms, cookies, session-state, query strings, 或者其他方法等等.那么最有效的是哪个呢?在 ASP.NET 2.0中,cross-page posting 和 server transfer提供了一个常见的编程模型,但却潜在地通过View State移动了大块的数据。而这些信息是否真正需要依赖于目标页面的描述。在很多情况下,目标页面仅仅需要接收启动运作的一些参数。if this is so,HTML客户端表单可能用移动数据更加有效,尽管HTML表单需要一个类ASP的编程模型。
ASP.NET 2.0为HtmlForm类增加了一些新的特性,然而核心行为并未改变,因此提交自身仍是ASP.NET编程的主要方法。您可以混合客户端form和服务器form,也可以拥有多个服务器form不过同一时间内仅有一个可见。
问题和评论请发送至 cutting@microsoft.com.作者简介
Dino Esposito在意大利罗马工作,任职讲师兼顾问。《Programming ASP.NET》和新书《Introducing ASP.NET 2.0》(两书均由Microsoft Press出版)的作者,他将大部分时间用于讲授ASP.NET和ADO.NET方面的课程以及在各种会议上演讲。如果希望与Dino取得联系,请发送email至 cutting@microsoft.com 或者浏览他的blog站点:http://weblogs.ASP.NET/despos。
译自 MSDN Magazine 2005年9月刊 .
图 1 Properties of the HtmlForm Class
Property | ASP.NET 1.x | ASP.NET 2.0 | Description |
---|---|---|---|
Attributes | ![]() |
![]() |
Inherited from Control. Gets a name/value collection with all the attributes declared on the tag. |
ClientID | ![]() |
![]() |
Inherited from Control. Gets the value of UniqueID. |
Controls | ![]() |
![]() |
Inherited from Control. Gets a collection object that represents the child controls of the form. |
DefaultButton | ![]() |
String property. Gets or sets the button control to display as the default button on the form. | |
DefaultFocus | ![]() |
String property. Gets or sets the button control to give input focus when the form is displayed. | |
Disabled | ![]() |
![]() |
Gets or sets a value indicating whether the form is disabled. Matches the disabled HTML attribute. |
EncType | ![]() |
![]() |
Gets or sets the encoding type. Matches the enctype HTML attribute. |
ID | ![]() |
![]() |
Inherited from Control. Gets or sets the programmatic identifier of the form. |
InnerHtml | ![]() |
![]() |
Inherited from HtmlContainerControl. Gets or sets the markup content found between the opening and closing tags of the form. |
InnerText | ![]() |
![]() |
Inherited from HtmlContainerControl. Gets or sets the text between the opening and closing tags of the form. |
Method | ![]() |
![]() |
Gets or sets a value that indicates how a browser posts form data to the server. The default value is POST. Can be set to GET if needed. |
Name | ![]() |
![]() |
Gets the value of UniqueID. |
Style | ![]() |
![]() |
Gets a collection of all cascading style sheet (CSS) properties applied to the form. |
SubmitDisabledControls | ![]() |
Indicates whether to force controls disabled on the client to submit their values, allowing them to preserve their values after the page posts back to the server. False by default. | |
TagName | ![]() |
![]() |
Returns "form". |
Target | ![]() |
![]() |
Gets or sets the name of the frame or window to render the HTML generated for the page. |
UniqueID | ![]() |
![]() |
Inherited from Control. Gets the unique, fully qualified name of the form. |
Visible | ![]() |
![]() |
Gets or sets a value that indicates whether the form is rendered. If false, the form is not rendered to HTML. |
图 2 Methods of the HtmlForm Class
Method | ASP.NET 1.x | ASP.NET 2.0 | Description |
---|---|---|---|
ApplyStyleSheetSkin | ![]() |
Applies the skin in the defined page theme in the manner defined by StyleSheetTheme. | |
DataBind | ![]() |
![]() |
Calls the DataBind method on all child controls. |
FindControl | ![]() |
![]() |
Retrieves and returns the control that matches the specified ID. |
Focus | ![]() |
Sets the input focus to a control. | |
HasControls | ![]() |
![]() |
Indicates whether the form contains any child controls. |
RenderControl | ![]() |
![]() |
Outputs the HTML code for the form. If tracing is enabled, caches tracing information to be rendered later, at the end of the page. |
图 3 Server and Client Forms in the Same Page
<html> <body> <table><tr><td> <form id="form1" runat="server"> <h2>Ordinary contents for an ASP.NET page</h2> </form> </td> <td> <form method="post" action="search.aspx"> <table><tr> <td>Keyword</td> <td><input type="text" id="Keyword" name="Keyword" /></td> </tr><tr> <td><input type="submit" id="Go" value="Search" /></td> </tr></table> </form> </td> </tr></table> </body> </html> |
图 4 Markup for an ASP.NET 1.x Wizard-Like Page
<html> <body> <form id="step0" runat="server" visible="true"> <h1>Welcome</h1> <asp:textbox runat="server" id="Textbox1" /> <asp:button ID="Button1" runat="server" text="Step #1" OnClick="Button1_Click" /> </form> <form id="step1" runat="server" visible="false"> <h1>Step #1</h1> <asp:textbox runat="server" id="Textbox2" /> <asp:button ID="Button2" runat="server" text="Previous step" OnClick="Button2_Click" /> <asp:button ID="Button3" runat="server" text="Step #2" OnClick="Button3_Click" /> </form> <form id="step2" runat="server" visible="false"> <h1>Finalizing</h1> <asp:button ID="Button4" runat="server" text="Finish" OnClick="Button4_Click" /> </form> </body> </html> |
图 6 IButtonControl Interface
Property | Description |
---|---|
CausesValidation | Boolean value indicating whether validation is performed when the control is clicked. |
CommandArgument | Gets or sets an optional parameter passed to the button's Command event along with the associated CommandName. |
CommandName | Gets or sets the command name associated with the button that is passed to the Command event. |
PostBackUrl | Indicates the URL that will handle the postback triggered through the button control. |
Text | Gets or sets the caption of the button. |
ValidationGroup | Gets or sets the name of the validation group to which the button belongs. |
Visible | Boolean value indicating whether the button control is rendered. |