创建动态数据输入用户界面

 

斯科特·米切尔
4GuysFromRolla.com

2004 年 12 月

总结: Scott Mitchell 演示了一种基于正在编辑的数据动态生成 ASP.NET 数据输入表单的方法。 ) (22 个打印页

下载 MSDNDynUI.msi 文件

目录

简介
ASP.NET 中的动态控件入门
生成动态数据输入用户界面引擎
结论

简介

创建数据驱动网站时,Web 开发人员面临的最常见任务之一是创建数据输入表单。 数据输入表单是向系统用户提供输入数据的方法的网页。 创建特定数据输入表单的任务通常从敲定具体说明需要从用户那里收集哪些信息的要求开始。 定义要求后,下一阶段是设计数据输入 Web 窗体,这涉及到创建图形用户界面,以及编写使用用户输入更新数据库的代码。

如果数据输入表单要求事先是众所周知的,并且当此类数据输入表单在系统的所有用户中都相同时,创建此类输入表单几乎没有挑战性。 但是,如果数据输入表单需要是动态的,则任务将变得更加艰巨。 例如,考虑一家公司的 Internet Web 应用程序,其用途是收集有关客户购买的产品的信息;一种在线产品注册系统。 使用此类应用程序时,用户提出的问题可能因他们购买的产品而异,或者他们是否从商店或公司的网站购买了产品。

当遇到需要提供动态数据输入用户界面(如上述示例所示)时,一种选择可能是“暴力破解”解决方案。 可以为公司销售的每个产品创建单独的网页,每个页面都有所需的特定数据输入元素。 这种朴素方法的问题在于,在发布新产品时,它需要添加新页面。 虽然创建这些新页面可能并不困难,但如果不有足够的调试和测试时间,它很耗时且容易出错。

理想情况下,当新产品发布时,非技术同事可以通过易于使用的基于 Web 的界面指定需要哪些问题。 由于能够在运行时动态加载 ASP.NET 网页上的控件,因此 ASP.NET 这种系统是可能的。 只需对开发和测试时间进行一点初始投资,即可创建可重用的动态数据输入用户界面引擎。 一种,即使是最不精通计算机的用户也能够轻松创建自定义数据输入表单。 在本文中,我们将了解在 ASP.NET 中使用动态控件的基础知识,然后介绍一个可以轻松自定义和扩展的完整、正常工作的动态数据输入系统。

ASP.NET 中的动态控件入门

如你所知,ASP.NET 网页由两个部分组成:

  • 包含静态 HTML 标记和 Web 控件的 HTML 部分,通过声明性语法添加。
  • 可作为单独的类文件实现的代码部分, (与 Visual Studio .NET) 一样,或在 HTML 文件的 块中 <script runat="server"> 实现。

ASP.NET 网页的 Web 控件在设计时通过声明性语法添加,该语法拼写出要添加的 Web 控件及其初始属性值,如下所示:

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

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

之所以调用控件层次结构,是因为它是控件的实际层次结构。 每个 ASP.NET 服务器控件可以有一组子控件和一个父控件。 当自动生成的类构造控件层次结构时,它会将表示 ASP.NET 页的 Page 类实例置于层次结构的顶部。 Page 类的子控件是在页面的 HTML 中定义的顶级服务器控件,该控件通常是一些静态 HTML 标记以及 Web 窗体的服务器控件。 (ASP.NET 页的 Web 窗体(该 <form runat="server"> 标记)作为 类的 HtmlForm 实例实现,可在 namespace 中找到 System.Web.UI.HtmlControls 该实例。)

与其他任何服务器控件一样,Web 窗体可以包含子控件。 Web 窗体的子控件是在 Web 窗体本身中找到的那些控件。 甚至 Web 窗体中的控件也可能有子控件本身:面板控件的内容构成其子控件;将数据绑定到 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 所示。

Aa479330.dynui_fig01S (en-us,MSDN.10) .gif

图 1. 控件层次结构

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

如上所述,每个 ASP.NET 服务器控件可以同时包含一组子控件和一个父控件。 子控件可通过服务器控件的 Controls 属性(类型 ControlCollection为 )进行访问。 类 ControlCollection 提供以下功能:

  • 使用只读 Count 属性确定有多少子控件。
  • 使用 Add()AddAt() 方法将新项添加到控件集合。
  • 使用 Clear() 方法删除所有子控件,或使用 或 RemoveAt() 方法删除特定控件Remove()

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

'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

将控件添加到 的 Controls 集合的Page末尾将导致控件显示在网页的底部。 如果需要对动态添加的控件的位置进行更多控制,可以向页面添加 PlaceHolder Web 控件,并指定层次结构中的位置以添加一个或多个动态控件。 若要在该位置添加动态控件,只需将它们添加到 PlaceHolder 的 Controls 集合中即可。 例如,如果要将 Label 放置在 Web 窗体中的某个位置,则可以添加 PlaceHolder 控件,如下所示:

<html>
<body>
   ...
   <form runat="server">
     ...
     <asp:PlaceHolder runat="server" id="dateTimeLabel"></asp:PlaceHolder>
     ...
    </form>
  </body>
</html>

若要添加我们过去示例中的动态 Label,而不是使用 Page.Controls.Add(lbl),请使用 dateTimeLabel.Controls.Add(lbl),从而将 Label 添加到 PlaceHolder 的 Controls 集合,而不是 PageControls 集合。 图 2 提供了在将动态 Label 添加到 PlaceHolder 集合之前和之后的控件层次结构的 Controls 图形图示。

Aa479330.dynui_fig02 (en-us,MSDN.10) .gif

图 2. 添加动态标签之前和之后控件层次结构的图形插图

通常,最好使用 Add() 方法将动态控件添加到集合的Controls末尾,而不是使用 AddAt()将其添加到集合中的特定位置。 这是因为每个控件记录其视图状态及其子控件的视图状态时,视图状态的保存方式。 保存其子控件的视图状态时,每个控件都会记录子控件的视图状态以及集合中 Controls 控件的序号索引。

回发时,当重新加载视图状态时,进程会自行反转,每个控件加载其子控件的视图状态。 重载其视图状态的控件通过视图状态信息枚举,在集合中的指定位置应用控件的 Controls 视图状态。 如果在加载视图状态之前,由于每个子控件的视图状态信息与集合中的Controls特定索引相关联,因此将控件Controls插入集合中尾端以外的位置,则可能会出现问题。

若要了解将动态控件添加到末尾以外的位置如何导致重新加载视图状态时出现问题,请参阅图 3。 图 3 显示了一个服务器控件 p ,其中包含三个子控件: c0c1c2,其中控件 c1 在回发中保留了某些视图状态。 如果在回发时将动态控件 c 添加到 pControls 集合的前面,则重新加载视图状态时,p 将尝试在索引 1 中重新加载 c1 的视图状态,该索引现在由 c0 占用。

Aa479330.dynui_fig03S (en-us,MSDN.10) .gif

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

删除控件时,可能会出现相同的视图状态相关问题。 当然,这一切取决于在页面生命周期中添加或删除控件的日期。 有关视图状态、页面生命周期以及添加和删除动态控件和视图状态的问题的更深入讨论,请务必阅读我前面的文章 了解 ASP.NET 视图状态

访问动态添加的控件

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

一种方法是通过对控件层次结构进行详尽检查来查找动态控件。 例如,下面的代码演示了如何以递归方式迭代根植于指定控件的控件层次结构。 例如,如果许多 DropDownList 控件已动态添加到指定的 PlaceHolder,则此类代码可能很有用。 在这种情况下,可以通过调用 RecurseThroughControlHierarchy(PlaceHolderControl)来枚举 PlaceHolder 的控件后代,将代码添加到“执行当前控件所需的任何操作”部分,c该部分检查以查看是否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定义,因此所有服务器控件(从 TextBoxs 到 PlaceHolders 到 Web Forms)都具有此方法可用。

调用控件的 FindControl() 方法不一定搜索控件的所有后代控件。 FindControl() 仅搜索当前 命名容器。 实现 INamingContainer 的控件充当命名容器,这意味着它们在控件层次结构中创建自己的 ID 命名空间。 例如,DataGrid 控件是一个命名容器。 如果 DataGrid 具有 ID myDataGrid,则其子控件的 ID前缀为其父控件的 ID,如 中所示 myDataGrid:childID。 需要注意的是, FindControl() 仅枚举命名容器中的子控件集,而不是控件层次结构中父级的所有后代。 (此外,若要在命名容器中搜索超出第一个级别的控件,则需要使用作用域正确的 ID.) 当使用 FindControl() 搜索动态添加的控件时,从动态控件的父控件调用 FindControl() (通常为 PlaceHolder 控件) 。

使用 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 网页时,无论是在初始页面访问还是回发时,ASP.NET 引擎自动生成的类每次都从头开始重新生成控件层次结构。 不仅重新构造了控件层次结构,而且控件的事件将重新连接到其指定的事件处理程序。 因此,将动态控件添加到 ASP.NET 页时,请务必在 每次 访问页面时添加这些控件。 许多从添加动态控件开始的开发人员将使用以下模式执行此操作:

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

此代码的问题在于,它仅在第一页访问时添加动态控件,而不是在后续回发时添加。 如果尝试使用此类代码,你会发现每当发生回发时,动态控件就会从页面中消失。 因此,必须确定通过从条件语句中移出 If Not Page.IsPostBack 代码,在所有页面访问中添加所有动态控件。

添加动态控件引发的一个重要问题是,何时在页面生命周期中添加此类控件。 正如我在 了解视图状态 ASP.NET 中所述,每当请求到达时,ASP.NET 页会继续执行多个步骤。 让我们花一点时间回顾一下页面生命周期中的德文阶段。 若要更深入地了解,请务必转到视图状态文章,重点介绍文章的T he he ASP.NET Page Lifecycle 部分。

ASP.NET 页生命周期的回顾

页面生命周期的第一个阶段是 实例化,在此期间自动生成的类从页面 HTML 部分中定义的静态控件生成控件层次结构。 构造控件层次结构时,将为每个添加的控件的属性分配声明性语法中指定的值。 在实例化之后是初始化阶段,此时已构造静态控件层次结构,但尚未重新加载视图状态 (假设页面请求是回发) 。 如果页面请求是回发,则初始化后将进入加载视图状态阶段。 此处,页面对在隐藏 VIEWSTATE 窗体字段中找到的视图状态数据进行扩展,并且控件层次结构中的每个控件会根据需要更新其状态。

如果页面请求是回发,则“加载回发数据”阶段遵循“加载视图状态”阶段。 在此阶段中,将检查发送的表单字段值,并相应地更新相应控件的属性。 例如,用户输入到 TextBox Web 控件中的文本通过 POST 机制发送回,同时指示 TextBox 控件的名称和用户输入的值。 页面采用这些值,在控件层次结构中查找相应的 TextBox,并将其属性分配给 Text 收到的值。

下一阶段是加载阶段,即事件处理程序触发时 Page_Load 。 加载阶段之后还有更多阶段,例如引发回发事件、保存视图状态和呈现网页,但这些阶段与动态控件的主题无关,因此不值得讨论。 图 4 显示了页面在其生命周期内继续浏览的事件的图形插图。

Aa479330.dynui_fig04S (en-us,MSDN.10) .gif

图 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 的背景色在第一页加载时确实为红色,但问题是,在回发时,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

如果在初始化阶段添加动态控件,则此详细信息不相关。 有关此问题的更深入讨论,请参阅 我的博客 文章 “控制生成和查看当天的状态课程”。

事件和动态控件

与静态服务器控件一样,动态添加的控件可以将其事件绑定到事件处理程序。 就像每次访问页面时必须向控件层次结构添加控件一样,每次访问页面时,都需要将动态控件的事件连接到指定的事件处理程序。 这样做的一部分挑战是,需要在 类中定义适当的事件处理程序。 如果控件是真正动态的,那么你怎么能知道代码隐藏类中需要哪些事件处理程序呢? 根据我的经验,我发现使用事件和动态控件的最佳解决方案是使用用户控件,而不是单一实例 Web 控件。 使用用户控件,可以在用户控件的代码部分中嵌入特定的事件处理程序和编程逻辑。 我们将在下一部分了解如何动态添加用户控件。

如果必须将动态添加的 Web 控件的事件与事件处理程序相关联,请确保在每次访问页面时都这样做。 本文下载中包含的以下代码演示如何将动态添加的 Button Web 控件的事件 Click 与现有事件处理程序相关联。 ph (是页面上的 PlaceHolder 控件的名称。有关使用 C# 将事件连接到事件处理程序的示例,以及更详细地了解.NET Framework中的事件处理,请参阅 Peter Bromberg 的文章 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 Object, ByVal e As EventArgs)
   Response.Write("The button has been clicked!")
End Sub

生成动态数据输入用户界面引擎

在过去的几年里,我处理过许多需要 动态数据输入用户界面的项目,这些用户界面依赖于一个更受用户影响的因素。 所有这些项目的基本要求是,这些动态接口需要能够由不精通计算机的用户轻松创建、更新和删除。 在这些项目过程中,我开发了一个动态数据输入用户界面引擎,该引擎允许开发人员创建用户界面构建基块,然后非开发人员可以组合在一起,形成特定于特定用户的用户界面。

本文的其余部分将逐步介绍此引擎的简化版本。 具体而言,本文中的演示演示如何根据客户的类型为客户提供唯一的数据输入用户界面。 例如,常规客户提供的用户界面与仅联机客户或批量购买客户不同。

动态数据输入用户界面引擎的基本部分包括:

  • 用户界面构建基块: 用户界面构建基块是用户控件,其创建由团队中的开发人员负责。 这些构建基块设计为仅在所收集的信息类型方面具有特定性,而不适用于所请求的特定数据。 例如,此演示中包含的其中一个 UI 构建基块是一个构建基块,它提示用户输入整数值作为输入。 用户控件包含 TextBox 和 CompareValidator,以确保用户输入有效的 Integer 值。 此构建基块可以通过将它与“你多大?”或“从你家到工作地点有多少英里?”等问题相关联,将其组合成动态数据输入用户界面?
  • 问题: 问题是由不精通计算机的用户通过基于 Web 的界面创建的自定义构建基块。 问题将某些文本与 UI 构建基块相关联。
  • 区分变量: 每个动态数据输入用户界面都基于一个或多个变量进行谓词。 例如,对于联机产品注册站点,用户界面可能取决于购买的产品。 对于员工信息的数据输入,UI 可能因员工部门而异。 对于本文中介绍的引擎,区分变量将硬编码为客户类型。
  • 动态问题: 对于给定的区分变量,指定一组问题。 问题和区分变量的组合映射构成了系统的动态问题。
  • 动态答案: 为给定客户填写动态数据输入表单后,必须将客户的信息保存到数据库中。 给定客户的答案集是系统中的动态答案。

动态数据输入用户界面引擎的用户界面构建基块部分在 ASP.NET 应用程序中作为用户控件实现。 其余部分作为数据库实体实现。 图 5 显示了引擎的实体关系图,该关系图描绘了各种部分在数据库中的表示方式。

Aa479330.dynui_fig05 (en-us,MSDN.10) .gif

图 5。 Entity-relastionship 示意图

查看图 5 时,首先请注意该 dq_Questions 表。 此表的记录表示系统中的问题。 对于每个问题,都有一些文本与问题 (QuestionText) 和用户控件 (ControlSrc) 相关联。 字段 ControlSrc 包含用户控件的文件名,例如 DQIntegerInput.ascx。 接下来,在左下角有 dq_Customers 桌子。 每个客户都有一个特定的客户类型,所有类型都在表中拼写出来 dq_CustomerTypes

动态问题(即问题和客户类型之间的映射集)通过 dq_DynamicQuestions 表实现。 在那里,问题和客户类型与排序顺序一起绑定在一起,该排序顺序指示特定客户类型的问题呈现顺序。 最后,动态答案存储在 dq_DynamicAnswers 表中,该表将每个动态问题与特定客户相关联。 由于我们无法确定给定问题的答案类型(可能是字符串、布尔值、整数等), dq_DynamicAnswers 因此表中有六列,系统允许的每个数据类型各一列。 给定的问题只能有一个类型,对于其答案,相应的字段将具有答案的值,而其余列将具有 NULL 值。

注意 有关数据模型的几个快速说明。 我决定在表上使用合成主键 dq_DynamicQuestions (DynamicQuestionID) 而不是制作 CustomerTypeIDQuestionID 复合主键,以允许给定客户类型出现重复的问题。 例如,问题可能是“其他注释”,并使用包含多行 TextBox 的用户界面构建基块。 由于你可能想要在一些其他问题之后提出“其他评论”问题,因此我决定允许重复问题。

dq_Questions 采用最简单的形式。 在以往的项目中,我在离开此表非常简单和在用户界面构建基块中嵌入细节,而不是向此表添加与 UI 交互的其他字段之间反弹。 例如,应用程序可能需要能够指示某些问题是必需的,而其他问题是可选的。 在这样的系统中,有两种方法可以解决此问题。 第一种是将责任置于 UI 构建基块中。 也就是说,不是为整数输入创建单个 UI 构建基块,而是创建两个 ,一个使用 RequiredFieldValidator 来确保输入值,另一个不施加此类条件。 形成问题时,管理员可以根据是否需要问题来选择要使用的 UI 构建基块。 另一种方法是向表添加 Required 字段 dq_Questions ,并仅使用一个 UI 构建基块。 使用第二种方法,每个 UI 构建基块都需要一个 Required 属性,并负责根据此属性值启用或禁用适当的验证控件。

最后,该 dq_DynamicAnswers 表包含六个答案相关字段,仅允许从 UI 构建基块获取标量答案。 也就是说,UI 构建基块的答案可以是字符串、整数、双精度、日期、货币或布尔值。 但是,如果需要 UI 构建基块具有更复杂的答案(例如地址),该地址本身可能有多个字段呢? 返回答案时,此类复杂的答案需要由 UI 构建基块序列化为可接受的类型之一。 显示答案时,需要相应地反序列化此类结果。 为此,可以依赖于 固有的二进制序列化功能。NET 的 ,但为此,可能需要将字段添加到 BinaryAnswer 类型 binary为 的此表。

用户界面构建基块的设计规则

为了便于使用开发人员设计的用户控件实现真正动态的数据输入用户界面,用作 UI 构建基块的用户控件必须提供基本级别的功能。 此基本级别的功能在 接口中 IUIBuildingBlock 进行了说明。 此接口定义了所有 UI 构建基块必须实现的三个属性:

  • DataType: 一个只读属性,返回 UI 构建基块提供的答案的数据类型。 必须是枚举中的值 DQDataTypes 之一。
  • QuestionText: 要显示在 UI 构建基块中的问题文本。
  • 答案: UI 构建基块的答案。

为了说明如何使用这些属性,我们来看一个简单的 UI 构建基块。 假设我们要创建一个 UI 构建基块,提示用户输入整数输入。 为此,我们可以创建一个新的用户控件,该控件在其 HTML 部分中包含以下内容:

<asp:Label id="dqQuestion" runat="server" CssClass="DQQuestionText"></asp:Label>:
<asp:TextBox id="dqAnswer" runat="server" CssClass="DQAnswer" Columns="4"></asp:TextBox>&nbsp;
<asp:CompareValidator id="CompareValidator1" runat="server" CssClass="DQErrorMessage" ErrorMessage="You must enter a number here."
   ControlToValidate="dqAnswer" Type="Integer" Operator="DataTypeCheck"></asp:CompareValidator>

此标记包括:

  • 标签 Web 控件 (dqQuestion 显示 UI 构建基块 QuestionText 属性的) ;
  • TextBox (dgAnswer) ,用户将在其中输入其整数值
  • 一个 CompareValidator,用于确保输入的输入确实是一个整数。

用户控件的源代码部分非常简单。 它具有用户控件的 类实现 IUIBuildingBlock 接口,并为三个必需属性提供逻辑:

Public Class DQIntegerQuestion
    Inherits System.Web.UI.UserControl
    Implements IUIBuildingBlock

    ...

    Public ReadOnly Property DataType() As DQDataTypes Implements IUIBuildingBlock.DataType
        Get
            Return DQDataTypes.Integer
        End Get
    End Property

    Public Property Answer() As Object Implements IUIBuildingBlock.Answer
        Get
            If dqAnswer.Text.Trim() = String.Empty Then
                Return DBNull.Value
            Else
                Return dqAnswer.Text
            End If
        End Get
        Set(ByVal Value As Object)
            dqAnswer.Text = Value
        End Set
    End Property

    Public Property QuestionText() As String Implements IUIBuildingBlock.QuestionText
        Get
            Return dqQuestion.Text
        End Get
        Set(ByVal Value As String)
            dqQuestion.Text = Value
        End Set
    End Property
End Class

只读 DataType 属性返回由用户控件返回的数据类型 - Integer。 属性QuestionText只是从 Label 控件的 Text 属性读取或写入dqQuestion,而 Answer 属性从 TextBox 的 Text 属性读取或写入dgAnswer。 这就是 UI 构建基块的全部功能。 对于简单的 UI 构建基块,例如,只有最少的代码和 HTML 标记,但不要让此示例的简单性成为 UI 构建基块的真正强大功能。 由于用户控件可以有多个包含事件处理程序等的 Web 控件,因此可以生成丰富的 UI 构建基块。 本文代码下载中包含的 UI 构建基块之一演示了如何在 UI 构建基块中使用两个依赖的 DropDownList。

注意 创建 UI 构建基块时,请务必将它们全部放在同一目录中。 不过,具体目录并不重要。 在 文件中, Web.config 你将找到一个 <appSettings> 键名称为 buildingBlockPath的元素。 此设置需要提供对用户控件目录的引用。 在代码下载中,默认路径为 ~/UserControls/,但如果需要,可以随意更改此路径。

有关将接口与动态加载的用户控件配合使用的好处的详细信息,请务必阅读 Tim Stall 的文章 了解接口及其有用性

创建问题并将其与客户类型关联

为了使创建动态数据输入用户界面成为非开发人员可以轻松执行的任务,我创建了一个基于 Web 的管理界面,用于创建问题并将其与客户类型相关联。 本文的代码下载中提供了此接口。

管理界面中有两个德语页面。 第一个 CreateQuestion.aspx是 ,允许管理员创建新问题。 回想一下,问题是特定的问题文本和 UI 构建基块。 网页相当简单,为用户提供了一种输入问题文本并从 UI 构建基块目录中选择用户控件的方法, (路径在 Web.config 文件) 指定。 图 6 显示了此页面的屏幕截图。

Aa479330.dynui_fig06 (en-us,MSDN.10) .gif

图 6。 面向非开发人员的基于 Web 的接口

管理界面中的下一个屏幕允许管理员指定与每种客户类型绑定的问题以及顺序。 如图 7 所示的界面非常一目了然。 管理员从排名靠前的 DropDownList 中选择客户类型,然后可以从第二个 DropDownListBox 添加问题。 DataGrid 列出所选客户类型的当前问题,允许用户从列表中删除问题或通过向上和向下箭头重新排序。

Aa479330.dynui_fig07S (en-us,MSDN.10) .gif

图 7。 用于选择问题顺序的 Web UI

显示动态问题和保存结果

系统管理员创建问题并映射到特定客户类型后,可以输入客户的数据。 该页面 EnterData.aspx 通过查询字符串获取客户的 ID,并为客户的客户类型构建动态数据输入用户界面。 此页面包含三种感兴趣的方法:

  • BuildDynamicUI () : 此方法从 Page_Init 事件处理程序 (调用,该事件处理程序在页面的生命周期) 的初始化阶段执行,并为适当的客户类型构建动态控件。 如前所述, BuildDynamicUI() 只需将必要的控件添加到控件层次结构。
  • Page_Load:Page_Load事件处理程序将初始默认值分配给动态添加的 Web 控件。 例如,如果用户已为特定客户提供某些值,则访问页面时,这些值将填充到相应的动态控件中。 这些属性仅在第一页访问时设置,而不在后续回发时设置。
  • btnSaveValues_Click: 此方法连接到 “保存” 按钮的 Click 事件。 它枚举动态添加的控件并更新数据库。

让我们简要了解一下这三种方法。 方法 BuildDynamicUI()Page_Init 事件处理程序调用。 此事件处理程序由 Visual Studio .NET 在“Web 窗体Designer生成的代码”区域中自动添加。) 该方法从 querystring 中获取客户的 ID,然后使用指定客户类型的动态问题填充 SqlDataReaderSqlDataReader然后循环访问。 对于每条记录,将加载指定的用户控件并将其添加到 dynamicControls PlaceHolder。 每个动态控件都有一个形式的 dqDynamicQuestionIDID。

Private Sub BuildDynamicUI()    'Called from Page_Init
    CustomerID = Convert.ToInt32(Request.QueryString("ID"))

    ...

    'Get the list of dynamic controls for the specified customer
    reader = SqlHelper.ExecuteReader(connectionString, _
                 CommandType.StoredProcedure, _
                 "dq_GetDynamicQuestionsForCustomerType", _
                 New SqlParameter("@CustomerTypeID", CustomerTypeID))

    'For each question, add the necessary user control
    While reader.Read
        Dim dq As UserControl = _
             LoadControl(ResolveUrl(buildingBlockPath & _
                                     reader("ControlSrc")))
        CType(dq, IUIBuildingBlock).QuestionText = reader("QuestionText")
        dq.ID = String.Concat("dq", reader("DynamicQuestionID"))

        dynamicControls.Controls.Add(dq)
        dynamicControls.Controls.Add(New LiteralControl("<br /><br />"))
    End While

    reader.Close()
End Sub

注意 在本文随附的示例代码中,我使用 Microsoft 数据访问应用程序块 (DAAB) 版本 2.0 来访问数据库。 DAAB 的 SqlHelper 类提供一个包装器,用于通过一行代码从 Microsoft SQL Server 数据库访问数据。 有关 DAAB 的详细信息,请务必访问用于 .NET 的官方数据访问应用程序块 页,并阅读 John Jakovich 的文章 检查数据访问应用程序块

此外,如代码所示,若要动态加载用户控件,需要使用 LoadControl(UserControlPath) 方法,而不是创建 User Control 类的新实例。 若要更深入地讨论原因(包括深入了解用户控件),请务必阅读 用户控件的广泛检查

接下来,在 Page_Load 事件处理程序中,从数据库中检索并循环访问客户对动态控件的当前答案。 引用相应的动态控件,并将其 Answer 属性设置为数据库中的答案。 这仅在第一页访问时完成,而不是在后续回发时完成,因为我们不希望覆盖用户为这些表单字段之一输入的值。

'Get the answers for this customer
'Get the list of dynamic controls for the specified customer
Dim reader As SqlDataReader = _
       SqlHelper.ExecuteReader(connectionString, _
               CommandType.StoredProcedure, _
               "dq_GetDynamicAnswersForCustomer", _
               New SqlParameter("@CustomerID", CustomerID))

While reader.Read
    Dim dq As IUIBuildingBlock = dynamicControls.FindControl(String.Concat("dq", reader("DynamicQuestionID")))
    If Not dq Is Nothing Then
        Select Case dq.DataType
            Case DQDataTypes.String
                dq.Answer = reader("StringAnswer").ToString()
            Case DQDataTypes.Integer
                dq.Answer = Convert.ToInt32(reader("IntegerAnswer"))
            Case DQDataTypes.Double
                dq.Answer = Convert.ToSingle(reader("DoubleAnswer"))
            Case DQDataTypes.Date
                dq.Answer = Convert.ToDateTime(reader("DateAnswer"))
            Case DQDataTypes.Currency
                dq.Answer = Convert.ToDecimal(reader("CurrencyAnswer"))
            Case DQDataTypes.Boolean
                dq.Answer = Convert.ToBoolean(reader("BooleanAnswer"))
        End Select
    End If
End While

最后,当用户单击“ 保存” 按钮时, dynamicControls 将枚举 PlaceHolder 的 Controls 集合,并且对于已回答的每个动态添加的控件,答案将写回到数据库。

'Create the needed parameters
Dim stringParam As New SqlParameter("@StringAnswer", SqlDbType.NText)
Dim integerParam As New SqlParameter("@IntegerAnswer", SqlDbType.Int)
Dim doubleParam As New SqlParameter("@DoubleAnswer", SqlDbType.Decimal)
Dim dateParam As New SqlParameter("@DateAnswer", SqlDbType.DateTime)
Dim currencyParam As New SqlParameter("@CurrencyAnswer", SqlDbType.Money)
Dim booleanParam As New SqlParameter("@BooleanAnswer", SqlDbType.Bit)

'Enumerate each answer and save it back to the database
For Each c As Control In dynamicControls.Controls
    If TypeOf c Is IUIBuildingBlock Then
        'Mark all of the parameters as NULL
        stringParam.Value = DBNull.Value : integerParam.Value = DBNull.Value
        doubleParam.Value = DBNull.Value : dateParam.Value = DBNull.Value
        currencyParam.Value = DBNull.Value : booleanParam.Value = DBNull.Value

        'Determine which parameter needs to be set
        Dim uib as IUIBuildingBlock = CType(c, IUIBuildingBlock)
        Select Case uib.DataType
            Case DQDataTypes.String
                stringParam.Value = uib.Answer
            Case DQDataTypes.Integer
                integerParam.Value = uib.Answer
            Case DQDataTypes.Double
                doubleParam.Value = uib.Answer
            Case DQDataTypes.Date
                dateParam.Value = uib.Answer
            Case DQDataTypes.Currency
                currencyParam.Value = uib.Answer
            Case DQDataTypes.Boolean
                booleanParam.Value = uib.Answer
        End Select

        Dim dynamicQuestionID As Integer = Convert.ToInt32(c.ID.Substring(2))
        SqlHelper.ExecuteReader(connectionString, _
               CommandType.StoredProcedure, "dq_AddDynamicAnswer", _
               New SqlParameter("@CustomerID", CustomerID), _
               New SqlParameter("@DynamicQuestionID", dynamicQuestionID), _
               stringParam, integerParam, doubleParam, _
               dateParam, currencyParam, booleanParam)
    End If
Next

关闭评论

动态数据输入用户界面引擎是针对 Web 应用程序开发此类系统的良好起点,但并未设计为提供与现有系统的无缝集成。 它被设计为一个示范,而不是一个完整的工作系统。 系统未完全完成的一个部分是管理界面,虽然功能正常,但远不是一个完整的系统。 具体而言,存在如何处理从特定客户类型中删除动态问题的问题。 例如,假设管理员配置了系统,以便仅联机客户被问及一个布尔值问题,“这是你从我们公司购买的第一个产品吗?”

现在,假设有一些客户回答了这个问题。 如果管理员决定从仅联机用户的问题集中删除此问题,会发生什么情况? 是否应从 dq_DynamicAnswers 表中删除相应的答案? 是否应该保存它们,以便提供过去答案的历史视图? 你必须为应用程序确定此问题的答案。 现在,当你删除自定义类型的动态问题时,管理界面不会执行任何操作,这意味着如果你有一个或多个客户回答了该问题,你将收到一个异常,并且不会删除该问题,因为这样做会违反数据库中建立的引用完整性。

结论

本文介绍了如何利用 ASP.NET 中的动态控件来创建动态数据输入用户界面。 如本文前半部分所述,ASP.NET 页由控件层次结构组成,该层次结构通常严格由静态定义的控件组成。 但是,在运行时,我们可以通过将动态控件添加到 Controls 层次结构中现有控件的集合来操作此控件层次结构。 我们还了解了用于访问动态添加的控件的技术,以及用于添加这些控件并与之交互的常见模式。

本文的后半部分介绍了用于创建和使用动态数据输入用户界面的特定实现。 检查的引擎允许非技术用户基于用户界面构建基块轻松创建问题,这些构建基块 ASP.NET 由开发人员创建的用户控件。 有了这些问题,这些非技术管理用户可以将一组问题与特定的客户类型相关联。 单个网页 EnterData.aspx根据访问页面的客户显示并保存相应的数据输入表单字段和值。

能够在运行时操作 ASP.NET 页的控件层次结构是一种功能强大且有用的工具,可在许多常见方案中使用应用程序。 通过本文,你应该能够自信地在 ASP.NET 页面中使用动态控件。

编程愉快!

特别感谢...

在将我的文章提交到 MSDN 编辑器之前,我让一些志愿者帮助校对文章,并提供有关文章内容、语法和方向的反馈。 本文审查过程的主要贡献者包括 米兰·内戈万马尔科·兰格尔、希尔顿·吉塞诺、 卡洛斯·桑托斯戴夫·唐纳森和卡尔·兰布雷希特。 如果你有兴趣加入不断增长的审阅者列表,请在 上给我添加一行 mitchell@4guysfromrolla.com

斯科特·米切尔 是六本书的作者,4GuysFromRolla.com 的创始人,也是一个全能的家伙。 自 1998 年以来,他一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 可以通过或他的博客http://ScottOnWriting.NET联系mitchell@4guysfromrolla.com他。

© Microsoft Corporation. 保留所有权利。