ASP.NET 2.0 动态创建控件问题

来源:互联网 发布:快钱是什么软件 编辑:程序博客网 时间:2024/05/22 06:20

今天在CSDN社区看到一个题为:asp.net的高級問題?不夠斤兩的不要進來。。。。。 的问题(http://community.csdn.net/Expert/topic/5336/5336458.xml?temp=.1451074 ),感觉好奇就进去看了看。看似一个简单的问题,想回答好这个问题还真的好好学学ASP.NET 的 View State。DotNet我也是刚开始学习,看了下面这篇摘自(http://www.microsoft.com/china/msdn/library/webservices/asp.net/dnasppDynamicUI.mspx)才终于找到答案。

ASP.NET 中的动态控件入门

众所周知,ASP.NET Web 页由两部分组成:

HTML 部分,它包含静态的 HTML 标记和 Web 控件,通过声明性语法来添加。

代码部分,可以作为独立的类文件实现(如采用 Visual Studio .NET),或者包含在 HTML 文件的 <script runat="server"> 块中。

ASP.NET Web 页的 Web 控件是在设计时通过声明性语法来添加的,它明确指出了要添加的 Web 控件及其初始属性值,如:

<asp:WebControlName 
runat="server" 
prop1
="Value1" 
prop2
="Value2" 
...
propN
="ValueN">
</asp:WebControlName>

要理解的一个重点是,当第一次访问 ASP.NET 页面,或者当其 HTML 部分修改后第一次访问时,ASP.NET 引擎会自动将混合的静态 HTML 内容和 Web 控件语法转换成一个类。这个自动生成的类的作用是创建控件层次结构。这个控件层次结构是组成页面的控件集 — 静态的 HTML 标记转换成 LiteralControl 实例,而 Web 控件转换成相应类类型的实例(例如, 转换成 System.Web.UI.WebControls 命名空间中的 TextBox 类的实例)。

之所以称为控件层次结构是因为它是控件的真正的层次结构。每个 ASP.NET 服务器控件可以有一组子控件和一个父控件。当自动生成的类构造控件层次结构时,它会将代表 ASP.NET 页面的 Page 类实例放在层次结构的顶层。Page 类的子控件是那些在页面的 HTML(通常是一些静态的 HTML 标记以及 Web 窗体的服务器控件)中定义的顶级服务器控件。(ASP.NET 页面的 Web 窗体 — 也就是 <form runat="server">标记 — 是作为 HtmlForm 类的实例实现的,可以在 System.Web.UI.HtmlControls 命名空间中找到这个类。)

和任何其他服务器控件一样,这个 Web 窗体可以包含子控件。Web 窗体的子控件是那些在该 Web 窗体本身中发现的控件。甚至 Web 窗体中的控件本身还可能有子控件:Panel 控件的内容构成了其子控件;当将数据绑定到一个 DataGrid 时,产生的内容构成了它的子控件集。因为顶级 Page 类可能有子控件,子控件又有子控件,子控件又有子控件,等等,这组控件就构成了控件层次结构。

为了帮助彻底理解这个概念(理解它对使用动态控件是至关重要的),请想象您有一个 ASP.NET 页面,它在 HTML 部分有以下内容:

<html>
<body>
  
<h1>Welcome to my Homepage!</h1>
  
<form runat="server">
    What is your name?
    
<asp:TextBox runat="server" ID="txtName"></asp:TextBox>
    
<br />What is your gender?
    
<asp:DropDownList runat="server" ID="ddlGender">
      
<asp:ListItem Select="True" Value="M">Male</asp:ListItem>
      
<asp:ListItem Value="F">Female</asp:ListItem>
      
<asp:ListItem Value="U">Undecided</asp:ListItem>
    
</asp:DropDownList>
    
<br />
    
<asp:Button runat="server" Text="Submit!"></asp:Button>
  
</form>
</body>
</html>

当第一次访问该页面时,会自动生成一个类,这个类包含以编程方式构建控件层次结构的代码。这个示例的控件层次结构如图 1 所示。


图 1. 控件层次结构

以编程方式使用控件层次结构

正如前面提到的,每个 ASP.NET 服务器控件可以包含一组子控件和一个父控件。子控件可通过类型为 ControlCollection 的服务器控件的 Controls 属性访问。ControlCollection 类提供了以下功能:

使用 Count 只读属性来确定有多少子控件。

使用 Add() 或 AddAt() 方法向控件集合添加新项。

通过 Clear() 方法删除所有子控件,或者通过 Remove() 或 RemoveAt() 方法删除特定控件。

要将一个控件作为 X 控件的子控件添加到控件层次结构中,只需创建该控件的相应类实例并添加到 X 控件的 Controls 集合中。例如,要向 Page 类的 Controls 集合添加一个 Label 控件,可以使用下列代码:

'Create a new Label instance
Dim lbl as New Label

'Add the control to the Page's Controls collection
Page.Controls.Add(lbl)

'Set the Label's Text property to the current date/time
lbl.Text = DateTime.Now

在 Page 的 Controls 集合尾部添加控件会使该控件出现在 Web 页的底部。如果您需要的控件比动态添加的控件的位置多,您可以在页面中添加一个 PlaceHolder Web 控件,在层次结构中指定要添加一个或多个动态控件的位置。要在该位置中添加动态控件,只需将它们添加到 PlaceHolder 的 Controls 集合中。例如,如果您想将 Label 放在 Web 窗体中的某个点,您可以按如下方式添加一个 PlaceHolder 控件:

 

要在上一个示例中添加动态的 Label,不应该使用 Page.Controls.Add(lbl),而应该使用 dateTimeLabel.Controls.Add(lbl),从而将该 Label 添加到 PlaceHolder 的 Controls 集合中,而不是添加到 Page 的 Controls 集合中。图 2 图示了将动态 Label 添加到 PlaceHolder 的 Controls 集合前后的控件层次结构。


图 2. 图示了添加动态 Label 前后的控件层次结构

通常,最好的方式是使用 Add() 方法将动态控件添加到 Controls 集合的尾部,而不是使用 AddAt() 将其添加到集合中的特定位置。其原因在于,视图状态的保存方式是每个控件记录自己的视图状态及其子控件的视图状态。当保存其子控件的视图状态时,每个控件记录子控件的视图状态及该控件在 Controls 集合中的序号索引。

在回发过程中,当重新加载视图状态时,将反向执行这一过程,同时每个控件加载其子控件的视图状态。重新加载视图状态的控件通过视图状态信息枚举,在 Controls 集合的指定位置应用该控件的视图状态。如果您在视图状态加载之前在 Controls 集合的非尾部位置插入一个控件,则会出现问题,因为每个子控件的视图状态信息是与 Controls 集合中的特定索引相连的。

要查看在非尾部位置添加动态控件为何会导致重新加载视图状态的问题,请参考图 3。图 3 显示了一个服务器控件 p,它具有三个子控件:c0c1c2,其中控件 c1 有一些视图状态在回发过程中保持不变。如果在回发过程中向 p 的 Controls 集合前端添加一个动态控件 c,则当重新加载视图状态时,p 会试图重新加载索引 1 中的 c1 的视图状态,而它现在已被 c0 所占用。


图 3. 具有三个子控件的服务器控件 p

当删除控件时,也同样会出现与视图状态相关的问题。当然,这一切都取决于在页面生命周期的什么时候添加或删除控件。有关视图状态、页面生命周期,以及添加和删除动态控件与视图状态的相关问题等的更详细讨论,请务必阅读我以前的文章 Understanding ASP.NET View State。

访问动态添加的控件

当向 ASP.NET 页面添加静态 Web 控件时,Visual Studio .NET 会自动在代码隐藏类中添加对 Web 控件的引用。这些对 Web 控件的引用允许对控件、其属性及方法进行强类型访问。当处理动态添加的控件时,可以使用两种技术来访问控件的属性、方法和事件。

一种方法是通过对控件层次结构进行彻底的检查,从而发现动态控件。例如,以下代码演示了如何递归循环访问以指定控件为根的控件层次结构。例如,如果已将大量 DropDownList 控件动态添加到指定的 PlaceHolder 中,则这样的代码就十分有用。在这种情况下,您可以通过调用 RecurseThroughControlHierarchy(PlaceHolderControl) 来枚举 PlaceHolder 的控件子代,在“Do whatever it is you need to do with the current control, c 的类型是否是 DropDownList,如果是,就采取某种操作。

Private Sub RecurseThroughControlHierarchy(ByVal c as Control)
  
'Do whatever it is you need to do with the current control, c

  
'Recurse through c's children controls
  For Each child as Control in c.Controls
    RecurseThroughControlHierarchy(child)
  
Next
End Sub

如果您有大量相似的服务器控件需要共同处理,则上述方法行得通。但在很多情况下,您可能有大量不同的控件,需要在不同时间分别访问并对每个控件执行不同的操作。要以编程方式处理特定的动态添加的控件,您可以使用 FindControl(ID) 方法,根据控件的 ID 搜索控件。FindControl() 方法是在 System.Web.UI.Control 类中定义的,所以所有的 服务器控件,从 TextBox 到 PlaceHolder,再到 Web 窗体,都有这个方法。

调用一个控件的 FindControl() 方法并不需要搜索该控件的所有子代控件。FindControl() 只搜索当前的命名容器 (naming container)。实现 INamingContainer 的控件行为上就像一个命名容器,意味着它们在控件层次结构中创建自己的 ID 命名空间。例如,DataGrid 控件是一个命名容器。给定一个 ID 为 myDataGrid 的 DataGrid,其子控件的 ID 以父控件的 ID 为前缀,如 myDataGrid:childID。重要的是认识到 FindControl() 只枚举子控件集或命名容器中的控件,而非控件层次结构中父控件的所有子代。(另外,要使搜索范围超越命名容器中的第一级控件,您需要使用作用域恰当的 ID。)其要点是,当使用 FindControl() 来查寻动态添加的控件时,要从该动态控件的父控件(通常是 PlaceHolder 控件)调用 FindControl()。

当使用 FindControl() 方法时,可以使用如下代码来分配一个唯一的 ID 给动态添加的控件,然后引用上述控件。

'When adding the control, set the ID property
Dim tb As New TextBox
PlaceHolderID.Controls.Add(tb)
tb.ID 
= "dynTextBox"

'At some later point in the page lifecycle, 
'
reference the dynamic TextBox
Dim dTB As TextBox
dTB 
= CType(PlaceHolderID.FindControl("dynTextBox"), TextBox)

由于 FindControl() 方法使用控件的 ID 来定位控件,所以当使用这种技术来访问动态添加的控件时,为每个动态添加的控件的 ID 属性分配一个唯一可识别的值是很重要的。根据情况的不同,可以使用不同的方法。我们在本文后面也将看到,当检查动态数据输入用户界面引擎时,每个动态问题都由数据库中的一行表示,它包含一个唯一的主键字段。这个主键字段值即在 ASP.NET 页面中作为每个动态添加的控件的 ID 使用。如果您不需要区分动态添加的控件,则可以使用另一种技术,该技术向这些控件提供递增的编号作为 ID,如 myDynCtrl1 用于第一个动态添加的控件,myDynCtrl2 用于第二个,等等。

页面生命周期和动态控件

任何时候访问一个 ASP.NET Web 页(不管是初始页面访问还是回发),每次 ASP.NET 引擎自动生成的类都会从头开始重新构建控件层次结构。不仅重新构造控件层次结构,而且将控件的事件重新连接到其指定事件处理程序。因此,当向 ASP.NET 页面添加动态控件时,确保在每次 页面访问添加这些控件是很重要的。许多开发人员在开始添加动态控件时都使用以下模式来实现:

'In the Page_Load event handler...
If Not Page.IsPostBack Then
  
'Add dynamic controls...
End If

这段代码的问题是它只在第一次页面访问时添加动态控件,而在后续回发时则没有添加。如果您尝试使用这段代码,您会发现,只要发生回发,您的动态控件就会从页面中消失。因此,您必须确保在所有页面访问中添加所有动态控件,方法是将这段代码移到 If Not Page.IsPostBack 条件语句外面。

添加动态控件引出的一个重要问题是此类控件应在页面生命周期的什么时候添加。正如我在 Understanding ASP.NET View State 中讨论的,只要一个请求到达,ASP.NET 页面就要经历许多步骤。让我们花点时间概述一下页面生命周期内几个紧密相连的阶段。为了能够更深入理解,要确保先阅读一下关于视图状态的文章,重点关注那篇文章中的 The ASP.NET Page Lifecycle 部分。

ASP.NET 页面生命周期回顾

页面生命周期中的第一个阶段是实例化,在这个阶段中,自动生成的类会根据页面的 HTML 部分中定义的静态控件构建控件层次结构。构造控件层次结构时,声明性语法中指定的值会赋给添加的每个控件的属性。实例化之后是初始化阶段,在这个阶段,静态控件层次结构已经构造,但还没重新加载视图状态(假定页面请求是回发)。如果页面请求是回发,则在初始化之后是加载视图状态阶段。在这个阶段中,页面会过滤出在隐藏的 VIEWSTATE 窗体字段中发现的视图状态数据,如果需要,控件层次结构中的每个控件会更新自己的状态。

如果页面请求是回发,则在加载视图状态阶段之后是加载回发数据阶段。这个阶段会检查发送的窗体字段值,并据此更新相应控件的属性。例如,通过 POST 机制(发出信号表示 TextBox 控件的名称和用户输入的值),来回送用户在 TextBox Web 控件中输入的文本。页面获得这些值,在控件层次结构中定位恰当的 TextBox,并将接收的值赋给它的 Text 属性。

下一个阶段是加载阶段,发生在 Page_Load 事件处理程序激发时。加载阶段之后还有更多阶段,如引发回发事件、保存视图状态和呈现 Web 页,但这些与动态控件的主题无关,因此不加以讨论。图 4 图示了页面在生命周期内所经历的事件。


图 4. 页面生命周期

确定在页面生命周期的什么时候添加动态控件

关于在页面生命周期的什么时候添加动态控件的问题可以归纳如下:动态控件需要在加载视图状态和重新加载回发数据之前添加,因为我们想要正确添加特定于动态控件的任何视图状态或回发值。考虑到这些限制,添加动态控件的正常时间是在初始化阶段,因为它发生在加载视图状态阶段和加载回发数据阶段之前。

然而,在初始化阶段,视图状态和回发数据都还没还原,因此不建议访问或设置可能存储在视图状态或被回发值修改的控件属性(不管是动态还是静态控件),因为这些值将被生命周期后续阶段的视图状态和回发值所覆盖。当处理动态控件时我使用了以下模式:

在初始化阶段,我向控件层次结构添加动态控件并设置 ID 属性

在加载阶段,我在 If Not Page.IsPostback 条件语句中为动态控件赋予任何需要的初始值。

我需要在每次回发时添加动态控件,但只在第一次页面加载时设置属性值,因为这些值会保留在视图状态中。以下代码片段说明了这种模式:

'In the Init event of the Page, add a dynamic TextBox
Dim tb as New TextBox
PlaceHolderID.Controls.Add(tb)
tb.ID 
= "dynTextBox"

'In the Page_Load event handler, set the properties 
'
of the TextBox
If Not Page.IsPostBack Then
  
Dim dTB As TextBox
  dTB 
= CType(PlaceHolderID.FindControl("dynTextBox"), TextBox)
  dTB.Text 
= "Some initial value"
  dTB.BackColor 
= Color.Red  'initial BackColor
End If

除了在初始化阶段加载动态控件外,您还可以在加载阶段添加,这样不会有什么负面影响。当将控件添加到另一个控件的 Controls 集合时,所添加的控件会立即在其新父控件的生命周期内被确立。例如,如果父控件处于初始化阶段,则会引发所添加控件的 Init 事件,使该控件与其父控件保持同步。如果父控件处于加载阶段或以后的阶段,则所添加的子控件会立即经历初始化阶段、加载视图状态阶段、加载回发数据阶段和加载阶段。

当在加载阶段添加控件时,有一个警告需要注意。当一个控件完成其加载视图状态阶段后,它就开始跟踪对其视图状态的更改。这意味着加载视图状态阶段之后 的任何属性更改都会自动保留在控件的视图状态中。在一个控件开始跟踪其视图状态的更改之前,属性值更改不会保留在视图状态中。如果您在初始化阶段添加控件然后在加载阶段设置其属性,则不会有问题,因为在初始化阶段和加载阶段之间已经发生了加载视图状态阶段,控件的跟踪视图状态更改标志已设置。也就是说,如果在初始化阶段添加动态控件,则从运行加载阶段起,动态控件的属性赋值会保留在视图状态中。

页面开发人员无法修改“跟踪视图状态更改标志”。System.Web.UI.Control(所有 ASP.NET 服务器控件都由此派生)只提供对该标志的受保护访问。具体来说,有一个名为 IsTrackingViewState 的受保护只读属性来指示是否在跟踪视图状态,还有一个受保护的 TrackViewState() 方法来指示应该开始跟踪视图状态。所有控件在初始化阶段结束时都会自动调用这个方法。

然而,如果您直到加载阶段才添加动态控件,则该动态控件的任何属性只有在将该控件添加到控件层次结构之后 才能设置,这一点很重要。为了帮助理解其中原因,请考虑如果在加载阶段执行以下代码会发生什么:

Dim tb as New TextBox

If Not Page.IsPostBack Then
  tb.BackColor 
= Color.Red  'initial BackColor
End If

PlaceHolderID.Controls.Add(tb)

正如您所看到的,在每次页面加载时都会创建一个 TextBox。只有在第一次页面加载时才会将 TextBox 的 BackColor 属性设置为 Red,而在以后的每次页面加载都会将该控件添加到控件层次结构中。虽然在第一次页面加载时 TextBox 的背景颜色确实为 Red,但问题是在回发时 TextBox 的背景颜色会还原为默认值(没有背景颜色)。这是因为 TextBox 的 BackColor 属性赋值没有保留到视图状态中,所以在回发时丢失。丢失的原因是 TextBox 与其他任何服务器控件一样,只有在加载视图状态阶段之后才开始跟踪视图状态。但是 TextBox 只有在被添加到控件层次结构之后才会经历这个阶段,所以 BackColor 赋值没有保留到视图状态中。若要更正这个问题,请确保将控件添加到控件层次结构中,使其提前经历加载视图状态阶段,然后再对其属性赋值,如下所示:

Dim tb as TextBox
PlaceHolderID.Controls.Add(tb)

If Not Page.IsPostBack Then
  tb.BackColor 
= Color.Red  'initial BackColor
End If

如果您在初始化阶段添加动态控件,则与上述细节无关。有关这个问题的更深入讨论,请参考 my blog 条目 Control Building and View State Lesson for the Day。

事件和动态控件

与静态服务器控件一样,动态添加的控件也可以将事件与事件处理程序相关联。正如每次页面访问都必须将控件添加到控件层次结构中,每次页面访问也都需要将动态控件的事件与指定事件处理程序连接起来。这样做的部分挑战是您需要在类中定义适当的事件处理程序。如果您的控件确实是动态的,则如何知道代码隐藏类需要什么样的事件处理程序呢?依我的经验,我发现处理事件和动态控件的最好办法是使用用户控件,而不要使用单独的 Web 控件。对于用户控件,我可以在用户控件的代码部分嵌入特定事件处理程序和程序设计逻辑。我们将在下一节介绍如何动态添加用户控件。

如果您必须将动态添加的 Web 控件的事件与事件处理程序相关联,请确保在每次页面访问时都进行关联。以下代码(包含在本文下载中)演示了如何将一个动态添加的 Button Web 控件的 Click 事件与一个现有的事件处理程序相关联。(ph 是页面中 PlaceHolder 控件的名称。有关用 C# 将事件与事件处理程序连接起来的示例,以及在 .NET Framework 中进行事件处理的更详细信息,请参阅 Peter Bromberg 的文章 Delegates to the Event。)

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
   
Dim b As New Button
   ph.Controls.Add(b)

   
If Not Page.IsPostBack Then
      b.Text 
= "Click Me"
   
End If

   
AddHandler b.Click, New EventHandler(AddressOf Me.ButtonClickEventHandler)
End Sub


Private Sub ButtonClickEventHandler(ByVal sender As ObjectByVal e As EventArgs)
   Response.Write(
"The button has been clicked!")
End Sub
原创粉丝点击