本文档是 Visual Basic 教程 (转至 Visual C# 教程 )
在本教程中 , 我们将通过构建一个最基本的留言板应用程序来解决这个问题。同时,我们将探讨在数据库中构建用户信息模型的不同方法,以及如何将数据与 Membership 框架创建的用户帐户关联起来。
简介ASP.NET 的 Membership 框架为管理用户提供了一个灵活的接口。 Membership API 包含用于验证凭据、获取当前登录用户的信息、创建新用户帐户和删除用户帐户等的方法。 Membership 框架中的每个用户帐户都只包含验证凭据和执行基本的用户帐户相关任务时所需的属性。MembershipUser 类 的方法和属性可证明此情况。该类定义了 Membership 框架中的用户帐户的模型。它的属性包含UserName 、Email 和 IsLockedOut ,方法有 GetPassword 和 UnlockUser 。 通常情况下,应用程序需要存储 Membership 框架中不包含的额外的用户信息。例如,一个网络零售商可能需要让每位用户存储自己的送货和帐单地址、付款信息、递送方式以及联系电话号码。此外,系统中的每个订单都要与特定用户帐户关联起来。 MembershipUser 类并不包含 PhoneNumber 、 DeliveryPreferences 和 PastOrders 等属性,那么,我们如何来跟踪应用程序所需的用户信息,并将这些信息与 Membership 框架结合起来呢?在本教程中,我们将通过构建一个最基本的留言板应用程序来解决这个问题。同时,我们将探讨在数据库中构建用户信息模型的不同方法,以及如何将数据与 Membership 框架创建的用户帐户关联起来。让我们开始吧 ! 步骤1 : 创建留言板应用程序的数据模型有很多技术可用于在数据库中获取用户信息 , 并将这些信息与 Membership 框架创建的用户帐户关联起来。为说明这些技术,我们需要增强教程中的 web 应用程序,以获取某些与用户相关的数据。(目前,应用程序的数据模型只包含 SqlMembershipProvider 所需的应用程序服务表。) 让我们创建一个简单的留言板应用程序,在该应用程序中,经过身份验证的用户可以留下评论。除了存储留言板评论外,我们还允许每位用户存储自己的家乡、主页和签名。如果用户提供了家乡、主页和签名,那么这些信息将出现在该用户在留言板中留下的每条消息中。 添加 GuestbookComments 表为获取留言板评论,我们需要创建一个名为 GuestbookComments 的数据库表,该表具有 CommentId 、 Subject 、 Body 和 CommentDate 等列。我们还需要让 GuestbookComments 表中的每条记录注明留下评论的用户。 要将该表添加到我们的数据库中,则需进入 Visual Studio 中的数据库资源管理器,然后向下深入到 SecurityTutorials 数据库。右键单击 Tables 文件夹,选择 Add New Table ,此操作将打开一个允许我们为新表定义列的界面。 图1 : 在 SecurityTutorials 数据库中添加一个新表 (单击此处查看实际大小的图像 ) 接下来 , 定义GuestbookComments 的列。首先 , 添加一个名为 CommentId 、类型为 uniqueidentifier 的列。该列将唯一标识留言板中的每条评论,因此禁止使用空值,并将它标记为表的主键。我们不是在每次 INSERT 操作时提供 CommentId 列的值,而是通过将该列的默认值设置为 NEWID() ,指示每次 INSERT 操作时为该列自动生成一个新的 uniqueidentifier 值。添加了第一个列、将它标记为主键并为其设置了默认值后,屏幕截图应如图 2 所示。 图2 : 添加一个名为 CommentId 的主键列 (单击此处查看实际大小的图像 ) 接下来 , 添加一个名为Subject 的 nvarchar(50) 类型的列 , 以及一个名为Body 的 nvarchar(MAX) 类型的列 , 并在这两个列中禁用空值。然后,添加一个名为 CommentDate 的 datetime 类型的列,禁用空值,并将 CommentDate 列的默认值设置为 getdate() 。 剩下的工作是添加一个列,使用该列将用户帐户与每条留言板评论关联起来。一种方法是添加一个名为 UserName 的 nvarchar(256) 类型的列。使用除 SqlMembershipProvider 之外的成员资格提供程序时,这是一个合适的选择。但在使用 SqlMembershipProvider 时(正如我们在本教程系列中面临的情况),这样做不能保证 aspnet_Users 表中的 UserName 列是唯一的。 aspnet_Users 表的主键是 UserId ,类型为 uniqueidentifier 。因此, GuestbookComments 表需要一个名为 UserId 的 uniqueidentifier 类型的列(禁用空值)。添加该列。 注意 : 如我们在《在 SQL Server 中创建 Membership Schema 》教程中讨论的那样 ,Membership 框架的设计意图是允许使用不同用户帐户的多个Web 应用程序共享同一个用户存储。它通过将用户帐户划分到不同的应用程序来实现此目的。虽然可以保证每个用户名在一个应用程序中是唯一的,但使用同一用户存储的不同应用程序中可能会有相同的用户名。在 aspnet_Users 表中,有一个基于 UserName 和 ApplicationId 列(而不是只基于 UserName 列)的复合 UNIQUE 约束。因此, aspnet_Users 表可能有两条(或多条)记录的 UserName 值是相同的。而 aspnet_Users 表还有一个基于 UserId 列(因为它是主键)的 UNIQUE 约束。该 UNIQUE 约束很重要,如果没有该约束,我们就不能在 GuestbookComments 和 aspnet_Users 表之间建立外键约束。 添加完 UserId 列后,单击工具栏中的 Save 图标来保存新创建的表,将新表命名为 GuestbookComments 。 对于GuestbookComments 表 , 我们还有最后一个要关注的问题 : 即我们需要在 GuestbookComments.UserId 列和aspnet_Users.UserId 列之间创建一个外键约束 。为此,单击工具栏中的 Relationship 图标,打开 Foreign Key Relationships 对话框。(或者,进入 Table Designer 菜单并选择 Relationships 来打开该对话框。) 单击 Foreign Key Relationships 对话框左下角的 Add 按钮。这将会添加一个新的外键约束,当然,我们还需要定义参与该关系的表。 图3 : 使用 Foreign Key Relationships 对话框来管理表的外键约束 (单击此处查看实际大小的图像 ) 接下来 , 单击“Table and Columns Specifications” 行右侧的省略号图标。这将打开 Tables and Columns 对话框,在该对话框中,我们可以指定主键表和列以及 GuestbookComments 表中的外键列。具体来说,我们将选择 aspnet_Users 和 UserId 作为主键表和主键列,并选择 GuestbookComments 表中的 UserId 作为外键列(参见图 4 )。定义了主外键表和主外键列后,单击 OK ,返回 Foreign Key Relationships 对话框。 图4 : 在 aspnet_Users 和 GuesbookComments 表之间建立外键约束 (单击此处查看实际大小的图像 ) 此时 , 外键约束就建立好了。该约束的存在可以确保上述两个表之间的关系完整性 , 这是通过保证留言板条目永远不会引用一个不存在的用户帐户来实现的。默认情况下,如果存在相应的子记录,则外键约束不允许删除父记录。即,如果一位用户发表了一条或多条评论,那么当我们试图删除该用户帐户时,删除操作会失败,除非先删除该用户的留言板评论。 可将外键约束配置为删除父记录时自动删除关联的子记录。换句话说,我们可以对外键约束进行设置,以便在删除一位用户的用户帐户时,自动删除该用户的留言板记录。为此,我们需要展开 “INSERT And UPDATE Specification” 部分,将 “Delete Rule” 属性设置为 Cascade 。 图5 : 将外键约束配置为级联删除 (单击此处查看实际大小的图像 ) 要保存外键约束 , 单击Close 按钮 , 退出 Foreign Key Relationships , 然后单击工具栏中的 Save 图标来保存表和关系。 存储用户的家乡、主页和签名GuestbookComments 表说明了如何存储与用户帐户有一对多关系的信息。由于每个用户帐户可能有任意数量的与其关联的评论,我们可以这样来定义该关系的模型:创建一个表来存储评论,并在该表中设置一列,用于将每条评论链接回发表该评论的特定用户。使用 SqlMembershipProvider 时,最好这样来建立该链接:创建一个名为 UserId 的 uniqueidentifier 类型的列,并在该列和 aspnet_Users.UserId 之间建立一个外键约束。 现在,我们需要将三个列与每个用户帐户关联起来,以便存储用户的家乡、主页和签名,这些信息将出现在用户发表的留言板评论中。有很多不同的方法可以完成这项工作:
我们将创建一个名为 UserProfiles 的新表 , 存储每位用户的家乡、主页和签名。在数据库资源管理器窗口中,右键单击 Tables 文件夹,选择创建一个新表。将第一列命名为 UserId ,将其类型设置为 uniqueidentifier ,禁用空值,并将该列标记为主键。然后添加以下各列:一个名为 HomeTown 的 nvarchar(50) 类型的列,一个名为 HomepageUrl 的 nvarchar(100) 类型的列,以及一个名为 Signature 的 nvarchar(500) 类型的列。这三个列都可以接受空值。 图6 : 创建 UserProfiles 表 (单击此处查看实际大小的图像 ) 保存该表 , 并命名为UserProfiles 。最后 , 在 UserProfiles 表的 UserId 列和 aspnet_Users.UserId 列之间建立一个外键约束。像我们在 GuestbookComments 和 aspnet_Users 表之间建立的外键约束一样,将该约束设置为级联删除。由于 UserProfiles 表中的 UserId 列是主键,这可确保每个用户帐户在 UserProfiles 表中的记录数不会多于一条。这种关系被称为一对一的关系。 我们创建好了数据模型,现在就可以使用该模型了。在步骤 2 和 3 中,我们将探讨当前登录用户如何查看和编辑自己的家乡、主页和签名信息。在步骤 4 中,我们将创建一个界面,允许经过身份验证的用户在该界面中向留言板提交新评论和查看现有的评论。 步骤2 : 显示用户的家乡、主页和签名允许当前登录用户查看和编辑他 / 她的家乡、主页和签名信息有很多种方式。我们可以手动创建带有 TextBox 和 Label 控件的用户界面,或使用一种 Web 数据控件,如 DetailsView 控件。要执行数据库 SELECT 和 UPDATE 语句,我们可以在页面的代码文件类中编写 ADO.NET 代码,或使用带有 SqlDataSource 的声明方法。不过,最理想的做法是在应用程序中包含一个分层的架构,我们可以在页面的代码文件类中通过编码来调用该架构,或通过 ObjectDataSource 控件以声明方式来调用该架构。 由于本教程系列重点关注表单身份验证、授权、用户帐户和角色,因此这里不会详细讨论这些不同的数据访问方法,以及分层的架构为何要比直接在 ASP.NET 页面中执行 SQL 语句更好。我打算使用 DetailsView 和 SqlDataSource ,这是最快捷、最方便的方法。当然,之前讨论的概念也可应用到其它的 Web 控件和数据访问逻辑。有关在 ASP.NET 中处理数据的更多信息,请参阅我的《在 ASP.NET 2.0 中处理数据 》系列教程。 打开 Membership 文件夹中的 AdditionalUserInfo.aspx 页面,在该页面中添加一个 DetailsView 控件,将其 ID 属性设置为 UserProfile ,并清除其 Width 和 Height 属性。展开 DetailsView 的智能标记,选择将它绑定到一个新的数据源控件,这将打开 DataSource Configuration Wizard (参见图 7 )。该向导的第一步要求指定数据源类型。由于我们要直接连接到 SecurityTutorials 数据库,因此选择 Database 图标,指定其 ID 为 UserProfileDataSource 。 图7 : 添加一个名为 UserProfileDataSource 的新 SqlDataSource 控件 (单击此处查看实际大小的图像 ) 下一个屏幕提示选择要使用的数据库。我们已经在Web.config 中为 SecurityTutorials 数据库定义了一个连接字符串。该连接字符串的名称 (SecurityTutorialsConnectionString) 应该位于下拉列表中。选择该名称并单击 Next 。 图8 : 从下拉列表中选择 SecurityTutorialsConnectionString (单击此处查看实际大小的图像 ) 接下来的屏幕要求我们指定要查询的表和列。从下拉列表中选择 UserProfiles 表,并选中所有的列。 图9 : 从 UserProfiles 表中返回所有的列 (单击此处查看实际大小的图像 ) 图9 中的当前查询会返回 UserProfiles 中的所有 记录 , 但我们只对当前登录用户的记录感兴趣。要添加一个 WHERE 子句,单击 WHERE 按钮,打开 Add WHERE Clause 对话框(参见图 10 )。我们可在该对话框中选择要筛选的列、运算符以及筛选参数源。选择 UserId 作为筛选列, “=” 作为运算符。 很遗憾,没有内置的参数源可以返回当前登录用户的 UserId 值。我们需要通过编码来获取该值。因此,将 Source 下拉列表置为 “None” ,单击 Add 按钮添加参数,然后单击 OK 。 图10 : 在 UserId 列上添加一个筛选参数 (单击此处查看实际大小的图像 ) 单击OK 后 , 我们将回到图 9 中所示的屏幕。然而,此时屏幕底部的 SQL 查询中应该包含一个 WHERE 子句。单击 Next ,进入 “Test Query” 屏幕。我们可在此处运行查询并查看结果。单击Finish 完成向导。 完成 DataSource Configuration Wizard 后, Visual Studio 根据向导中指定的设置来创建 SqlDataSource 控件。此外,它还会在 DetailsView 中为 SqlDataSource 的 SelectCommand 返回的每个列添加相应的 BoundField 。没有必要在 DetailsViewe 中显示 UserId 列,因为用户不必知道该值。我们可以直接在 DetailsView 控件的声明标记中删除该列,或通过在其智能标记中单击 “Edit Fields” 链接来进行处理。 此时 , 页面的声明标记应如下所示 :
在选择数据之前 , 我们需要通过编码将SqlDataSource 控件的 UserId 参数设置为当前登录用户的 UserId 。要实现这一点,我们可为 SqlDataSource 的 Selecting 事件创建一个事件处理程序,并添加以下代码:
上述代码首先通过调用 Membership 类的 GetUser 方法来获得对当前登录用户的引用。这将返回一个 MembershipUser 对象,该对象的 ProviderUserKey 属性包含 UserId 。然后,将该 UserId 值分配给 SqlDataSource 的 @UserId 参数。 注意 :Membership.GetUser() 方法返回有关当前登录用户的信息。如果一个匿名用户访问该页面,则该方法返回一个空值。在这种情况下,当下一行代码试图读取 ProviderUserKey 属性时,就会导致发生 NullReferenceException 。当然,我们不必担心 AdditionalUserInfo.aspx 页面中的 Membership.GetUser() 会返回一个 Nothing 值,因为我们在前面的教程中将 URL 授权配置为只有经过身份验证的用户才能访问该文件夹中的 ASP.NET 资源。如果需要访问一个页面的当前登录用户的信息,而且该页面允许匿名访问,则在引用 MembershipUser 对象的属性之前,一定要检查 GetUser() 方法返回的 MembershipUser 对象不为 Nothing。 如果通过浏览器访问 AdditionalUserInfo.aspx 页面,则将看到一个空白页,因为我们还未在 UserProfiles 表中添加任何记录。在步骤 6 中,我们将探讨如何自定义 CreateUserWizard 控件,以便在创建一个新用户帐户时自动在 UserProfiles 表中添加一条新记录。然而,现在我们需要在表中手动创建一条记录。 进入 Visual Studio 的数据库资源管理器,展开 Tables 文件夹。右键单击 aspnet_Users 表,选择 “Show Table Data” 查看表中的记录;对 UserProfiles 表也进行相同的操作。图 11 显示的是垂直平铺的记录。在我的数据库中,表 aspnet_Users 中现在有 Bruce 、 Fred 和 Tito 的记录,但 UserProfiles 表中没有他们的记录。 图11 : 显示 aspnet_Users 和 UserProfiles 表的内容 (单击此处查看实际大小的图像 ) 通过手动输入 HomeTown 、HomepageUrl 和 Signature 列的值 , 在 UserProfiles 表中添加一条新记录。在 UserProfiles 新记录中获取一个有效 UserId 值的最简单方法是:在 aspnet_Users 表中选择特定用户帐户的 UserId 列,将其中的值复制并粘贴到 UserProfiles 的 UserId 列。图 12 显示的是为 Bruce 添加了一条新记录后的 UserProfiles 表。 图12 : 在 UserProfiles 表中为 Bruce 添加了一条记录 (单击此处查看实际大小的图像 ) 返回 AdditionalUserInfo.aspx 页面 , 以 Bruce 的身份登录。如图 13 所示,屏幕显示 Bruce 的设置。 图13 : 显示当前访问用户的设置 (单击此处查看实际大小的图像 ) 注意 : 在 UserProfiles 表中手动为每位用户添加记录。在步骤 6 中,我们将探讨如何自定义 CreateUserWizard 控件,以便在创建一个新用户帐户时自动在 UserProfiles 表中添加一条新记录。 步骤3 : 允许用户编辑他 / 她的家乡、主页和签名此时 , 当前登录用户可以查看他们的家乡、主页和签名设置 , 但还不能进行修改这些设置。我们接下来对 DetailsView 控件进行修改,以允许用户编辑这些数据。 需要做的第一件事情是为 SqlDataSource 添加一个 UpdateCommand ,指定要执行的 UPDATE 语句及该语句相应的参数。选择 SqlDataSource ,并在 Properties 窗口中单击 UpdateQuery 属性旁边的省略号按钮,打开 Command and Parameter Editor 对话框。在文本框中输入以下 UPDATE 语句:
接下来 , 单击“Refresh Parameters” 按钮 , 这将在 SqlDataSource 控件的UpdateParameters 集合中为 UPDATE 语句中的每个参数创建一个对应的参数。将所有参数的源设置为 None ,然后单击 OK 按钮关闭对话框。 图14 : 指定 SqlDataSource 的 UpdateCommand 和 UpdateParameters (单击此处查看实际大小的图像 ) 由于我们对SqlDataSource 控件做了这些改动 ,DetailsView 控件现在可以支持编辑了。在 DetailsView 的智能标记中,选中 “Enable Editing” 复选框,这将在控件的 Fields 集合中添加一个 CommandField ,其 ShowEditButton 属性被设置为 True 。这样,当 DetailsView 以只读模式呈现时,将显示一个 Edit 按钮;而当以编辑模式呈现时,将显示 Update 和 Cancel 按钮。与其让用户单击 Edit 进入编辑状态,还不如将 DetailsView 控件的 DefaultMode 属性 设置为 Edit ,让 DetailsView 一直处于可编辑的状态。 进行了这些更改后 ,DetailsView 控件的声明标记应如下所示 :
注意添加的CommandField 以及 DefaultMode 属性。 然后 , 通过浏览器测试该页面。以在 UserProfiles 表中有相应记录的用户身份访问时,该用户的设置将显示在一个可编辑的界面中。 图15 :DetailsView 呈现一个可编辑的界面 (单击此处查看实际大小的图像 ) 尝试改变设置并单击 Update 按钮。似乎什么事情都没有发生。实际上,系统中发生了一次回传,更改的值被保存到数据库中,但没有视觉反馈提示已经进行了保存。 为弥补这一点 , 我们回到Visual Studio , 在 DetailsView 上添加一个 Label 控件 , 将其ID 设置为 SettingsUpdatedMessage ,Text 属性设置为 “Your settings have been updated” ,Visible 和 EnableViewState 属性设置为 False 。
只要对DetailsView 进行了更新 , 我们就需要显示 SettingsUpdatedMessage 标签。为此 ,我们 为 DetailsView 的 ItemUpdated 事件创建一个事件处理程序,并添加以下代码 :
通过浏览器回到 AdditionalUserInfo.aspx 页面 , 并更新数据。此时,会显示一条有用的状态消息。 图16 : 更新设置后 , 屏幕显示一条简短的消息 (单击此处查看实际大小的图像 ) 注意 :DetailsView 控件的编辑界面还有很多要完善的地方。它使用标准尺寸的文本框,但 Signature 字段可能是一个多行的文本框。另外,还应使用 RegularExpressionValidator 来确保主页 URL (如果输入了)是以 “http://” 或 “https://” 开头的。此外,由于 DetailsView 控件的 DefaultMode 属性被设置为 Edit ,则 Cancel 按钮就没什么用了。因此,我们应将它删除,或者在用户单击该按钮时将用户重定向到某个其它页面(如 ~/Default.aspx )。我将这些增强作为练习留给读者。 在母版页中添加一个到 AdditionalUserInfo.aspx 页面的链接目前 , 网站并没有提供任何到 AdditionalUserInfo.aspx 页面的链接。访问该页面的唯一方式是直接在浏览器的地址栏中输入该页面的 URL 。让我们在 Site.master 母版页中添加一个到该页面的链接。 回想一下,母版页在它的 LoginContent ContentPlaceHolder 中包含一个 LoginView Web 控件,该控件对经过身份验证的访问者和匿名访问者显示不同的标记。更新 LoginView 控件的 LoggedInTemplate ,使其包含一个到 AdditionalUserInfo.aspx 页面的链接。完成这些更改后 ,LoginView 控件 的声明 标记 应如下所示 :
注意在LoggedInTemplate 中添加的 lnkUpdateSettings HyperLink 控件。有了该链接后,经过身份验证的用户可快速跳转到能够查看和修改其家乡、主页和签名设置的页面。 步骤4 : 添加新留言板评论经过身份验证的用户可在 Guestbook.aspx 页面中查看留言板并留下评论。我们首先创建用来添加新留言板评论的界面。 在 Visual Studio 中打开 Guestbook.aspx 页面,构建一个用户界面,该界面包含两个 TextBox 控件,一个用于输入新评论的主题,一个用于输入评论的正文。将第一个 TextBox 控件的 ID 属性设置为 Subject , Columns 属性设置为 40 ;将第二个控件的 ID 设置为 Body , TextMode 属性设置为 MultiLine , Width 和 Rows 属性分别设置为 “95%” 和 8 。要完成该用户界面,还要添加一个名为 PostCommentButton 的 Web 按钮控件,并将其 Text 属性设置为 “Post Your Comment” 。 由于每条留言板评论都需要主题和正文,我们分别为这两个 TextBox 控件添加一个 RequiredFieldValidator 。将这两个控件的 ValidationGroup 属性设置为 “EnterComment” ;同样,将 PostCommentButton 控件的 ValidationGroup 属性设置为 “EnterComment” 。有关 ASP.NET 的验证控件的更多信息,请查阅 ASP.NET 中的表单身份验证 、分析 ASP.NET 2.0 中的验证控件 ,以及 W3Schools 上的验证服务器控件教程 。 生成用户界面后,页面的声明标记应如下所示:
用户界面完成后 , 我们接下来要做的是 , 在单击PostCommentButton 时向 GuestbookComments 表中插入一条新记录。该任务有多种实现方式:我们可在该按钮的 Click 事件处理程序中编写 ADO.NET 代码;可在页面中添加一个 SqlDataSource 控件,配置其 InsertCommand ,然后在 Click 事件处理程序中调用其 Insert 方法;或者创建一个中间层,该层负责插入新留言板评论,然后在 Click 事件处理程序中调用该功能。由于我们在步骤 3 中探讨了如何使用 SqlDataSource ,此处,我们使用 ADO.NET 代码。 注意 : 用于通过编码从 Microsoft SQL Server 数据库中访问数据的 ADO.NET 类位于 System.Data.SqlClient 命名空间中。因此,我们需要将此命名空间导入到页面的代码文件类中(即,导入 System.Data.SqlClient )。 为PostCommentButton 的 Click 事件创建一个 事件处理程序, 并添加以下代码 :
Click 事件处理程序首先检查用户提供的数据是否有效 , 如果无效 , 则事件处理程序在插入记录之前退出。如果提供的数据有效,则获取当前登录用户的 UserId 值,并将该值存储在局部变量 currentUserId 中。该值是必需的,因为向 GuestbookComments 表中插入记录时必须提供 UserId 值。 然后,从 Web.config 中获取 SecurityTutorials 数据库的连接字符串,并指定 INSERT SQL 语句。之后,创建一个 SqlConnection 对象并打开该对象。接下来,构建一个 SqlCommand 对象,并为 INSERT 查询中要用的参数赋值。然后,执行 INSERT 语句,并关闭连接。在事件处理程序的最后,清空 Subject 和 Body 这两个 TextBox 控件的 Text 属性,因而用户的值不会在回传后仍然存在。 在浏览器中测试该页面。由于该页面在 Membership 文件夹中,匿名用户不能访问。因此,我们需要先登录(如果尚未登录)。在 Subject 和 Body Textbox 控件中输入值,然后单击 PostCommentButton 按钮。这将会在 GuestbookComments 表中添加一条新记录。回传时,刚才提供的主题和正文将从 Textbox 控件中清除。 单击 PostCommentButton 按钮后,没有提示评论已添加到留言板的视觉反馈。我们还需要更新此页面来显示现有的留言板评论 , 此功能将在步骤 5 中实现。一旦实现,刚添加的评论将显示在评论列表中,从而提供适当的视觉反馈。现在,我们需要通过检查 GuestbookComments 表的内容来确定留言板评论已经保存。 图 17 显示的是添加两条评论后 GuestbookComments 表的内容。 图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 的智能标记,在 Choose Data Source 下拉列表中,将该控件绑定到一个新的数据源。如我们在步骤 2 中看到的那样,这将打开 Data Source Configuration Wizard 。选择 Database 图标,将生成的 SqlDataSource 命名为 CommentsDataSource ,单击 OK 。接下来,在下拉列表中选择 SecurityTutorialsConnectionString 连接字符串,单击 Next 。 在步骤 2 中,我们通过在下拉列表中选择 UserProfiles 表并选择要返回的列来指定要查询的数据(参见图 9 )。然而,这次我们想构建这样的一个 SQL 语句:不仅返回 GuestbookComments 表中的记录,还要返回评论者的家乡、主页、签名和用户名。因此,选择 “Specify a custom SQL statement or stored procedure” 单选按钮,单击 Next 。 这将打开 “Define Custom Statements or Stored Procedures” 屏幕。单击 Query Builder 按钮,以图形方式构建该查询。查询生成器首先提示我们指定要查询的表。选择 GuestbookComments 、 UserProfiles 和 aspnet_Users 表,单击 OK 。这样就将这三个表添加到设计图面了。由于 GuestbookComments 、 UserProfiles 和 aspnet_Users 表之间有外键约束,查询生成器将自动联接这些表。 最后要做的是指定要返回的列。从 GuestbookComments 表中选择 Subject 、 Body 和 CommentDate 列;从 UserProfiles 表中返回 HomeTown 、 HomepageUrl 和 Signature 列;从 aspnet_Users 表中返回 UserName 。再在 SELECT 查询的后面添加 “ORDER BY CommentDate DESC” ,以便首先返回最近的留言。完成这些选择后,查询生成器界面应该类似于图 18 中的屏幕截图。 图18 : 构建的查询联接了 GuestbookComments 、UserProfiles 和 aspnet_Users 表 (单击此处查看实际大小的图像 ) 单击OK 关闭查询生成器窗口 , 返回 “Define Custom Statements or Stored Procedures” 屏幕。单击 Next 进入 “Test Query” 屏幕,我们可在此处通过单击 Test Query 按钮来查看查询结果。准备好之后,单击 Finish 完成 Configure Data Source 向导。 在步骤 2 中,当我们完成 Configure Data Source 向导后,系统会更新相关的 DetailsView 控件的 Fields 集合,以包含 SelectCommand 返回的各列的 BoundField 。而 ListView 却保持不变;我们还需要定义其布局。可以通过声明标记手动构建 ListView 的布局,或通过其智能标记中的 “Configure ListView” 选项来构建。我通常愿意手动定义标记。当然,使用哪种方式就看自己的喜好了。 我对 ListView 控件使用了以下 LayoutTemplate 、 ItemTemplate 和 ItemSeparatorTemplate :
LayoutTemplate 定义控件生成的标记 , 而ItemTemplate 呈现 SqlDataSource 返回的每条记录。 ItemTemplate 的最终标记位于 LayoutTemplate 的 itemPlaceholder 控件中。除 itemPlaceholder 控件外, LayoutTemplate 还包含一个 DataPager 控件,该控件限制 ListView 在每页只显示 10 条留言板评论(默认值),并显示一个分页界面。 我的 ItemTemplate 在一个 <h4> 元素中显示每条留言板评论的主题,评论正文位于主题的下方。请注意显示正文使用的语法,该语法获取 Eval("Body") 数据绑定语句返回的数据,将数据转化为字符串,然后使用 <br /> 元素替代换行符。要显示提交评论时输入的换行符,这种转换是必要的,因为 HTML 会忽略空白。用户的签名以斜体字显示在评论正文的下方,签名的后面是用户的家乡、一个到用户主页的链接、发表评论的日期和时间以及评论者的用户名。 花些时间在浏览器中查看本页。你应该会发现,在步骤 5 中添加到留言板的评论显示在此处。 图19 :Guestbook.aspx 现在显示留言板中的评论 (单击此处查看实际大小的图像 ) 尝试在留言板中添加一条新评论。单击PostCommentButton 按钮时 , 页面回传 , 评论被添加到数据库 , 但 ListView 控件没有更新 , 因此不能显示新评论。这个问题可用下面的任意一种方法来弥补:
本教程的教程网站可下载代码中举例说明了这两种技术。我们将 ListView 控件的 EnableViewState 属性设置为 False ,并在 Click 事件处理程序中提供了通过编码重新将数据绑定 ListView 时所需的代码,只是将它注释掉了。 注意 : 现在, AdditionalUserInfo.aspx 页面允许用户查看和编辑其家乡、主页和签名设置。如果还能更新 AdditionalUserInfo.aspx 以显示登录用户的留言板评论,这就更好了。即,除了检查和修改信息外,用户还可访问 AdditionalUserInfo.aspx 页面来查看过去发表的评论。 我将 这留给有兴趣的读者进行练习。 步骤6 : 自定义CreateUserWizard 控件以包含家乡、主页和签名的界面Guestbook.aspx 页面用到的 SELECT 查询用一个内联接来合并GuestbookComments 、UserProfiles 和 aspnet_Users 表中的相关记录。如果一位在 UserProfiles 中没有记录的用户在留言板中发表了评论,则该评论不会在 ListView 中显示,因为只有在 UserProfiles 和 aspnet_Users 表中有匹配记录时,内联接才返回 GuestbookComments 记录。如我们在步骤 3 中看到的那样,如果一位用户在 UserProfiles 表中没有记录,则他 / 她不能在 AdditionalUserInfo.aspx 页面中查看或编辑自己的设置。 毫无疑问,由于我们的设计理念, Membership 系统中的每个用户帐户在 UserProfiles 表中都有一条匹配记录,这一点很重要。我们想要实现的是:只要通过 CreateUserWizard 创建了一个新 Membership 用户帐户,就在 UserProfiles 表中添加一条相应的记录。 正如在 《创建用户帐户 》 教程中讨论的那样 ,CreateUserWizard 控件创建了新 Membership 用户帐户后将触发它的 CreatedUser 事件 。我们可为该事件创建一个事件处理程序,获取刚创建的用户的 UserId ,然后在 UserProfiles 表中插入一条记录,其中 HomeTown 、 HomepageUrl 和 Signature 列都采用默认值。此外,我们还可以自定义 CreateUserWizard 控件的界面,添加一些文本框,用于提示用户输入上述各列的值。 我们先看一下如何在 CreatedUser 事件处理程序中使用默认值向 UserProfiles 表添加一条新记录。然后,我们将探讨如何自定义 CreateUserWizard 控件的用户界面,在其中包含一些表单域来收集新用户的家乡、主页和签名信息。 向 UserProfiles 表添加默认记录在《创建用户帐户 》教程中 , 我们在Membership 文件夹中的 CreatingUserAccounts.aspx 页面中添加了一个 CreateUserWizard 控件。为使 CreateUserWizard 控件在创建用户帐户时向 UserProfiles 表添加一条记录,需要更改 CreateUserWizard 控件的功能。我们不是在 CreatingUserAccounts.aspx 页面中进行这些改动,而是在 EnhancedCreateUserWizard.aspx 页面中添加一个新的 CreateUserWizard 控件,然后进行本教程所需的更改。 在 Visual Studio 中打开 EnhancedCreateUserWizard.aspx 页面,从工具栏中将一个 CreateUserWizard 控件拖放到页面上。将该 CreateUserWizard 控件的 ID 属性设置为 NewUserWizard 。如我们在《创建用户帐户 》教程中讨论的那样, CreateUserWizard 的默认用户界面提示访问者输入必要的信息。一旦提供了这些信息,控件内部就在 Membership 框架中创建一个新用户帐户,而我们不必写一行代码。 CreateUserWizard 控件在其工作流过程中会触发一系列事件。访问者提供要求的信息并提交表单后, CreateUserWizard 控件首先触发其 CreatingUser 事件 。如果创建过程中出现问题,则触发CreateUserError 事件 ;如果创建用户成功,则触发 CreatedUser 事件 。在《创建用户帐户 》教程中,我们为 CreatingUser 事件创建了一个事件处理程序,以确保提供的用户名的前后没有空白,并且用户名不会出现在密码中。 为在 UserProfiles 表中为刚创建的用户添加一条记录,我们需要为 CreatedUser 事件创建一个事件处理程序。 CreatedUser 事件被触发时,用户帐户已在 Membership 框架中创建,因此我们可以提取该帐户的 UserId 值。 为 NewUserWizard 的 CreatedUser 事件创建一个 事件处理程序, 并添加以下代码 :
上述代码首先获取刚添加的用户帐户的UserId 。这是通过使用 Membership.GetUser(username) 方法返回特定用户的信息,然后使用 ProviderUserKey 属性提取该用户帐户的 UserId 来实现的。用户在 CreateUserWizard 控件中输入的用户名可通过 UserName 属性 来获取。 接下来,从 Web.config 中获取连接字符串,并指定 INSERT 语句。然后实例化必要的 ADO.NET 对象,并执行命令。代码将一个 DBNull 实例分配给 @HomeTown 、 @HomepageUrl 和 @Signature 参数,这可达到为 HomeTown 、 HomepageUrl 和 Signature 列插入数据库 NULL 值的效果。 通过浏览器访问 EnhancedCreateUserWizard.aspx 页面,并创建一个新用户帐户。完成后,返回 Visual Studio 并检查 aspnet_Users 和 UserProfiles 表的内容(如我们在图 12 中做的那样)。你应该会看到 aspnet_Users 表中的新用户帐户以及相应的 UserProfiles 记录( HomeTown 、 HomepageUrl 和 Signature 为空值)。 图20 : 添加了新用户帐户和 UserProfiles 记录 (单击此处查看实际大小的图像 ) 访问者提供新帐户信息并单击 “Create User” 按钮后 , 系统创建新用户帐户 , 并在 UserProfiles 表中添加一条记录。然后, CreateUserWizard 显示其 CompleteWizardStep ,该 CompleteWizardStep 显示一条成功消息和一个 Continue 按钮。单击 Continue 按钮会导致一次回传,但什么都不会发生,用户仍然停留在 EnhancedCreateUserWizard.aspx 页面。 我们可以通过CreateUserWizard 控件的 ContinueDestinationPageUrl属性 来指定一个 URL , 在用户单击 Continue 按钮时将用户导航到该 URL 。将 ContinueDestinationPageUrl 属性设置为 “~/Membership/AdditionalUserInfo.aspx” ,这会将新用户导航到 AdditionalUserInfo.aspx ,用户可在该页面查看和更新他们的设置。 自定义 CreateUserWizard 的界面以提示新用户输入家乡、主页和签名CreateUserWizard 控件的默认界面完全可以实现简单的帐户创建场景 , 因为这些场景只需要收集核心用户帐户信息 , 如用户名、密码和电子邮箱地址。但如果在创建帐户时想提示访问者输入其家乡、主页和签名,该怎么办呢?我们可以自定义 CreateUserWizard 控件的界面,以便在注册时收集其它信息, CreatedUser 事件处理程序在向基础数据库插入其它记录时会使用这些信息。 CreateUserWizard 控件扩展了 ASP.NET Wizard 控件,后者允许页面开发人员定义一系列有序的 WizardStep 。 Wizard 控件呈现活动步骤,并提供允许访问者在这些步骤间切换的导航界面。该控件适合将一个长时间运行的任务分解成几个简短的步骤。有关 Wizard 控件的更多信息,请参阅使用 ASP.NET 2.0 Wizard 控件创建分步的用户界面 。 CreateUserWizard 控件的默认标记定义了两个 WizardStep : CreateUserWizardStep 和 CompleteWizardStep 。
第一个WizardStep , 即 CreateUserWizardStep , 呈现提示输入用户名、密码和电子邮箱地址等信息的界面。访问者提供这些信息并单击 “Create User” 后,屏幕将显示 CompleteWizardStep ,该 CompleteWizardStep 显示成功消息和一个 Continue 按钮。 要自定义 CreateUserWizard 控件的界面以包含其它表单域,我们可以:
有一点务必要明确 , 在CreateUserWizardStep 内单击 “Create User” 按钮时 ,CreateUserWizard 控件将执行用户帐户创建过程。这跟 CreateUserWizardStep 的后面是否有其它 WizardStep 没有关系。 当向 CreateUserWizard 控件添加一个自定义的 WizardStep 以收集其它的用户输入时,可将该自定义的 WizardStep 放在 CreateUserWizardStep 的前面或者后面。如果将它放在 CreateUserWizardStep 的前面,则 CreatedUser 事件处理程序可以使用在该自定义 WizardStep 中收集到的其它用户输入。然而,如果将自定义的 WizardStep 放在 CreateUserWizardStep 的后面,则显示自定义的 WizardStep 时,新用户帐户已创建完毕, CreatedUser 事件也已经触发了。 图 21 显示的是在 CreateUserWizardStep 的前面添加 WizardStep 时的工作流。由于在 CreatedUser 事件触发时,系统已经收集到其它用户信息,我们要做的就是更新 CreatedUser 事件处理程序,以获取这些输入并将这些输入作为 INSERT 语句的参数值(而不是使用 DBNull.Value )。 图21 : 在 CreateUserWizardStep 的前面添加 WizardStep 时 CreateUserWizard 的工作流 (单击此处查看实际大小的图像 ) 然而 , 如果将自定义的 WizardStep 放在 CreateUserWizardStep 的后面 , 创建用户帐户过程发生在用户输入其家乡、主页和签名信息之前。在这种情况下,用户帐户创建后,需要将这些信息插入到数据库中,如图 22 所示。 图22 : 在 CreateUserWizardStep 的后面添加 WizardStep 时 CreateUserWizard 的工作流 (单击此处查看实际大小的图像 ) 图22 中所示的工作流在步骤 2 结束时向UserProfiles 表插入一条记录。然而,如果访问者在步骤 1 完成后关闭了浏览器,我们将得到这样的一个状态:用户帐户已创建,但没有向 UserProfiles 表添加相应的记录。一个解决办法是在 CreatedUser 事件处理程序(步骤 1 完成后触发的)中向 UserProfiles 表添加一条记录,不过相应的列为空值或默认值,然后在步骤 2 完成后更新该记录。这可确保即使用户中途退出注册过程,系统也会为该用户帐户添加一条 UserProfiles 记录。 对于本教程,我们会创建一个新的 WizardStep ,将它放在 CreateUserWizardStep 的后面, CompleteWizardStep 的前面。我们会首先创建 WizardStep 并进行配置,然后查看所使用的代码。 在 CreateUserWizard 控件的智能标记中,选择 “Add/Remove WizardSteps” ,这将打开 WizardStep Collection Editor 对话框。添加一个新的 WizardStep ,将其 ID 设置为 UserSettings , Title 设置为 “Your Settings” , StepType 设置为 Step 。然后将它放在 CreateUserWizardStep (“Sign Up for Your New Account”) 的后面, CompleteWizardStep (“Complete”) 的前面,如图 23 所示。 图23 : 在 CreateUserWizard 控件中添加一个新的 WizardStep (单击此处查看实际大小的图像 ) 单击 OK 关闭 WizardStep Collection Editor 对话框。CreateUserWizard 控件的最新声明标记中包含了该WizardStep :
注意新的<asp:WizardStep> 元素。我们需要在此处添加收集新用户的家乡、主页和签名信息的用户界面。我们可在声明语法中或通过设计器输入该内容。要使用设计器,在智能标记中的下拉列表中选择 “Your Settings” 步骤,以便在设计器中查看该步骤。 注意 : 在智能标记的下拉列表中选择一个步骤会更新CreateUserWizard 控件的 ActiveStepIndex 属性 , 该属性指定开始步骤的索引。因此,如果在设计器中使用该下拉列表来编辑 “Your Settings” 步骤,一定要将开始步骤的索引设置回 “Sign Up for Your New Account” ,从而在用户首次访问 EnhancedCreateUserWizard.aspx 页面时显示 “Sign Up for Your New Account” 步骤。 在 “Your Settings” 步骤内创建一个用户界面,该界面包含三个 TextBox 控件,分别为 HomeTown 、 HomepageUrl 和 Signature 。构建完该界面后, CreateUserWizard 的声明标记应如下所示:
然后 , 通过浏览器访问该页面 , 创建一个新用户帐户 , 并指定家乡、主页和签名的值。完成 CreateUserWizardStep 后,系统会在 Membership 框架中创建该用户帐户, CreatedUser 事件处理程序开始运行。该处理程序会在 UserProfiles 表中添加一条新记录,但 HomeTown 、 HomepageUrl 和 Signature 的值为数据库空值,没有使用用户输入的家乡、主页和签名值。最终结果是,新用户帐户有一条 UserProfiles 记录,但 HomeTown 、 HomepageUrl 和 Signature 列的值还未指定。 我们需要在 “Your Settings” 步骤之后执行代码,获取用户输入的家乡、主页和签名值,并更新相应的 UserProfiles 记录。每次用户在一个 Wizard 控件的步骤之间切换时,都会触发 Wizard 的 ActiveStepChanged 事件 。我们可以为该事件创建一个事件处理程序,在 “Your Settings” 步骤完成后更新 UserProfiles 表。 为 CreateUserWizard 的 ActiveStepChanged 事件创建一个事件处理程序,并添加以下代码:
上述代码首先确定我们是否是刚到达 “Complete” 步骤。由于 “Your Settings” 步骤完成后立即进入 “Complete” 步骤,则当访问者进入 “Complete” 步骤时,表明他 / 她刚完成 “Your Settings” 步骤。 在这种情况下,我们需要通过编码引用 UserSettings WizardStep 内的 TextBox 控件。为此,首先使用 FindControl 方法通过编码引用 UserSettings WizardStep ,然后再引用该 WizardStep 内的 TextBox 控件。一旦引用了 TextBox 控件,我们就可以执行 UPDATE 语句了。 UPDATE 语句的参数个数与 CreatedUser 事件处理程序中的 INSERT 语句的参数个数相同,但此处我们使用的是用户提供的家乡、主页和签名值。 创建好该事件处理程序后,通过浏览器访问 EnhancedCreateUserWizard.aspx 页面,创建一个新用户帐户,并指定家乡、主页和签名的值。创建完新帐户后,你应该会被重定向到 AdditionalUserInfo.aspx 页面,刚才输入的家乡、主页和签名信息会在该页面中显示。 注意 : 我们的网站目前有两个可供访问者创建新帐户的页面: CreatingUserAccounts.aspx 和 EnhancedCreateUserWizard.aspx 。网站的站点地图和登录页面指向 CreatingUserAccounts.aspx 页面,但 CreatingUserAccounts.aspx 页面不会提示用户输入其家乡、主页和签名信息,也不会在 UserProfiles 表中添加相应的记录。因此,我们或者更新 CreatingUserAccounts.aspx 页面以提供上述功能,或者更新站点地图和登录页面来引用 EnhancedCreateUserWizard.aspx (而不是 CreatingUserAccounts.aspx )。如果选择后者,则一定要更新 Membership 文件夹中的 Web.config 文件,以便允许匿名用户访问 EnhancedCreateUserWizard.aspx 页面。 小结在本教程中,我们探讨了对与 Membership 框架内的用户帐户有关的数据建立模型的技术。我们特别讨论了如何对与用户帐户有一对多关系的实体建立模型,以及如何对与用户帐户有一对一关系的数据建立模型。此外,我们还研究了如何显示、插入和更新相关信息。本文中的一些示例使用 SqlDataSource 控件,而其它示例使用了 ADO.NET 代码。 本教程结束了我们对用户帐户的探讨。从下一篇教程开始,我们会将重心转向角色。在接下来的几篇教程中,我们将探讨 Roles 框架,研究如何创建新角色、如何将角色分配给用户、如何确定某个用户属于什么角色,以及如何应用基于角色的授权。 快乐编程!
|