存储其他用户信息 (C#)

作者 :Scott Mitchell

注意

自本文撰写以来,ASP.NET 成员资格提供程序已被 ASP.NET Identity 取代。 强烈建议更新应用以使用 ASP.NET 标识 平台,而不是本文撰写时介绍的成员资格提供程序。 ASP.NET 标识比 ASP.NET 成员身份系统具有许多优势,包括:

  • 性能更好
  • 改进了可扩展性和可测试性
  • 支持 OAuth、OpenID Connect 和双因素身份验证
  • 基于声明的标识支持
  • 更好地与 ASP.Net Core 的互操作性

下载代码下载 PDF

在本教程中,我们将通过构建一个非常基本的留言簿应用程序来回答这个问题。 在此过程中,我们将查看用于对数据库中的用户信息进行建模的不同选项,然后了解如何将此数据与成员资格框架创建的用户帐户相关联。

简介

Asp。NET 的成员资格框架提供了用于管理用户的灵活界面。 成员资格 API 包括用于验证凭据、检索有关当前登录用户的信息、创建新用户帐户和删除用户帐户等的方法。 成员资格框架中的每个用户帐户仅包含验证凭据和执行与用户帐户相关的基本任务所需的属性。 类的方法和属性MembershipUser证明了这一点,类在 Membership 框架中为用户帐户建模。 此类具有 、 和 IsLockedOutUserNameEmail属性,以及 和 UnlockUserGetPassword方法。

通常,应用程序需要存储成员资格框架中未包括的其他用户信息。 例如,在线零售商可能需要让每个用户存储其送货地址和帐单邮寄地址、付款信息、送货首选项和联系电话号码。 此外,系统中的每个订单都与特定的用户帐户相关联。

MembershipUser不包括 或 DeliveryPreferencesPhoneNumberPastOrders属性。 那么,我们如何跟踪应用程序所需的用户信息并将其与成员资格框架集成? 在本教程中,我们将通过构建一个非常基本的留言簿应用程序来回答这个问题。 在此过程中,我们将查看用于对数据库中的用户信息进行建模的不同选项,然后了解如何将此数据与成员资格框架创建的用户帐户相关联。 让我们开始吧!

步骤 1:创建 Guestbook 应用程序的数据模型

可以使用多种技术来捕获数据库中的用户信息,并将其与成员资格框架创建的用户帐户相关联。 为了说明这些技术,我们需要扩充教程 Web 应用程序,以便它捕获某种与用户相关的数据。 (目前,应用程序的数据模型仅 SqlMembershipProvider包含 .)

让我们创建一个非常简单的留言簿应用程序,经过身份验证的用户可在其中发表评论。 除了存储留言簿评论之外,还允许每个用户存储其家乡、主页和签名。 如果提供,用户的家乡、主页和签名将显示在他在留言簿中留下的每条消息上。

GuestbookComments添加表

为了捕获留言簿注释,我们需要创建一个名为 GuestbookComments 的数据库表,其中包含 、SubjectBodyCommentDateCommentId列。 我们还需要让表中的每条记录 GuestbookComments 引用留下注释的用户。

若要将此表添加到数据库,请转到 Visual Studio 中的“数据库资源管理器”并向下钻取到 SecurityTutorials 数据库。 右键单击“表”文件夹,然后选择“添加新表”。 这将显示一个接口,该接口允许我们定义新表的列。

向 SecurityTutorials 数据库添加新表

图 1:向数据库添加新表 SecurityTutorials (单击以查看全尺寸图像)

接下来,定义 GuestbookComments的 列。 首先添加类型为 的uniqueidentifierCommentId。 此列将唯一标识留言簿中的每个批注,因此禁止 NULL ,并将其标记为表的主键。 我们可以通过将列的CommentId默认值设置为 来指示应为此字段INSERT自动生成一个新值,而不是在每个 INSERT上提供一个uniqueidentifierNEWID()。 添加第一个字段,将其标记为主键并设置其默认值后,屏幕应类似于图 2 所示的屏幕截图。

添加名为 CommentId 的主列

图 2:添加名为 CommentId 的主列 (单击以查看全尺寸图像)

接下来,添加类型nvarchar(50)为 的Subject列和类型 nvarchar(MAX)为 的列BodyNULL禁止在这两列中使用 。 之后,添加类型为 的datetimeCommentDate。 禁用 NULL ,并将 CommentDate 列的默认值设置为 getdate()

剩下的就是添加一个列,将用户帐户与每个留言簿注释相关联。 一种选择是添加类型nvarchar(256)为 的列UserName。 使用成员资格提供程序(而不是 )时, SqlMembershipProvider这是一个合适的选择。 但在使用 时,SqlMembershipProvider正如我们在本系列教程中一样,表中的UserNameaspnet_Users列不一定是唯一的。 表 aspnet_Users 的主键是 UserId ,并且 的类型 uniqueidentifier为 。 因此,表 GuestbookComments 需要一个名为 UserId 的列类型 uniqueidentifier (不允许 NULL 值) 。 继续并添加此列。

注意

在 SQL Server 中创建成员资格架构教程中所述,成员资格框架旨在使具有不同用户帐户的多个 Web 应用程序能够共享同一个用户存储。 它通过将用户帐户分区到不同的应用程序来执行此操作。 虽然保证每个用户名在应用程序中是唯一的,但同一用户名可以在使用同一用户存储的不同应用程序中使用。 和 字段的 aspnet_Users 表中UserNameApplicationId有一个复合约束,但不只是 字段有UserName一个复合UNIQUE约束。 因此,aspnet_Users表可能会有两个 (或更多个具有相同 UserName 值的) 记录。 但是,UNIQUE表的UserId字段 (存在约束aspnet_Users,因为它是主键) 。 约束UNIQUE很重要,因为没有约束,就无法在 和 aspnet_Users 表之间建立GuestbookComments外键约束。

添加 UserId 列后,单击工具栏中的“保存”图标保存表。 将新表 GuestbookComments命名为 。

表的最后一个问题GuestbookComments是:需要在列和 aspnet_Users.UserId 列之间GuestbookComments.UserId创建外键约束。 为此,请单击工具栏中的“关系”图标以启动“外键关系”对话框。 (或者,可以转到“表Designer”菜单并选择“关系”来启动此对话框)

单击“外键关系”对话框左下角的“添加”按钮。 这将添加新的外键约束,尽管我们仍然需要定义参与关系的表。

使用“外键关系”对话框管理表的外键约束

图 3:使用“外键关系”对话框管理表的外键约束 (单击以查看全尺寸图像)

接下来,单击右侧“表和列规范”行中的省略号图标。 这将启动“表和列”对话框,我们可以从中指定主键表和列以及表中 GuestbookComments 的外键列。 具体而言,选择 aspnet_UsersUserId 作为主键表和列,并从 UserId 表中 GuestbookComments 选择 作为外键列 (请参阅图 4) 。 定义主键表和外键表和列后,单击“确定”返回到“外键关系”对话框。

在 aspnet_Users 和 GuesbookComments 表之间建立外键约束

图 4:在 和 GuesbookComments 表之间建立aspnet_Users外键约束 (单击以查看全尺寸图像)

此时已建立外键约束。 此约束的存在通过保证永远不会有引用不存在用户帐户的 guestbook 条目来确保两个表之间的关系 完整性 。 默认情况下,如果存在相应的子记录,外键约束将禁止删除父记录。 也就是说,如果用户创建一个或多个留言簿注释,然后我们尝试删除该用户帐户,除非先删除他的留言簿注释,否则删除将失败。

可以将外键约束配置为在删除父记录时自动删除关联的子记录。 换句话说,我们可以设置此外键约束,以便在删除用户的用户帐户时自动删除用户的来宾簿条目。 若要完成此操作,请展开“INSERT 和 UPDATE 规范”部分,并将“删除规则”属性设置为 Cascade。

配置外键约束以级联删除

图 5:将外键约束配置为级联删除 (单击以查看全尺寸图像)

若要保存外键约束,请单击“关闭”按钮退出外键关系。 然后单击工具栏中的“保存”图标以保存表和此关系。

存储用户的家乡、主页和签名

GuestbookComments 表说明了如何存储与用户帐户共享一对多关系的信息。 由于每个用户帐户可能有任意数量的关联注释,因此通过创建一个表来保存一组注释,其中包含一个列,该列将每个注释链接到特定用户,从而对这种关系进行建模。 使用 时,SqlMembershipProvider最好通过创建类型uniqueidentifier为 的UserId列以及此列和 aspnet_Users.UserId之间的外键约束来建立此链接。

现在,我们需要将三列与每个用户帐户相关联,以存储用户的家乡、主页和签名,这些签名将显示在他的留言簿评论中。 有多种不同的方法可以实现此目的:

  • 将新列添加到aspnet_Usersaspnet_Membership表。 我不建议使用此方法,因为它修改 了 所使用的 SqlMembershipProvider架构。 这个决定可能会回来困扰你。 例如,如果 ASP.NET 的未来版本使用不同的 SqlMembershipProvider 架构,该怎么办。 Microsoft 可能包含将 ASP.NET 2.0 SqlMembershipProvider 数据迁移到新架构的工具,但如果修改了 ASP.NET 2.0 SqlMembershipProvider 架构,则可能无法进行此类转换。

  • 使用 ASP。NET 的配置文件框架,定义家乡、主页和签名的配置文件属性。 ASP.NET 包括用于存储其他用户特定数据的配置文件框架。 与成员资格框架一样,配置文件框架是在提供程序模型之上生成的。 .NET Framework附带一个 SqlProfileProvider ,用于将配置文件数据存储在SQL Server数据库中。 事实上,我们的数据库已有 (aspnet_Profile) 使用的SqlProfileProvider表,就像我们在创建成员资格架构教程中重新添加应用程序服务时添加的一样SQL Server表。
    配置文件框架main优点是,它允许开发人员在 中Web.config定义配置文件属性 - 无需编写任何代码即可将配置文件数据序列化到基础数据存储和从基础数据存储序列化。 简言之,定义一组配置文件属性并在代码中使用这些属性非常简单。 但是,在版本控制方面,配置文件系统留下很多需要的属性,因此,如果应用程序希望以后添加新的用户特定属性,或者删除或修改现有属性,则配置文件框架可能不是最佳选择。 此外, SqlProfileProvider 以高度非规范化的方式存储配置文件属性,使得几乎不可能直接针对配置文件数据 (运行查询,例如,有多少用户拥有纽约) 。
    有关配置文件框架的详细信息,请参阅本教程末尾的“进一步阅读”部分。

  • 将这三列添加到数据库中的新表中,并在该表与aspnet_Users 之间建立一对一关系.与使用配置文件框架相比,此方法涉及的工作要多一些,但在如何在数据库中建模其他用户属性方面提供了最大的灵活性。 这是我们将在本教程中使用的选项。

我们将创建一个名为 UserProfiles 的新表,用于保存每个用户的家乡、主页和签名。 右键单击“数据库资源管理器”窗口中的“表”文件夹,然后选择创建新表。 将第一列 UserId 命名为 ,并将其类型设置为 uniqueidentifier。 禁止 NULL 值并将列标记为主键。 接下来,添加名为 的列:HomeTown,类型 HomepageUrlnvarchar(50)nvarchar(100);类型为 ,类型为 的nvarchar(500)签名。 这三列中的每一列都可以接受一个 NULL 值。

创建 UserProfiles 表

图 6:创建 UserProfiles 表 (单击以查看全尺寸图像)

保存表并将其命名为 UserProfiles。 最后,在表的 UserId 字段和 aspnet_Users.UserId 字段之间建立UserProfiles外键约束。 正如我们对 和 aspnet_Users 表之间的GuestbookComments外键约束所做的那样, 让此约束级联删除。 由于 中的 UserIdUserProfiles 字段是主键,这可确保每个用户帐户的 UserProfiles 表中不会有多条记录。 这种类型的关系称为一对一关系。

创建数据模型后,即可使用它。 在步骤 2 和 3 中,我们将了解当前登录的用户如何查看和编辑其家乡、主页和签名信息。 在步骤 4 中,我们将为经过身份验证的用户创建界面,以便将新评论提交到留言簿并查看现有评论。

步骤 2:显示用户的家乡、主页和签名

有多种方法可让当前登录的用户查看和编辑其家乡、主页和签名信息。 可以使用 TextBox 和 Label 控件手动创建用户界面,也可以使用其中一个数据 Web 控件,例如 DetailsView 控件。 若要执行数据库 SELECTUPDATE 语句,我们可以在页面的代码隐藏类中编写 ADO.NET 代码,或者对 SqlDataSource 使用声明性方法。 理想情况下,应用程序将包含分层体系结构,我们可以从页面的代码隐藏类以编程方式调用该体系结构,或通过 ObjectDataSource 控件以声明方式调用该体系结构。

由于本教程系列重点介绍表单身份验证、授权、用户帐户和角色,因此不会全面讨论这些不同的数据访问选项,或者为何分层体系结构优先于直接从 ASP.NET 页执行 SQL 语句。 我将演练使用 DetailsView 和 SqlDataSource(最快速、最简单的选项),但讨论的概念当然可以应用于替代 Web 控件和数据访问逻辑。 有关在 ASP.NET 中使用数据的详细信息,请参阅我在 ASP.NET 2.0 中使用数据 教程系列。

打开 文件夹中的页面AdditionalUserInfo.aspxMembership,将 DetailsView 控件添加到页面,将其 ID 属性设置为 UserProfile ,并清除其 WidthHeight 属性。 展开 DetailsView 的智能标记,然后选择将其绑定到新的数据源控件。 这将启动数据源配置向导 (请参阅图 7) 。 第一步要求指定数据源类型。 由于我们要直接连接到 SecurityTutorials 数据库,因此请选择“数据库”图标,并将 ID 指定为 UserProfileDataSource

添加名为 UserProfileDataSource 的新 SqlDataSource 控件

图 7:添加名为 UserProfileDataSource 的新 SqlDataSource 控件 (单击以查看全尺寸图像)

下一个屏幕会提示数据库使用。 我们已在 中 Web.configSecurityTutorials 数据库定义了连接字符串。 此连接字符串名称 - SecurityTutorialsConnectionString - 应位于下拉列表中。 选择此选项并单击“下一步”。

从Drop-Down列表中选择 SecurityTutorialsConnectionString

图 8SecurityTutorialsConnectionString 从“Drop-Down列表”中选择 (单击以查看全尺寸图像)

后续屏幕要求我们指定要查询的表和列。 UserProfiles从下拉列表中选择表,然后检查所有列。

从 UserProfiles 表恢复所有列

图 9:恢复表中的所有列 UserProfiles (单击以查看全尺寸图像)

图 9 中的当前查询返回 中的所有UserProfiles记录,但我们只对当前登录的用户记录感兴趣。 若要添加 WHERE 子句,请单击 WHERE 按钮以显示“添加 WHERE 子句”对话框, (请参阅图 10) 。 可在此处选择要筛选的列、运算符和筛选器参数的源。 选择 作为列,选择 UserId “=”作为运算符。

遗憾的是,没有内置参数源可返回当前登录用户 UserId 的值。 我们需要以编程方式获取此值。 因此,将“源”下拉列表设置为“无”,单击“添加”按钮添加参数,然后单击“确定”。

在 UserId 列上添加 Filter 参数

图 10:在 UserId “列”上添加筛选器参数 (单击以查看全尺寸图像)

单击“确定”后,将返回到图 9 所示的屏幕。 但是,这一次,屏幕底部的 SQL 查询应包含 WHERE 子句。 单击“下一步”转到“测试查询”屏幕。 可以在此处运行查询并查看结果。 单击“完成”以完成向导。

完成 DataSource 配置向导后,Visual Studio 会基于向导中指定的设置创建 SqlDataSource 控件。 此外,它还手动将 BoundFields 添加到由 SqlDataSource 的 返回的每个列的 SelectCommandDetailsView。 无需在 DetailsView 中显示 UserId 字段,因为用户不需要知道此值。 可以直接从 DetailsView 控件的声明性标记中删除此字段,或者单击其智能标记中的“编辑字段”链接。

此时,页面的声明性标记应如下所示:

<asp:DetailsView ID="UserProfile" runat="server"
     AutoGenerateRows="False" DataKeyNames="UserId"
     DataSourceID="UserProfileDataSource">
     <Fields>
          <asp:BoundField DataField="HomeTown" HeaderText="HomeTown"
               SortExpression="HomeTown" />
          <asp:BoundField DataField="HomepageUrl" HeaderText="HomepageUrl"
               SortExpression="HomepageUrl" />
          <asp:BoundField DataField="Signature" HeaderText="Signature"
               SortExpression="Signature" />
     </Fields>
</asp:DetailsView>
<asp:SqlDataSource ID="UserProfileDataSource" runat="server"
          ConnectionString="<%$ ConnectionStrings:SecurityTutorialsConnectionString %>"
          SelectCommand="SELECT [UserId], [HomeTown], [HomepageUrl], [Signature] FROM
          [UserProfiles] WHERE ([UserId] = @UserId)">
     <SelectParameters>
          <asp:Parameter Name="UserId" Type="Object" />
     </SelectParameters>
</asp:SqlDataSource>

在选择数据之前,我们需要以编程方式将 SqlDataSource 控件的 UserId 参数设置为当前登录用户的 UserId 。 为此,可以为 SqlDataSource 的 Selecting 事件创建事件处理程序,并在其中添加以下代码:

protected void UserProfileDataSource_Selecting(object sender, 
          SqlDataSourceSelectingEventArgs e)
{
     // Get a reference to the currently logged on user
     MembershipUser currentUser = Membership.GetUser();
 
     // Determine the currently logged on user's UserId value
     Guid currentUserId = (Guid)currentUser.ProviderUserKey;
 
     // Assign the currently logged on user's UserId to the @UserId parameter
     e.Command.Parameters["@UserId"].Value = currentUserId;
}

上述代码首先通过调用 Membership 类的 GetUser 方法获取对当前登录用户的引用。 这将返回一个 MembershipUser 对象,其 ProviderUserKey 属性包含 UserIdUserId然后将该值分配给 SqlDataSource 的 @UserId 参数。

注意

方法 Membership.GetUser() 返回有关当前登录用户的信息。 如果匿名用户访问页面,它将返回 值 null。 在这种情况下,在尝试读取 ProviderUserKey 属性时,这将导致NullReferenceException以下代码行上的 。 当然,我们不必担心在AdditionalUserInfo.aspx页面中返回值,Membership.GetUser()因为我们在上一null教程中配置了 URL 授权,以便只有经过身份验证的用户才能访问此文件夹中的 ASP.NET 资源。 如果需要在允许匿名访问的页面中访问有关当前登录用户的信息,请确保检查,在引用其属性之前,GetUser()从 方法返回非null MembershipUser 对象。

如果通过浏览器访问页面, AdditionalUserInfo.aspx 你将看到一个空白页,因为我们尚未向 UserProfiles 表中添加任何行。 在步骤 6 中,我们将了解如何自定义 CreateUserWizard 控件,以在创建新用户帐户时自动将 UserProfiles 新行添加到表中。 但是,目前,我们需要在表中手动创建记录。

在 Visual Studio 中导航到“数据库资源管理器”并展开“表”文件夹。 右键单击表并选择 aspnet_Users “显示表数据”以查看表中的记录;对 UserProfiles 表执行相同操作。 图 11 显示了垂直平铺时的这些结果。 在我的数据库中,目前 aspnet_Users 有 Bruce、Fred 和 Tito 的记录,但表中没有记录 UserProfiles

显示aspnet_Users和 UserProfiles 表的内容

图 11:单击查看全尺寸图像 (显示 和 UserProfiles 表的内容aspnet_Users)

通过手动键入 、 HomepageUrlSignature 字段的值HomeTown,将新记录添加到UserProfiles表中。 在新记录中获取有效UserId值的最简单方法是从表中的特定用户帐户aspnet_Users中选择UserId字段,并将其复制并粘贴到 UserId 中的 UserProfiles字段中。UserProfiles 图 12 显示了 UserProfiles 为 Bruce 添加新记录后的表。

已将记录添加到 Bruce 的 UserProfiles

图 12:为 Bruce 添加了 UserProfiles 记录 (单击以查看全尺寸图像)

返回到 AdditionalUserInfo.aspx 以 Bruce 身份登录的页面。 如图 13 所示,显示了 Bruce 的设置。

显示当前正在访问的用户的设置

图 13:当前访问的用户显示其设置 (单击以查看全尺寸图像)

注意

继续操作,并在表中为每个成员身份用户手动添加记录 UserProfiles 。 在步骤 6 中,我们将了解如何自定义 CreateUserWizard 控件,以在创建新用户帐户时自动将 UserProfiles 新行添加到表中。

步骤 3:允许用户编辑其家乡、主页和签名

此时,当前登录的用户可以查看其家乡、主页和签名设置,但他们尚无法修改它们。 让我们更新 DetailsView 控件,以便可以编辑数据。

我们需要做的第一件事是为 SqlDataSource 添加 UpdateCommand ,指定要 UPDATE 执行的语句及其相应的参数。 选择 SqlDataSource,并从属性窗口单击 UpdateQuery 属性旁边的省略号,以显示“命令和参数编辑器”对话框。 在文本框中输入以下 UPDATE 语句:

UPDATE UserProfiles SET
     HomeTown = @HomeTown,
     HomepageUrl = @HomepageUrl,
     Signature = @Signature
WHERE UserId = @UserId

接下来,单击“刷新参数”按钮,这将在 SqlDataSource 控件的 UpdateParameters 集合中为 语句中的每个 UPDATE 参数创建一个参数。 保留所有参数的源设置为“无”,然后单击“确定”按钮完成对话框。

指定 SqlDataSource 的 UpdateCommand 和 UpdateParameters

图 14:指定 SqlDataSource 的 UpdateCommandUpdateParameters (单击以查看全尺寸图像)

由于我们对 SqlDataSource 控件进行了添加,DetailsView 控件现在可以支持编辑。 在 DetailsView 的智能标记中,检查“启用编辑”复选框。 这会将 CommandField 添加到控件的 Fields 集合,其 ShowEditButton 属性设置为 True。 在只读模式下显示 DetailsView 时,将呈现“编辑”按钮,在编辑模式下显示“更新”和“取消”按钮。 不过,通过将 DetailsView 控件的 属性Edit设置为 ,我们可以使 DetailsView 呈现为“始终可编辑”状态,DefaultMode而无需用户单击“编辑”。

进行这些更改后,DetailsView 控件的声明性标记应如下所示:

<asp:DetailsView ID="UserProfile" runat="server"
          AutoGenerateRows="False" DataKeyNames="UserId"
          DataSourceID="UserProfileDataSource" DefaultMode="Edit">
     <Fields>
          <asp:BoundField DataField="HomeTown" HeaderText="HomeTown"
               SortExpression="HomeTown" />
          <asp:BoundField DataField="HomepageUrl" HeaderText="HomepageUrl"
               SortExpression="HomepageUrl" />
          <asp:BoundField DataField="Signature" HeaderText="Signature"
               SortExpression="Signature" />
          <asp:CommandField ShowEditButton="True" />
     </Fields>
</asp:DetailsView>

请注意 CommandField 和 属性的 DefaultMode 添加。

继续通过浏览器测试此页面。 当访问的用户在 中 UserProfiles具有相应记录时,用户的设置将显示在可编辑的界面中。

DetailsView 呈现可编辑的界面

图 15:DetailsView 呈现可编辑的界面 (单击以查看全尺寸图像)

尝试更改值并单击“更新”按钮。 似乎什么也没发生。 存在回发,值已保存到数据库,但没有发生保存的视觉反馈。

若要解决此问题,请返回到 Visual Studio 并在 DetailsView 上方添加一个 Label 控件。 将其 ID 设置为 ,其 Text 属性设置为“你的设置已更新”,将其 VisibleEnableViewState 属性设置为 falseSettingsUpdatedMessage

<asp:Label ID="SettingsUpdatedMessage" runat="server"
     Text="Your settings have been updated."
     EnableViewState="false"
     Visible="false"></asp:Label>

每当更新 DetailsView 时,都需要显示 SettingsUpdatedMessage 标签。 为此,请为 DetailsView 的 ItemUpdated 事件创建事件处理程序,并添加以下代码:

protected void UserProfile_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
{
     SettingsUpdatedMessage.Visible = true;
}

通过浏览器返回到 AdditionalUserInfo.aspx 页面并更新数据。 此时会显示一条有用的状态消息。

更新设置时显示一条短消息

图 16:更新设置时显示短消息 (单击以查看全尺寸图像)

注意

DetailsView 控件的编辑界面需要很多。 它使用标准大小的文本框,但“签名”字段可能是多行文本框。 应使用 RegularExpressionValidator 来确保主页 URL(如果输入)以“http://”或“https://”开头。 此外,由于 DetailsView 控件的 DefaultMode 属性设置为 Edit,因此“取消”按钮不执行任何操作。 应将其删除,或者在单击时将用户重定向到其他页面 (,例如 ~/Default.aspx) 。 我将这些增强功能保留为读者练习。

目前,该网站不提供指向该 AdditionalUserInfo.aspx 页面的任何链接。 访问它的唯一方法是直接在浏览器的地址栏中输入页面的 URL。 让我们在 Site.master 母版页中添加指向此页的链接。

回想一下,母版页在其 LoginContent ContentPlaceHolder 中包含 LoginView Web 控件,该控件为经过身份验证的访问者和匿名访问者显示不同的标记。 更新 LoginView 控件的 LoggedInTemplate 以包含指向 AdditionalUserInfo.aspx 页面的链接。 进行这些更改后,LoginView 控件的声明性标记应如下所示:

<asp:LoginView ID="LoginView1" runat="server">
     <LoggedInTemplate>
          Welcome back,
          <asp:LoginName ID="LoginName1" runat="server" />.
          <br />
          <asp:HyperLink ID="lnkUpdateSettings" runat="server" 
               NavigateUrl="~/Membership/AdditionalUserInfo.aspx">
               Update Your Settings</asp:HyperLink>
     </LoggedInTemplate>
     <AnonymousTemplate>
          Hello, stranger.
     </AnonymousTemplate>
</asp:LoginView>

请注意,将 HyperLink 控件添加到 lnkUpdateSettingsLoggedInTemplate 通过此链接,经过身份验证的用户可以快速跳转到页面以查看和修改其家乡、主页和签名设置。

步骤 4:添加新的留言簿注释

通过 Guestbook.aspx 身份验证的用户可在该页中查看留言簿并留下评论。 让我们从创建接口开始,以添加新的留言簿注释。

Guestbook.aspx在 Visual Studio 中打开页面并构造一个由两个 TextBox 控件组成的用户界面,一个用于新批注的主题,一个用于其正文。 将第一个 TextBox 控件的 ID 属性设置为 Subject ,将其 Columns 属性设置为 40;将第二BodyID个的 设置为 ,将其 TextMode 设置为 MultiLine,将其 WidthRows 属性分别设置为“95%”和 8。 若要完成用户界面,请添加一个名为 PostCommentButton 的按钮 Web 控件,并将其 Text 属性设置为“发布您的评论”。

由于每个留言簿注释都需要主题和正文,因此请为每个 TextBox 添加 RequiredFieldValidator。 ValidationGroup将这些控件的 属性设置为“EnterComment”;同样,将PostCommentButton控件的 ValidationGroup 属性设置为“EnterComment”。 有关 ASP 的详细信息。NET 的验证控件在 ASP.NET 中检查表单验证

创建用户界面后,页面的声明性标记应如下所示:

<h3>Leave a Comment</h3>
<p>
     <b>Subject:</b>
     <asp:RequiredFieldValidator ID="SubjectReqValidator" runat="server"
          ErrorMessage="You must provide a value for Subject"
          ControlToValidate="Subject" ValidationGroup="EnterComment">
     </asp:RequiredFieldValidator><br/>
     <asp:TextBox ID="Subject" Columns="40" runat="server"></asp:TextBox>
</p>
<p>
     <b>Body:</b>
     <asp:RequiredFieldValidator ID="BodyReqValidator" runat="server"
          ControlToValidate="Body"
          ErrorMessage="You must provide a value for Body" ValidationGroup="EnterComment">
     </asp:RequiredFieldValidator><br/>
     <asp:TextBox ID="Body" TextMode="MultiLine" Width="95%"
          Rows="8" runat="server"></asp:TextBox>
</p>
<p>
     <asp:Button ID="PostCommentButton" runat="server" 
          Text="Post Your Comment"
          ValidationGroup="EnterComment" />
</p>

用户界面完成后,下一个任务是在单击 时PostCommentButtonGuestbookComments新记录插入表中。 这可以通过多种方式实现:我们可以在 Button 的Click事件处理程序中编写 ADO.NET 代码;我们可以将 SqlDataSource 控件添加到页面,配置其 InsertCommand,然后从Click事件处理程序调用其 Insert 方法;或者我们可以生成一个负责插入新来宾簿注释的中间层,并从事件处理程序调用此功能Click。 由于我们在步骤 3 中介绍了如何使用 SqlDataSource,因此让我们在此处使用 ADO.NET 代码。

注意

用于以编程方式访问 Microsoft SQL Server 数据库中数据的 ADO.NET 类位于 命名空间中System.Data.SqlClient。 可能需要将此命名空间导入页面的代码隐藏类 (,即 using System.Data.SqlClient;) 。

PostCommentButtonClick 事件创建事件处理程序,并添加以下代码:

protected void PostCommentButton_Click(object sender, EventArgs e)
{
     if (!Page.IsValid)
          return;
 
     // Determine the currently logged on user's UserId
     MembershipUser currentUser = Membership.GetUser();
     Guid currentUserId = (Guid)currentUser.ProviderUserKey;
 
     // Insert a new record into GuestbookComments
     string connectionString = 
          ConfigurationManager.ConnectionStrings["SecurityTutorialsConnectionString"].ConnectionString;
     string insertSql = "INSERT INTO GuestbookComments(Subject, Body, UserId) VALUES(@Subject,
               @Body, @UserId)";
 
     using (SqlConnection myConnection = new SqlConnection(connectionString))
     {
          myConnection.Open();
          SqlCommand myCommand = new SqlCommand(insertSql, myConnection);
          myCommand.Parameters.AddWithValue("@Subject", Subject.Text.Trim());
          myCommand.Parameters.AddWithValue("@Body", Body.Text.Trim());
          myCommand.Parameters.AddWithValue("@UserId", currentUserId);
          myCommand.ExecuteNonQuery();
          myConnection.Close();
     }
 
     // "Reset" the Subject and Body TextBoxes
     Subject.Text = string.Empty;
     Body.Text = string.Empty;
}

事件处理程序 Click 首先检查用户提供的数据是否有效。 否则,事件处理程序在插入记录之前退出。 假设提供的数据有效,将检索当前登录用户 UserId 的值并将其存储在局部变量中 currentUserId 。 需要此值,因为在将记录插入 时GuestbookComments必须提供UserId值。

然后,将从 Web.config 中检索数据库的连接字符串SecurityTutorialsINSERT并指定 SQL 语句。 SqlConnection然后创建并打开 对象。 接下来,构造 一个 SqlCommand 对象,并分配查询中使用的 INSERT 参数的值。 INSERT然后执行 语句并关闭连接。 在事件处理程序的末尾, Subject 将清除 和 Body TextBoxes 的属性 Text ,以便不会在回发中保留用户的值。

继续在浏览器中测试此页面。 由于此页面位于 Membership 文件夹中,匿名访问者无法访问它。 因此,如果尚未) ,则需要首先登录 (。 在 SubjectBody TextBox 中输入一个值,然后单击按钮 PostCommentButton 。 这将导致将新记录添加到 GuestbookComments。 回发时,将从 TextBox 中擦除你提供的主题和正文。

单击 PostCommentButton 该按钮后,没有将注释添加到留言簿的视觉反馈。 我们仍然需要更新此页面以显示现有的留言簿注释,我们将在步骤 5 中执行此操作。 完成此操作后,刚添加的注释将显示在批注列表中,提供足够的视觉反馈。 现在,请通过检查表的内容 GuestbookComments 来确认是否已保存您的留言簿注释。

图 17 显示了留下两个注释后表的内容 GuestbookComments

可以在 GuestbookComments 表中查看 Guestbook 注释

图 17:可以在表中看到留言簿注释 GuestbookComments (单击以查看全尺寸图像)

注意

如果用户尝试插入包含潜在危险标记(如 HTML)的留言簿注释,ASP.NET 将引发 HttpRequestValidationException。 若要详细了解此异常、引发异常的原因以及如何允许用户提交潜在危险值,请参阅 请求验证白皮书

步骤 5:列出现有来宾簿注释

除了留下评论外,访问 Guestbook.aspx 页面的用户还应能够查看留言簿的现有批注。 为此,请在页面底部添加名为 的 CommentList ListView 控件。

注意

ListView 控件是新 ASP.NET 版本 3.5。 它旨在以非常可自定义和灵活的布局显示项列表,但仍提供内置的编辑、插入、删除、分页和排序功能,如 GridView。 如果使用 ASP.NET 2.0,则需要改用 DataList 或 Repeater 控件。 有关使用 ListView 的详细信息,请参阅 Scott Guthrie 的博客文章 Asp:ListView 控件和我的文章 “使用 ListView 控件显示数据”。

打开 ListView 的智能标记,然后从“选择数据源”下拉列表中将控件绑定到新的数据源。 正如我们在步骤 2 中看到的,这将启动数据源配置向导。 选择“数据库”图标,将生成的 SqlDataSource CommentsDataSource命名为 ,然后单击“确定”。 接下来,从下拉列表中选择 SecurityTutorialsConnectionString 连接字符串,然后单击“下一步”。

此时,在步骤 2 中,我们通过从下拉列表中选取 UserProfiles 表并选择要返回的列来指定要查询的数据 (参考图 9) 。 但是,这一次,我们希望创建一个 SQL 语句,该语句不仅从 中拉回记录,还从 GuestbookComments注释者的家乡、主页、签名和用户名中拉回记录。 因此,选择“指定自定义 SQL 语句或存储过程”单选按钮,然后单击“下一步”。

此时会显示“定义自定义语句或存储过程”屏幕。 单击“查询生成器”按钮以图形方式生成查询。 查询生成器首先提示我们指定要查询的表。 GuestbookComments选择 、 UserProfilesaspnet_Users 表,然后单击“确定”。 这会将所有三个表添加到设计图面。 由于 、 UserProfilesaspnet_Users 表之间存在GuestbookComments外键约束,因此查询生成器会自动JOIN创建这些表。

剩下的就是指定要返回的列。 GuestbookComments从表中选择 SubjectBodyCommentDate 列;从UserProfiles表中返回 HomeTownHomepageUrlSignature 列;从 aspnet_Users返回 UserName 。 此外,在查询末尾SELECT添加“ORDER BY CommentDate DESC”,以便首先返回最新的帖子。 进行这些选择后,查询生成器界面应类似于图 18 中的屏幕截图。

GuestbookComments、UserProfiles 和 aspnet_Users 表的构造查询 JOIN

图 18:构造的查询 JOIN 包含 GuestbookCommentsUserProfilesaspnet_Users 表 (单击以查看全尺寸图像)

单击“确定”关闭“查询生成器”窗口并返回到“定义自定义语句或存储过程”屏幕。 单击“下一步”转到“测试查询”屏幕,单击“测试查询”按钮可以查看查询结果。 准备就绪后,单击“完成”以完成“配置数据源”向导。

在步骤 2 中完成“配置数据源”向导时,关联的 DetailsView 控件的 Fields 集合已更新,以包含由 返回的每个列的 SelectCommandBoundField。 但是,ListView 保持不变:我们仍然需要定义其布局。 ListView 的布局可以通过其声明性标记或从其智能标记中的“配置 ListView”选项手动构造。 我通常更喜欢手动定义标记,但使用最自然的方法。

我最终将以下 LayoutTemplateItemTemplateItemSeparatorTemplate 用于我的 ListView 控件:

<asp:ListView ID="CommentList" runat="server" DataSourceID="CommentsDataSource">
     <LayoutTemplate>
          <span ID="itemPlaceholder" runat="server" />
          <p>
               <asp:DataPager ID="DataPager1" runat="server">
                    <Fields>
                         <asp:NextPreviousPagerField ButtonType="Button" 
                              ShowFirstPageButton="True"
                              ShowLastPageButton="True" />
                    </Fields>
               </asp:DataPager>
          </p>
     </LayoutTemplate>
     <ItemTemplate>
          <h4><asp:Label ID="SubjectLabel" runat="server" 
               Text='<%# Eval("Subject") %>' /></h4>
          <asp:Label ID="BodyLabel" runat="server" 
               Text='<%# Eval("Body").ToString().Replace(Environment.NewLine, "<br />") %>' />
          <p>
               ---<br />
               <asp:Label ID="SignatureLabel" Font-Italic="true" runat="server"
                    Text='<%# Eval("Signature") %>' />
               <br />
               <br />
               My Home Town:
               <asp:Label ID="HomeTownLabel" runat="server" 
                    Text='<%# Eval("HomeTown") %>' />
               <br />
               My Homepage:
               <asp:HyperLink ID="HomepageUrlLink" runat="server" 
                    NavigateUrl='<%# Eval("HomepageUrl") %>' 
                    Text='<%# Eval("HomepageUrl") %>' />
          </p>
          <p align="center">
               Posted by
               <asp:Label ID="UserNameLabel" runat="server" 
                    Text='<%# Eval("UserName") %>' /> on
               <asp:Label ID="CommentDateLabel" runat="server" 
                    Text='<%# Eval("CommentDate") %>' />
          </p>
     </ItemTemplate>
     <ItemSeparatorTemplate>
          <hr />
     </ItemSeparatorTemplate>
</asp:ListView>

LayoutTemplate定义控件发出的标记,而 ItemTemplate 呈现 SqlDataSource 返回的每个项。 ItemTemplate的 生成的标记放置在 的 itemPlaceholder 控件中LayoutTemplate。 除了 itemPlaceholderLayoutTemplate 还包括一个 DataPager 控件,该控件将 ListView 限制为仅显示每页 10 个留言簿注释, (默认) 并呈现分页界面。

“我的 ItemTemplate ”在元素中 <h4> 显示每个留言簿注释的主题,其正文位于主题下方。 请注意,用于显示正文的语法采用数据绑定语句返回 Eval("Body") 的数据,将其转换为字符串,并将换行符 <br /> 替换为 元素。 需要此转换才能显示提交注释时输入的换行符,因为 HTML 会忽略空格。 用户的签名以斜体显示在正文下方,后跟用户的家乡、指向其主页的链接、发表评论的日期和时间以及留下批注的人员的用户名。

花点时间通过浏览器查看页面。 应在此处显示步骤 5 中添加到留言簿的批注。

Guestbook.aspx 现在显示 Guestbook 的注释

图 19Guestbook.aspx 现在显示留言簿的注释 (单击以查看全尺寸图像)

尝试将新批注添加到留言簿。 单击 PostCommentButton 按钮后,页面会回发,注释将添加到数据库,但 ListView 控件不会更新以显示新批注。 可以通过以下任一方法修复此问题:

  • 更新 PostCommentButton 按钮的 Click 事件处理程序,使其在将新注释插入数据库后调用 ListView 控件 DataBind() 的 方法,或者
  • 将 ListView 控件的 EnableViewState 属性设置为 false。 此方法之所以有效,是因为通过禁用控件的视图状态,它必须在每次回发时重新绑定到基础数据。

本教程可下载的教程网站说明了这两种方法。 ListView 控件的 EnableViewState 属性和 false 以编程方式将数据重新绑定到 ListView 所需的代码存在于事件处理程序中 Click ,但被注释掉。

注意

目前,该 AdditionalUserInfo.aspx 页面允许用户查看和编辑其家乡、主页和签名设置。 最好进行更新 AdditionalUserInfo.aspx 以显示已登录用户的留言簿注释。 也就是说,除了检查和修改她的信息外,用户还可以访问该 AdditionalUserInfo.aspx 页面,查看她过去对留言簿的评论。 我留给感兴趣的读者练习。

步骤 6:自定义 CreateUserWizard 控件以包括主页、主页和签名的接口

SELECT页面使用的Guestbook.aspx查询使用 来INNER JOIN合并 、 UserProfilesaspnet_Users 表之间的GuestbookComments相关记录。 如果在 中UserProfiles没有记录的用户进行留言簿注释,则批注将不会显示在 ListView 中,因为 INNER JOIN 当 和 aspnet_UsersUserProfiles存在匹配的记录时,仅返回GuestbookComments记录。 正如我们在步骤 3 中看到的,如果用户没有记录, UserProfiles 她无法在页面中查看或编辑她的设置 AdditionalUserInfo.aspx

不用说,由于我们的设计决策,成员资格系统中的每个用户帐户在表中都有匹配的 UserProfiles 记录非常重要。 我们希望在通过 CreateUserWizard 创建新的成员身份用户帐户时,将相应的记录添加到 UserProfiles 其中。

创建用户帐户 教程中所述,在创建新的成员身份用户帐户后,CreateUserWizard 控件将引发其 CreatedUser 事件。 我们可以为此事件创建事件处理程序,获取刚刚创建的用户的 UserId,然后在表中插入具有 、 和 Signature 列的默认值HomeTown的记录UserProfilesHomepageUrl 此外,还可以通过自定义 CreateUserWizard 控件的界面来提示用户输入这些值,以包含其他 TextBox。

首先,让我们看看如何使用默认值向事件处理程序中的UserProfilesCreatedUser表添加新行。 之后,我们将了解如何自定义 CreateUserWizard 控件的用户界面,以包含其他表单字段来收集新用户的家乡、主页和签名。

将默认行添加到UserProfiles

创建用户帐户教程中,我们向 文件夹中的页面CreatingUserAccounts.aspxMembership添加了 CreateUserWizard 控件。 为了使 CreateUserWizard 控件在创建用户帐户时将记录添加到 UserProfiles 表中,我们需要更新 CreateUserWizard 控件的功能。 与其对 CreatingUserAccounts.aspx 页面进行这些更改,不如向页面添加新的 CreateUserWizard 控件 EnhancedCreateUserWizard.aspx ,并在其中对本教程进行修改。

EnhancedCreateUserWizard.aspx在 Visual Studio 中打开页面,并将“工具箱”中的 CreateUserWizard 控件拖到该页上。 将 CreateUserWizard 控件的 ID 属性设置为 NewUserWizard。 正如我们在 创建用户帐户 教程中所述,CreateUserWizard 的默认用户界面会提示访问者提供必要信息。 提供此信息后,控件会在成员资格框架中内部创建新的用户帐户,无需编写一行代码。

CreateUserWizard 控件在其工作流期间引发大量事件。 访问者提供请求信息并提交表单后,CreateUserWizard 控件最初会触发其 CreatingUser 事件。 如果在创建过程中出现问题,则会CreateUserError触发事件;但是,如果成功创建用户,则会引发CreatedUser事件创建用户帐户教程中,我们为事件创建了一个事件处理程序CreatingUser,以确保提供的用户名不包含任何前导或尾随空格,并且用户名未出现在密码中的任何位置。

若要在 UserProfiles 表中为刚刚创建的用户添加行,我们需要为 CreatedUser 事件创建事件处理程序。 触发事件时 CreatedUser ,已在成员资格框架中创建用户帐户,使我们能够检索帐户的 UserId 值。

NewUserWizard的事件 CreatedUser 创建事件处理程序,并添加以下代码:

protected void NewUserWizard_CreatedUser(object sender, EventArgs e)
{
     // Get the UserId of the just-added user
     MembershipUser newUser = Membership.GetUser(NewUserWizard.UserName);
     Guid newUserId = (Guid)newUser.ProviderUserKey;
 
     // Insert a new record into UserProfiles
     string connectionString = 
          ConfigurationManager.ConnectionStrings["SecurityTutorialsConnectionString"].ConnectionString;
     string insertSql = "INSERT INTO UserProfiles(UserId, HomeTown, HomepageUrl,
          Signature) VALUES(@UserId, @HomeTown, @HomepageUrl, @Signature)";
 
     using (SqlConnection myConnection = new SqlConnection(connectionString))
     {
          myConnection.Open();
          SqlCommand myCommand = new SqlCommand(insertSql, myConnection);
          myCommand.Parameters.AddWithValue("@UserId", newUserId);
          myCommand.Parameters.AddWithValue("@HomeTown", DBNull.Value);
          myCommand.Parameters.AddWithValue("@HomepageUrl", DBNull.Value);
          myCommand.Parameters.AddWithValue("@Signature", DBNull.Value);
          myCommand.ExecuteNonQuery();
          myConnection.Close();
     }
}

上述代码通过检索刚刚添加的用户帐户的 UserId 进行。 这是通过使用 Membership.GetUser(username) 方法返回有关特定用户的信息,然后使用 ProviderUserKey 属性检索其 UserId 来实现的。 用户在 CreateUserWizard 控件中输入的用户名通过其 UserName 属性提供。

接下来,从 Web.config 中检索连接字符串, INSERT 并指定 语句。 必要的 ADO.NET 对象将被实例化并执行命令。 该代码将实例分配给 DBNull@HomepageUrl@Signature 参数,其效果是插入 HomeTownHomepageUrl、 和 Signature 字段的数据库NULL值。@HomeTown

EnhancedCreateUserWizard.aspx通过浏览器访问页面并创建新的用户帐户。 执行此操作后,返回到 Visual Studio 并检查 和 UserProfiles 表的内容aspnet_Users (,就像我们在图 12) 中所做的那样。 你应该会在 中aspnet_Users看到新的用户帐户,并且UserProfiles (相应的行,其中包含 NULLHomepageUrlSignature) 的值HomeTown

添加了新的用户帐户和 UserProfiles 记录

图 20:添加新的用户帐户和 UserProfiles 记录 (单击以查看全尺寸图像)

访问者提供其新帐户信息并单击“创建用户”按钮后,将创建用户帐户,并将一行添加到 UserProfiles 表中。 然后,CreateUserWizard 会显示其 CompleteWizardStep,其中显示一条成功消息和一个“继续”按钮。 单击“继续”按钮会导致回发,但不执行任何操作,使用户停滞在 EnhancedCreateUserWizard.aspx 页面上。

通过 CreateUserWizard 控件的 属性单击“继续”按钮时,我们可以指定将用户发送到的 ContinueDestinationPageUrlURL。 将 ContinueDestinationPageUrl 属性设置为“~/Membership/AdditionalUserInfo.aspx”。 这会将新用户转到 AdditionalUserInfo.aspx,他们可以在其中查看和更新其设置。

自定义 CreateUserWizard 的界面以提示新用户的家乡、主页和签名

CreateUserWizard 控件的默认界面足以满足只需收集用户名、密码和电子邮件等核心用户帐户信息的简单帐户创建方案。 但是,如果我们想要提示访问者在创建帐户时输入她的家乡、主页和签名,该怎么办? 可以自定义 CreateUserWizard 控件的 接口,以在注册时收集其他信息,并且此信息可用于事件处理程序将 CreatedUser 其他记录插入基础数据库中。

CreateUserWizard 控件扩展了 ASP.NET 向导控件,该控件允许页面开发人员定义一系列有序 WizardSteps的 。 向导控件呈现活动步骤,并提供导航界面,使访问者能够浏览这些步骤。 向导控件非常适合将长任务分解为几个简短步骤。 有关向导控件的详细信息,请参阅 使用 ASP.NET 2.0 向导控件创建分步用户界面

CreateUserWizard 控件的默认标记定义两个 WizardStepsCreateUserWizardStepCompleteWizardStep

<asp:CreateUserWizard ID="NewUserWizard" runat="server"
     ContinueDestinationPageUrl="~/Membership/AdditionalUserInfo.aspx">
     <WizardSteps>
          <asp:CreateUserWizardStep ID="CreateUserWizardStep1" runat="server">
          </asp:CreateUserWizardStep>
          <asp:CompleteWizardStep ID="CompleteWizardStep1" runat="server">
          </asp:CompleteWizardStep>
     </WizardSteps>
</asp:CreateUserWizard>

第一个 WizardStep呈现 CreateUserWizardStep提示输入用户名、密码、电子邮件等的接口。 访问者提供此信息并单击“创建用户”后,会显示 CompleteWizardStep,其中显示了成功消息和“继续”按钮。

若要自定义 CreateUserWizard 控件的界面以包含其他窗体字段,我们可以:

  • 创建一个或多个新WizardStep用于包含其他用户界面元素的 。 若要向 CreateUserWizard 添加新的 WizardStep ,请单击其智能标记中的“添加/删除 WizardSteps”链接以启动 WizardStep 集合编辑器。 在此处,可以添加、删除或重新排序向导中的步骤。 这是我们将在本教程中使用的方法。

  • CreateUserWizardStep转换转换为可编辑的WizardStep.这会将 CreateUserWizardStep 替换为等效WizardStep的 ,其标记定义与 匹配 CreateUserWizardStep的用户界面。 通过将 转换为 CreateUserWizardStepWizardStep 我们可以重新定位控件或向此步骤添加其他用户界面元素。 若要将 CreateUserWizardStepCompleteWizardStep 转换为可编辑的 WizardStep,请单击控件的智能标记中的“自定义创建用户步骤”或“自定义完成步骤”链接。

  • 使用上述两个选项的某种组合。

需要记住的一个重要事项是,当从其 CreateUserWizardStep内部单击“创建用户”按钮时,CreateUserWizard 控件将执行其用户帐户创建过程。 后是否有额外的 WizardStepCreateUserWizardStep 并不重要。

将自定义 WizardStep 添加到 CreateUserWizard 控件以收集其他用户输入时,可以将自定义 WizardStep 放置在 之前或之后 CreateUserWizardStep。 如果它位于 之前 CreateUserWizardStep ,则从自定义 WizardStep 收集的其他用户输入可用于 CreatedUser 事件处理程序。 但是,如果自定义 WizardStep 在此之后 CreateUserWizardStep 显示自定义 WizardStep ,则已创建新的用户帐户, CreatedUser 并且事件已触发。

图 21 显示了在 之前添加 WizardStepCreateUserWizardStep时的工作流。 由于在事件触发时 CreatedUser 收集了其他用户信息,因此只需更新 CreatedUser 事件处理程序以检索这些输入,并将这些输入用于 INSERT 语句的参数值 (而不是 DBNull.Value) 。

当其他向导步骤位于 CreateUserWizardStep 之前时,CreateUserWizard 工作流

图 21:CreateUserWizard 工作流当附加 WizardStep 项位于 CreateUserWizardStep (单击以查看全尺寸图像)

但是,如果将自定义WizardStep放置在 之后CreateUserWizardStep,则创建用户帐户过程发生在用户有机会输入其家乡、主页或签名之前。 在这种情况下,需要在创建用户帐户后将此附加信息插入数据库,如图 22 所示。

当其他向导步骤在 CreateUserWizardStep 之后出现时,CreateUserWizard 工作流

图 22: (单击以查看全尺寸图像CreateUserWizardStep,CreateUserWizard 工作流WizardStep)

图 22 中显示的工作流将等待将记录插入表中, UserProfiles 直到步骤 2 完成。 但是,如果访问者在步骤 1 后关闭其浏览器,我们将已达到创建用户帐户的状态,但未向 UserProfiles添加任何记录。 一种解决方法是在事件处理程序 (中插入UserProfilesCreatedUser一条记录,NULL该记录在步骤 1) 后触发,然后在步骤 2 完成后更新此记录。 这可确保 UserProfiles 即使用户中途退出注册过程,也会为用户帐户添加记录。

在本教程中,让我们创建一个新的 WizardStep ,它发生在 之后 CreateUserWizardStep ,但在 之前 CompleteWizardStep。 让我们先将 WizardStep 设置到位并配置,然后查看代码。

从 CreateUserWizard 控件的智能标记中,选择“添加/删除 WizardStep ”,这将打开 WizardStep “集合编辑器”对话框。 添加一个新的 WizardStep,将其 ID 设置为 UserSettings,将其 Title 设置为“你的设置”,将 设置为 StepTypeStep。 然后将它 CreateUserWizardStep 置于 (“注册新帐户”) 之后,并在 (“完成”) 之前 CompleteWizardStep ,如图 23 所示。

向 CreateUserWizard 控件添加新向导步骤

图 23:向 CreateUserWizard 控件添加新 WizardStep (单击以查看全尺寸图像)

单击“确定”关闭“ WizardStep 集合编辑器”对话框。 新的 WizardStep 由 CreateUserWizard 控件的更新声明性标记证明:

<asp:CreateUserWizard ID="NewUserWizard" runat="server"
     ContinueDestinationPageUrl="~/Membership/AdditionalUserInfo.aspx">
     <WizardSteps>
          <asp:CreateUserWizardStep ID="CreateUserWizardStep1" runat="server">
          </asp:CreateUserWizardStep>
          <asp:WizardStep runat="server" ID="UserSettings" StepType="Step"
               Title="Your Settings">
          </asp:WizardStep>
          <asp:CompleteWizardStep ID="CompleteWizardStep1" runat="server">
          </asp:CompleteWizardStep>
     </WizardSteps>
</asp:CreateUserWizard>

请注意新 <asp:WizardStep> 元素。 我们需要添加用户界面,以便在此处收集新用户的家乡、主页和签名。 可以使用声明性语法或通过Designer输入此内容。 若要使用Designer,请从智能标记的下拉列表中选择“你的设置”步骤,以查看Designer中的步骤。

注意

通过智能标记的下拉列表选择一个步骤会更新 CreateUserWizard 控件的 ActiveStepIndex 属性,该属性指定起始步骤的索引。 因此,如果使用此下拉列表编辑Designer中的“你的设置”步骤,请务必将其设置回“注册新帐户”,以便在用户首次访问EnhancedCreateUserWizard.aspx页面时显示此步骤。

在“你的设置”步骤中创建一个用户界面,其中包含名为 、 HomepageUrlSignature的三个 HomeTownTextBox 控件。 构造此接口后,CreateUserWizard 的声明性标记应如下所示:

<asp:CreateUserWizard ID="NewUserWizard" runat="server"
     ContinueDestinationPageUrl="~/Membership/AdditionalUserInfo.aspx">
     <WizardSteps>
          <asp:CreateUserWizardStep ID="CreateUserWizardStep1" runat="server">
          </asp:CreateUserWizardStep>
          <asp:WizardStep runat="server" ID="UserSettings" StepType="Step"
               Title="Your Settings">
               <p>
                    <b>Home Town:</b><br />
                    <asp:TextBox ID="HomeTown" runat="server"></asp:TextBox>
               </p>
               <p>
                    <b>Homepage URL:</b><br />
                    <asp:TextBox ID="HomepageUrl" Columns="40" runat="server"></asp:TextBox>
               </p>
               <p>
                    <b>Signature:</b><br />
                    <asp:TextBox ID="Signature" TextMode="MultiLine" Width="95%"
                         Rows="5" runat="server"></asp:TextBox>
               </p>
          </asp:WizardStep>
          <asp:CompleteWizardStep ID="CompleteWizardStep1" runat="server">
          </asp:CompleteWizardStep>
     </WizardSteps>
</asp:CreateUserWizard>

继续通过浏览器访问此页面,并创建新的用户帐户,为家乡、主页和签名指定值。 完成后, CreateUserWizardStep 将在成员资格框架中创建用户帐户,并 CreatedUser 运行事件处理程序,这会向 UserProfiles添加新行,但数据库 NULL 值为 HomeTownHomepageUrlSignature。 永远不会使用为家乡、主页和签名输入的值。 最终结果是一个新的用户帐户,其中包含一个 UserProfiles 记录,其 HomeTownHomepageUrlSignature 字段尚未指定。

我们需要在“你的设置”步骤之后执行代码,该步骤采用用户输入的家乡、磨练页和签名值,并更新相应的 UserProfiles 记录。 每当用户在 Wizard 控件中的步骤之间移动时,向导的事件ActiveStepChanged都会触发。 我们可以为此事件创建事件处理程序, UserProfiles 并在完成“你的设置”步骤时更新表。

为 CreateUserWizard 的 ActiveStepChanged 事件添加事件处理程序,并添加以下代码:

protected void NewUserWizard_ActiveStepChanged(object sender, EventArgs e)
{
     // Have we JUST reached the Complete step?
     if (NewUserWizard.ActiveStep.Title == "Complete")
     {
          WizardStep UserSettings = NewUserWizard.FindControl("UserSettings") as
          WizardStep;
 
          // Programmatically reference the TextBox controls
          TextBox HomeTown = UserSettings.FindControl("HomeTown") as TextBox;
          TextBox HomepageUrl = UserSettings.FindControl("HomepageUrl") as TextBox;
          TextBox Signature = UserSettings.FindControl("Signature") as TextBox;
 
          // Update the UserProfiles record for this user
          // Get the UserId of the just-added user
          MembershipUser newUser = Membership.GetUser(NewUserWizard.UserName);
          Guid newUserId = (Guid)newUser.ProviderUserKey;
 
          // Insert a new record into UserProfiles
          string connectionString = 
               ConfigurationManager.ConnectionStrings["SecurityTutorialsConnectionString"].ConnectionString;
          string updateSql = "UPDATE UserProfiles SET HomeTown = @HomeTown, HomepageUrl
               = @HomepageUrl, Signature = @Signature WHERE UserId = @UserId";
 
          using (SqlConnection myConnection = new SqlConnection(connectionString))
          {
               myConnection.Open();
               SqlCommand myCommand = new SqlCommand(updateSql, myConnection);
               myCommand.Parameters.AddWithValue("@HomeTown", HomeTown.Text.Trim());
               myCommand.Parameters.AddWithValue("@HomepageUrl", HomepageUrl.Text.Trim());
               myCommand.Parameters.AddWithValue("@Signature", Signature.Text.Trim());
               myCommand.Parameters.AddWithValue("@UserId", newUserId);
               myCommand.ExecuteNonQuery();
               myConnection.Close();
          }
     }
}

上述代码首先确定我们是否刚刚到达了“完成”步骤。 由于“完成”步骤紧接在“你的设置”步骤之后,那么当访问者到达“完成”步骤时,这意味着她刚刚完成了“你的设置”步骤。

在这种情况下,我们需要以编程方式引用 中的 UserSettings WizardStepTextBox 控件。 为此,首先使用 FindControl 方法以编程方式引用 UserSettings WizardStep,然后再次从 中 WizardStep引用 TextBox。 引用 TextBox 后,即可执行 UPDATE 语句。 语句 UPDATE 的参数数目 INSERT 与事件处理程序中的 CreatedUser 语句相同,但此处我们使用用户提供的家乡、主页和签名值。

完成此事件处理程序后,通过浏览器访问 EnhancedCreateUserWizard.aspx 页面,并创建一个新的用户帐户,指定家乡、主页和签名的值。 创建新帐户后,应重定向到 AdditionalUserInfo.aspx 页面,其中显示了刚刚输入的家乡、主页和签名信息。

注意

我们的网站目前有两个页面,访问者可以从中创建新帐户: CreatingUserAccounts.aspxEnhancedCreateUserWizard.aspx。 网站的站点地图和登录页指向该 CreatingUserAccounts.aspx 页面,但 CreatingUserAccounts.aspx 页面不会提示用户输入其家乡、主页和签名信息,并且不会向 UserProfiles添加相应的行。 因此,更新 CreatingUserAccounts.aspx 页面以使其提供此功能,或更新站点地图和登录页以引用 EnhancedCreateUserWizard.aspx 而不是 CreatingUserAccounts.aspx。 如果选择后一个选项,请务必更新 Membership 文件夹的文件 Web.config ,以便允许匿名用户访问页面 EnhancedCreateUserWizard.aspx

摘要

在本教程中,我们了解了对成员资格框架中与用户帐户相关的数据建模的技术。 具体而言,我们了解了与用户帐户共享一对多关系的实体以及共享一对一关系的数据的建模。 此外,我们还了解了如何显示、插入和更新此相关信息,其中一些示例使用 SqlDataSource 控件,另一些示例使用 ADO.NET 代码。

本教程完成对用户帐户的了解。 从下一教程开始,我们将把注意力转向角色。 在接下来的几个教程中,我们将了解角色框架、如何创建新角色、如何将角色分配给用户、如何确定用户所属的角色以及如何应用基于角色的授权。

编程愉快!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

Scott Mitchell 是多本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上联系 mitchell@4guysfromrolla.com 斯科特,也可以通过他的博客在 联系 http://ScottOnWriting.NET

特别感谢...

本教程系列由许多有用的审阅者审阅。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处 mitchell@4GuysFromRolla.com放置一行。