零代码 Web 站点(转载)

零代码 Web 站点
转载: http://www.bbs3j.com/show.aspx?id=103&cid=5

此应用程序演示 ASP.NET 2.0 中的改进,使您能够创建一个强大的交互 Web 应用程序,而无需编写任何 Microsoft Visual Basic 代码。

新概念

大多数重要的 Web 应用程序都需要某种数据存储,不管它是 Microsoft SQL Server 数据库、Microsoft Access 数据文件、XML 文件还是某些其他类型的数据源。根据应用程序的复杂程度,UI 显示直接从数据存储检索的数据,或者从一组业务对象获得的数据。在 ASP.NET 1.1 中,即使一个简单的 Web 页(用于显示直接来自数据库的产品列表)也需要大量代码来连接数据库,并将数据绑定到服务器控件。在 ASP.NET 2.0 中,许多常见的数据访问情况可以只通过声明 ASPX 标记来实现 — 不要求编程。

ASP.NET 2.0 中新的“零代码”功能涵盖最常见的数据访问情况。最简单的情况是使用一个服务器控件显示数据源数据的单向数据绑定。这种情况的常见例子是在下拉列表中显示州/省/市/自治区列表。一种更复杂的情况是使用多个控件的单向数据绑定,其中在一个控件中选择的值会影响在另一个依赖控件中显示的值 — 例如,在下拉列表中显示国家/地区列表,然后当选中某个国家/地区时,在另一个下拉列表中显示该国家特有的州/省/市/自治区列表。对于某些服务器控件,ASP.NET 2.0 甚至包括内置的排序和分页功能。

ASP.NET 2.0 中令人印象最深刻的“零代码”功能指:能够轻松地提供添加、更新和删除数据的用户界面和数据访问逻辑,而无需像在 ASP.NET 1.1 中一样编写这些基本操作所需的基础结构代码。

声明性语言

零代码 Web 站点这个短语可能有点用词不当,因为 ASP.NET 标记语言(即 ASPX 语言)正逐渐被认为是一种声明性编程语言。如果您具有严格的命令式编程背景,即使用过诸如 C、S、Java 和 Visual Basic 等编程语言,那么将 ASPX 视为一种编程语言也许听起来有点令人困惑,因为声明性编程是一种很不一样的编程模型。对于许多程序员而言,基于 XML 的语言“感觉”根本不像一种编程语言。命令性编程往往相当直接地意味着非常低级的计算机操作方式。在任何特定的时刻,程序都处于通过执行所提供的指令而达到的特定状态。通过执行其他指令,程序会以一个新状态结束。当用命令性语言编程时,您通过提供实现目标所需的特定操作(即,告诉计算机如何实现目标)来实现期望的目标。当用声明性语言编程时,您指定目标,而编译器或解释器使用它预定义的算法来确定实现该目标的适当操作。

如果不习惯考虑声明性编程,这些概念可能听起来有点陌生,但您可能已经不知不觉地成为了一名更有经验的以声明性方式编程的程序员。考虑以下声明性代码:

SELECT * FROM Products WHERE ProductID = 55

您可能会认为这是 SQL 代码。SQL 是一个非常流行的声明性语言实例。SELECT 查询表示一个最终结果:选择 Products 表中 ProductID列值为 55 的各行的所有列。将这种查询表示为一组可执行的操作,该任务由该查询所发送到的任何数据库的查询处理引擎来完成。现在考虑一个 ASP.NET 标记块:

<form runat="server"> 
<asp:TextBox id="txtCountry”Text="Canada”runat="server"/> 
<asp:Button id="btnSave”Text="Save”runat="server”/> 
</form>

该“代码”没有包含一组指令,而是定义一个最终结果:一个窗体包含一个用“Canada”预填充的文本框和一个标记为“Save”的按钮。ASP.NET 有一个预定算法,可以将这些声明转换成适合不同类型浏览器的输出(例如,适合 Internet Explorer 的 HTML)。不必提供获取正确输出所需的任何特定操作,因为 ASP.NET 替您负责这些工作。

正如本节中的应用程序所演示的,ASP.NET 标记只需很少的编程工作就可以提供很多功能。使用 ASPX 语言不只可以控制布局或到 HTML 的简单映射,ASPX 还允许定义行为,包括丰富的用户界面和数据库通信。当使用 ASPX 时,您使用的远不只是用于编码用户界面的数据格式 — 您正在使用一种最新和最流行的声明性语言进行编程。

数据绑定

在发布 .NET Framework 之前,数据绑定的名声不佳,因为对于需要复杂行为的应用程序而言,使用 Visual Basic 6 非常麻烦。.NET Framework 1.0 版引入新的数据绑定功能,该功能模仿大多数开发人员自己编码以获取数据绑定行为的做法。.NET Framework 的一个最了不起的功能是:可以使 Web 开发人员在 Web 窗体上使用高效而简单的单向数据绑定。只要提供一个可绑定的服务器控件(例如具有数据源的 DropDownList),该服务器控件就可以负责循环数据,并为每个数据项生成适当的 HTML。虽然这样做非常简单,但 ASP.NET 1.1 中的数据绑定仍需要代码来创建数据源,将数据源分配给适当的数据控件,然后调用数据控件的 DataBind方法来实际使控件循环访问数据源。在 ASP.NET 2.0 中,只需封装数据源设置和 ASPX 中的绑定,而让服务器控件负责其余的工作。

演练

ASP.NET 2.0 中最简单的数据绑定是数据源数据的只读表格显示。使用 Visual Studio 2005 IDE,首先创建一个新的 Web 窗体,并将数据源控件拖到该窗体上。此应用程序使用 SqlDataSource控件,不过还可以使用其他数据源控件,例如 Microsoft Access 数据文件或 XML 文档。SqlDataSource控件负责连接到 Microsoft SQL Server 数据库并检索所请求的数据。

当将新的 SqlDataSource控件添加到窗体时,Data Source Configuration Wizard 可以指导您设置该控件。第一步是建立该控件与数据库通信的连接设置。图 6-1 显示已经配置第一步中到 Northwind 数据库的连接。


6-1 Data Source Configuration Wizard。

接着,指定希望数据源控件从数据源选择的数据。此应用程序使用一个简单的查询来选择 Northwind 数据库中 Products 表的所有行:

SELECT * FROM [Products]

有了连接和 SELECT 查询,SqlDataSource控件就具备从数据库检索数据所需的全部信息。向导所生成的 ASPX 标记的结果为:

<asp:SqlDataSource ID="ProductsDS”Runat="server” 
SelectCommand="SELECT * FROM [Products]" 
ConnectionString="<%$ ConnectionStrings:NorthwindConnection %>"> 
</asp:SqlDataSource>

您将注意到,以声明方式从 web.config 配置文件读取连接字符串使用了新语法。web.config 中对应的项如下所示(为提高可读性,添加了换行符):

<connectionStrings> 
<add name="NorthwindConnection" 
connectionString="Server=(local); 
Integrated Security=True; 
Database=Northwind; 
Persist Security Info=True" 
providerName="System.Data.SqlClient”/> 
</connectionStrings>

既然 Web 窗体有了数据源控件,您就可以添加支持数据绑定的其他控件,并将它们绑定到数据源控件。对于简单的表格显示,新的 GridView控件不需要特殊的配置 — 只要为其提供数据源控件的 ID,它就可以生成完整的数据表。可以使用 GridView的 Common Tasks 智能标记来分配数据源,或者只使用下面的 ASPX 标记来设置 GridView 的 DataSourceID属性:

<asp:GridView ID="ProductsGrid”Runat="server”DataSourceID="ProductsDS"> 
</asp:GridView>

可视化 Web 设计器中的最终结果如图 6-2 所示。SqlDataSource控件在设计视图中是可见的,但当在 Internet Explorer 中查看时,只有由 GridView生成的数据表是可见的,如图 6-3 所示。


6-2 使用零代码绑定到SqlDataSource 的 GridView。


6-3 在 Internet Explorer 中查看的最终输出。

双向数据绑定

虽然使用零代码进行单向数据绑定非常方便,但创建快速只读显示的总工作效率优势不是那么显著。在 ASP.NET 1.1 中,更可怕的工作是为 CRUD(创建、读取、更新和删除)操作进行双向数据绑定,并提供一个可用的界面,包含诸如排序和分页这样的功能(例如,每页显示 10 条记录,并有 Next 和 Previous 按钮)。ASP.NET 2.0 简化了双向数据绑定工作,并同样需要零代码。奥秘在于数据源和 GridView控件的功能。

在此应用程序的第一页,可以看到如何创建配置为只选择数据的 SqlDataSource 控件。但 SqlDataSource控件还可以用于更新、插入和删除数据。对于不同类型的查询,SqlDataSource控件只需适当的 SQL 命令和参数。可以自己输入多行 ASPX 标记,也可以使用 Data Source Configuration Wizard 的高级功能来创建标记。

在为 Data Source Configuration Wizard 设置数据选择后,“Advanced Options”按钮就可以使用了,它打开图 6-4 所示的对话框窗口。选中第一个复选框将提示向导利用您提供的选择信息来探测数据库的数据结构。查询完数据库的结构后,向导将自动创建 SqlDataSource控件对数据库执行 CRUD 操作所需的全部 ASPX 标记。


6-4 Data Source Configuration Wizard 的高级 SQL 生成选项。

在 Visual Studio .NET 2003 中,向导(例如 Data Adapter Wizard)可以帮助您创建进行 CRUD 操作的代码,但结果是 Visual Basic(或者 C#)代码,然后必须将其以编程方式绑定到数据控件。另外,将生成的代码放置在代码文件的“设计器生成的”区域,在这里进行更改会有极大的危险,因为如果再次调用向导这些更改将丢失。将生成的代码移出“设计器生成的”区域意味着不能使用向导来更改适配器设置。相比之下,Visual Studio 2005 中的 Data Source Configuration Wizard 生成的 ASPX 标记用相应的参数声明性地表示更新、插入和删除命令。下面的 ASPX 清单显示向导为 Northwind Products 表生成的标记。

<asp:SqlDataSource ID="SqlDataSource1”Runat="server” 
SelectCommand="SELECT [Products].* FROM [Products]" 
ConnectionString="<%$ ConnectionStrings:NorthwindConnection %>“ 
DeleteCommand="DELETE FROM [Products] 
WHERE [Products].[ProductID] = @ProductID" 
InsertCommand="INSERT INTO [Products] ([Products].[ProductName], 
[Products].[SupplierID], [Products].[CategoryID], 
[Products].[QuantityPerUnit], [Products].[UnitPrice], 
[Products].[UnitsInStock], [Products].[UnitsOnOrder], 
[Products].[ReorderLevel], [Products].[Discontinued]) 
VALUES (@ProductName, @SupplierID, @CategoryID, 
@QuantityPerUnit, @UnitPrice, @UnitsInStock, 
@UnitsOnOrder, @ReorderLevel, @Discontinued)" 
UpdateCommand="UPDATE [Products] SET 
[Products].[ProductName] = @ProductName, 
[Products].[SupplierID] = @SupplierID, 
[Products].[CategoryID] = @CategoryID, 
[Products].[QuantityPerUnit] = @QuantityPerUnit, 
[Products].[UnitPrice] = @UnitPrice, 
[Products].[UnitsInStock] = @UnitsInStock, 
[Products].[UnitsOnOrder] = @UnitsOnOrder, 
[Products].[ReorderLevel] = @ReorderLevel, 
[Products].[Discontinued] = @Discontinued 
WHERE [Products].[ProductID] = @ProductID"> 
<DeleteParameters> 
<asp:Parameter Type="Int32”Name="ProductID"></asp:Parameter> 
</DeleteParameters> 
<UpdateParameters> 
<asp:Parameter Type="String”Name="ProductName"></asp:Parameter> 
<asp:Parameter Type="Int32”Name="SupplierID"></asp:Parameter> 
<asp:Parameter Type="Int32”Name="CategoryID"></asp:Parameter> 
<asp:Parameter Type="String”Name="QuantityPerUnit”/> 
<asp:Parameter Type="Decimal”Name="UnitPrice"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="UnitsInStock"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="UnitsOnOrder"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="ReorderLevel"></asp:Parameter> 
<asp:Parameter Type="Boolean”Name="Discontinued"></asp:Parameter> 
<asp:Parameter Type="Int32”Name="ProductID"></asp:Parameter> 
</UpdateParameters> 
<InsertParameters> 
<asp:Parameter Type="String”Name="ProductName"></asp:Parameter> 
<asp:Parameter Type="Int32”Name="SupplierID"></asp:Parameter> 
<asp:Parameter Type="Int32”Name="CategoryID"></asp:Parameter> 
<asp:Parameter Type="String”Name="QuantityPerUnit”/> 
<asp:Parameter Type="Decimal”Name="UnitPrice"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="UnitsInStock"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="UnitsOnOrder"></asp:Parameter> 
<asp:Parameter Type="Int16”Name="ReorderLevel"></asp:Parameter> 
<asp:Parameter Type="Boolean”Name="Discontinued"></asp:Parameter> 
</InsertParameters> 
</asp:SqlDataSource>

现在,其他数据控件(如 GridView)可用于更改数据库中的数据(通过 SqlDataSource控件)。为了使 GridView控件能够更改数据,在“Properties”窗口中将 AutoGenerateDeleteButton和 AutoGenerateEditButton属性设置为 True,如图 6-5 所示。


6-5 启用数据更改的 GridView 属性。

所得到的标记与单向数据绑定示例没有太大的不同。添加的只是具有两个属性的 CommandField元素,它告诉 ASP.NET 生成编辑和删除链接按钮:

<asp:GridView ID="GridView1”Runat="server” 
DataSourceID="SqlDataSource1”DataKeyNames="ProductID"> 
<Columns> 
<asp:CommandField ShowDeleteButton="True” 
ShowEditButton="True"> 
</asp:CommandField> 
</Columns> 
</asp:GridView>

当在浏览器中查看该 GridView 时,可以看到每行数据的 Edit 与 Delete 链接。如果单击 Edit 链接,该行就变成一组可编辑的控件(一般是文本框),如图 6-6 所示。单击“Update”将更改保存到基础数据源(本例为 Microsoft SQL Server)。这就是不用编写一行代码的双向数据绑定!


6-6 在 Internet Explorer 中使用零代码编辑产品。

修饰零代码站点

这些零代码功能的确令人注目,但目前在视觉上不是太有感染力或者不是太用户友好。随着家庭和商务用户越来越习惯于利用 Web 来工作和娱乐,他们要求 Web 页更具有视觉感染力 — 即使是编辑库存数据。幸运的是,ASP.NET 2.0 对格式设置有大量的支持,换句话说,它可以改善零代码 Web 站点的外观。例如,GridView控件允许您控制颜色、字体和布局信息来创建具有吸引力并且实用的数据表。

为数据控件选择一个专业的预打包外观的最快方法是使用 Auto Format 功能,通过“Common Tasks”智能标记窗口可以访问该功能,如图 6-7 所示。还可以利用“Common Tasks”窗口来启用 GridView控件的排序和分页功能。如果您曾经尝试使用 ASP.NET 1.1 中的分页(例如 DataGrid),您就会意识到使用 GridView控件是多么简单 — 只需选中一个复选框即可。GridView和 SqlDataSource负责控制分页的其余细节。


6-7 GridView Common Tasks 智能标记对话框窗口。

Auto Format”对话框允许从许多预构建的格式中进行选择,这些格式包括颜色、字体、行的样式、可选行样式、选定的行样式、标题行样式,等等。图 6-8 所示的“Auto Format”对话框用于为支持分页(每页最多 10 条记录)但不支持排序的 GridView选择一种格式。


6-8 “Auto Format”对话框窗口。

Visual Studio 2005 中的 Auto Format 功能为选定的控件生成许多元素和属性。利用 Sand & Sky 格式生成下面的标记:

<asp:GridView ID="GridView1”Runat="server” 
AutoGenerateColumns="False”DataKeyNames="ProductID" 
DataSourceID="SqlDataSource1”BorderWidth="1px” 
BackColor="LightGoldenrodYellow" 
GridLines="None”CellPadding="2” 
BorderColor="Tan”ForeColor="Black"> 
<FooterStyle BackColor="Tan"></FooterStyle> 
<PagerStyle ForeColor="DarkSlateBlue”HorizontalAlign="Center” 
BackColor="PaleGoldenrod"> 
</PagerStyle> 
<HeaderStyle Font-Bold="True”BackColor="Tan"></HeaderStyle> 
<AlternatingRowStyle BackColor="PaleGoldenrod"></AlternatingRowStyle> 
... 
</asp:GridView>

为每个网格复制所有这些格式设置,从而使 Web 站点中数据网格的格式保持相同,这是一项非常费力的任务。幸运的是,通过创建一个新的主题(将在下一个应用程序中说明)可以将这种格式统一应用于 Web 站点。

当启用分页时,可以利用 GridView控件的 PageSize属性来更改每页显示的数据行数。

小结

每一个 Web 应用程序都是独特的,但总会有一些相似之处。一个相似之处是用于只读显示的基本数据访问,或用于查看和修改数据的界面。ASP.NET 2.0 中新的零代码功能使得编写有用的 Web 应用程序构造块而无需编写任何代码成为可能!利用向导和其他设计时功能,Visual Studio 2005 可以生成定义重要 Web 应用程序的所有 ASPX 标记。数据访问只是使用零代码功能可以实现的常见任务之一。在下面的两个应用程序中您将看到,无需编写任何代码还可以创建一个具有身份验证功能的 Web 站点,包含新用户注册和用户登录。

应用程序:成员身份、配置文件和角色

此应用程序重点介绍 ASP.NET 2.0 中新的成员身份、配置文件和基于角色的安全功能,这使得确保 Web 应用程序的安全性更简单。

新概念

大多数实际的 Web 应用程序都有一些受限区域,这些区域要求对用户进行身份验证,这样才能根据是否授权用户使用受保护的资源来授予或拒绝授予访问权限。常见的例子是更新内容或查看站点统计信息的管理区域。ASP.NET 支持几种身份验证方案,包括集成的 Windows 身份验证和 Passport 身份验证。Windows 和 Passport 身份验证仅对某些类型的应用程序是适合的,而对许多应用程序都不适用。Windows 身份验证要求每个用户都有一个用户帐户。Passport 身份验证对于小型和中型站点而言成本过高。因此,许多站点选择自定义身份验证逻辑和自定义数据存储来存储证书和其他与用户相关的数据,例如姓名、地址等。ASP.NET 1.1 站点可以利用 Forms 身份验证完成诸如设置身份验证 cookie 这样的工作,它们可以使用一个通用的 API 来决定某个用户是否登录,以及该用户属于什么授权角色。而创建管理用户帐户的基础结构是 Web 开发人员的责任。

结果是将无数的 ASP.NET Web 应用程序投入生产,每个应用程序中实现的身份验证模型几乎完全相同。这种常用的身份验证模型非常普遍,因此应当直接将它嵌入到 Web 应用程序平台中。而这正是 ASP.NET 2.0 提供的新的成员身份和用户配置文件功能。

成员身份

ASP.NET 2.0 中新的成员身份功能主要以三种方式简化用户管理:

1.自动创建数据存储。(ASP.NET 2.0 为 Access 和 Microsoft SQL Server 配备有内置的提供程序。)当尝试使用成员身份功能时,ASP.NET 2.0 检查是否配置了指定的数据存储。如果没有配置,ASP.NET 就创建。ASP.NET 2.0 中包含一个 Access 成员身份数据文件的模板和用于创建 Microsoft SQL Server 成员身份数据库的脚本。

2.它们包含创建和验证用户以及显示用户特定信息和登录状态的服务器控件。新控件(如 Login、LoginStatus、CreateUserWizard 和 ChangePassword控件)提供预构建的用户界面构造块,包括用于最常见的与成员身份相关的任务的功能。在新 Web 控件应用程序中重点介绍这些控件。

3.它们提供一种以编程方式管理用户的应用程序编程接口 (API)。Membership API 是通过 Membership 类访问的,包含非常有用的方法,例如 CreateUser、DeleteUser 和 ValidateUser。

利用 ASP.NET 2.0 成员身份,不用编写任何代码就可以实际创建一个具有受保护页、自动重定向到登录页、用户创建(注册)和用户登录的 Web 站点!在此应用程序的“演练”一节中您将看到,web.config 中的一些 XML 元素和一些服务器控件都是使用 ASP.NET 2.0 创建一个支持身份验证的 Web 应用程序所需要的。

用户配置文件

ASP.NET 2.0 中的成员身份功能允许您收集对于身份验证非常重要的基本用户信息,例如用户名、密码、电子邮件地址以及取回密码所需的问题/回答机密对。但经常需要收集和存储其他信息,例如姓名、Web 站点或 Weblog URL、职称,还可能更多。ASP.NET 2.0 中新的用户配置文件功能使您能够定义应用程序必须存储的有关用户的其他信息。只需在 web.config 中指定需要的信息,然后在运行时填充特殊 Profile 对象中的相应字段。

ASP.NET 2.0 生成的 Profile 对象是特定于应用程序的,并且包含的强类型属性映射到应用程序 web.config 文件中的项。在设计时,Visual Studio IDE 读取这些项,并自动构建一个专有类(从 HttpProfileBase 继承),这样就可以在“Intellisense”菜单中使用特定于应用程序的属性,并且可以通过编译器进行验证。此应用程序的“演练”一节说明如何在 web.config 中设置用户配置文件,然后说明如何在运行时访问特定于用户的配置文件数据。

基于角色的安全性

可以通过在 Web 站点上对用户进行身份验证来识别用户,但如果没有其他的基于角色的授权,就不能根据用户的身份限制其对 Web 资源的访问。ASP.NET 始终内置支持通过用户的安全用户对象来确定用户是否具有某种角色。使用 Windows 身份验证时,角色来自于 Windows 安全组。但是使用 ASP.NET 1.1 中的 Forms 身份验证时,必须构建自己的安全用户对象,并在对每个 Web 请求进行身份验证时填充它的角色集合。作为开发人员,您还要负责编写代码来维护用户角色的数据存储,以及在运行时加载这些数据。

通过引入 ASP.NET 角色管理,ASP.NET 2.0 扩展基于角色的安全功能,包括新的 Roles 类和 web.config 的 <roleManager>配置节。ASP.NET 角色管理提供最常用的基于角色的安全功能,这些功能在以前必须自己构建。Roles 类提供的 API 可以用于创建和删除角色,从角色中添加和删除用户,枚举角色,枚举具有某种角色的用户,枚举某个用户所具有的角色等。ASP.NET 角色管理负责持久化角色成员身份数据,在运行时加载角色成员身份数据,以及为用户的安全用户添加适当的角色。

演练

此应用程序演示如何使用 ASP.NET 2.0 中新的成员身份、配置文件和基于角色的安全功能。本演练包括有关配置这些新功能和以编程方式使用它们的详细信息。

成员身份

在 Visual Studio 2005 中配置 ASP.NET 成员身份非常容易。当在 Visual Studio 2005 中创建新 Web 站点时,解决方案资源管理器的外观如图 6-9 所示。


6-9 访问 Membership API 之前的解决方案资源管理器。

当在运行时访问 Membership API 时,系统会自动创建在 ASP.NET 配置中指定的成员身份数据库(如果它不存在)。ASP.NET 2.0 附带成员身份的 Microsoft Access 数据文件模板,以及一组用于创建 Microsoft SQL Server 成员身份数据库的 SQL 脚本。用于持久化成员身份数据的数据存储类型用 ASP.NET 配置来控制。如果没有在 web.config 中显式声明数据提供程序,则使用 machine.config 文件中的默认值。在此应用程序中,machine.config 中的默认值是 Microsoft Access 的提供程序,因此当访问 Membership API 时,系统会在 Web 站点的 Data 子文件夹中自动创建一个 Access 数据文件。


6-10 显示自动生成成员身份数据文件的解决方案资源管理器。

此应用程序中的第一个 Web 窗体访问 Membership API 中三个最有用的方法:CreateUser、ValidateUser 和 GetAllUsers。该 Membership API 还包括进行身份验证管理所需的其他类型的维护功能,例如通过电子邮件地址查找用户、修改用户、删除用户以及设置密码。

该 API 中的方法易于使用并且清楚易懂。例如,CreateUser方法的最简单版本采用两个易于理解的参数:用户名和密码。CreateUser的重载版本还采用电子邮件地址作为参数。您只需编写如下一行代码即可创建成员身份数据库中的新用户:

Membership.CreateUser(txtUserName.Text, txtPassword.Text)

CreateUser 方法调用可以创建数据库中的用户,但有时需要检索数据库中的数据,以便可以验证用户的凭据。只需调用 ValidateUser 方法即可自动完成所有这些工作:

If Membership.ValidateUser(txtUserName.Text, txtPassword.Text) Then 
FormsAuthentication.SetAuthCookie(txtUserName.Text, False) 
Response.Redirect(Request.Path) 
End If

Membership API 使得可以轻而易举地完成常见类型的用户管理任务,否则您必须编写这些任务的所有数据访问和业务逻辑。例如,要检索成员身份数据库中的所有用户列表,只需枚举 GetAllUsers方法返回的用户集合:

Dim users As MembershipUserCollection = Membership.GetAllUsers()

此应用程序中 Default.aspx 的全部源包括用于用户名和密码的 TextBox控件,以及用于创建用户或以用户身份登录的 Button 控件。该 Web 窗体还包含存放成员身份数据库中所有用户列表的 ListBox。用 Label 来显示登录状态。下面的 ASPX 标记和 Visual Basic 代码是处理成员身份的 Default.aspx 的子集(即,这些代码没有添加用户配置文件和基于角色的安全功能):

<%@ page language="VB”%> 
<script runat="server"> 
Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) 
Dim users As MembershipUserCollection = Membership.GetAllUsers() 
Dim userArray As New ArrayList(users.Count) 
For Each mu As MembershipUser In users 
userArray.Add(mu.Username) 
Next 
UserList.DataSource = userArray 
UserList.DataBind() 
If User.Identity.IsAuthenticated Then 
lblLoginStatus.Text = “You are currently authenticated as “+ _ 
User.Identity.Name 
End If 
End Sub 
Sub btnCreate_Click(ByVal sender As Object, _ 
ByVal e As System.EventArgs) 
Membership.CreateUser(txtUserName.Text, txtPassword.Text) 
End Sub 
Sub btnLogin_Click(ByVal sender As Object, ByVal e As System.EventArgs) 
If Membership.ValidateUser(txtUserName.Text, txtPassword.Text) Then 
FormsAuthentication.SetAuthCookie(txtUserName.Text, False) 
Response.Redirect(Request.Path) 
End If 
End Sub 
</script> 
<html> 
<head runat="server"> 
<title>Untitled Page</title> 
</head> 
<body> 
<form id="form1”runat="server"> 
<div> 
Username: <asp:TextBox ID="txtUserName” 
Runat="server"></asp:TextBox> 
<br /> 
Password: <asp:TextBox ID="txtPassword” 
Runat="server"></asp:TextBox> 
<br /> 
<asp:Button ID="btnCreate”Runat="server”Text="Create User” 
OnClick="btnCreate_Click”/> 
<asp:Button ID="btnLogin”Runat="server”Text="Login” 
OnClick="btnLogin_Click”/> 
<asp:Label ID="lblLoginStatus”Runat="server"></asp:Label> 
</div> 
<p> 
Users in Membership database: 
<br /> 
<asp:ListBox ID="UserList”runat="server"></asp:ListBox> 
</p> 
</form> 
</body> 
</html>

图 6-11 显示在运行时只添加成员身份功能的 Default.aspx。在新的 Web 控件应用程序中您将看到,ASP.NET 2.0 通过提供新的安全控件(如 Login和 LoginStatus控件)使身份验证的用户界面更简单。


6-11 Internet Explorer 中显示成员身份数据库内所有用户的 Web 窗体。

ASP.NET 中默认的身份验证模式是 Windows。为了使用 Forms 身份验证(此应用程序使用这种方式),必须将 web.config 中 <authentication> 元素的 Mode 属性设置为“Forms”。

用户配置文件

ASP.NET 2.0 中的用户配置文件有时称为用户个性化或配置文件个性化,在 Web 站点的 web.config 文件中进行配置。可以在该配置文件中以声明性方式声明希望为每个用户存储的数据,然后在某个特殊的 Profile 对象中填充对应的属性。下面的 web.config 文件子集配置 Web 站点来存储每个用户的 FirstName和 LastName字符串值:

<?xml version="1.0”?> 
<configuration> 
<system.web> 
<authentication mode="Forms”/> 
<profile inherits="System.Web.Profile.HttpProfileBase, System.Web, 
Version=2.0.3600.0, Culture=neutral, 
PublicKeyToken=b03f5f7f11d50a3a"> 
<properties> 
<add name="FirstName”type="System.String”/> 
<add name="LastName”type="System.String”/> 
</properties> 
</profile> 
</system.web> 
</configuration>

Visual Studio 2005 从 web.config 文件读取这些配置信息,并动态地创建从 HttpProfileBase 继承的专用类。该专用类包含 web.config 文件中定义的具有正确数据类型的所有属性。图 6-12 例举一个在 Intellisense 中可用的自定义配置文件属性。


6-12 在 Intellisense 中可用的强类型配置文件属性。

在运行时,ASP.NET 2.0 负责管理配置文件数据的持久性。作为程序员,您唯一的任务是读写这些数据。下面的代码说明如何设置 FirstName和 LastName属性,并将它们保存在配置文件数据存储中:

Profile.FirstName = txtFirstName.Text 
Profile.LastName = txtLastName.Text 
Profile.Save()

要从 Profile 对象读回数据,只需颠倒赋值语句即可:

txtFirstName.Text = Profile.FirstName 
txtLastName.Text = Profile.LastName

默认情况下,这些配置文件属性只能为经过身份验证的用户设置(因此也只为他们存储)。但在有些情况下,当用户匿名浏览站点时,您需要捕获信息;而当该用户登录站点时,您需要维护数据。ASP.NET 用户配置文件本身支持这种情况。必须使用 web.config 中的 <anonymousIdentification>元素显式启用匿名配置处理。然后,希望支持匿名用户的任何属性都必须将 allowAnonymous属性设置为 true。下面来自 web.config 的 XML 显示此应用程序的这些设置:

<anonymousIdentification enabled="true”/> 
<profile inherits="System.Web.Profile.HttpProfileBase, System.Web, 
Version=2.0.3600.0, Culture=neutral, 
PublicKeyToken=b03f5f7f11d50a3a”> 
<properties> 
<add name="FirstName”type="System.String”/> 
<add name="LastName”type="System.String”/> 
<add name="FirstVisit”type="System.String”allowAnonymous="true”/> 
</properties> 
</profile>

基于角色的安全性

ASP.NET 2.0 角色管理提供许多与创建和维护角色与角色成员身份有关的功能。要启用 Web 站点的角色管理,可以将 <role-Manager> 元素添加到 web.config 文件。在配置角色管理时,可以选择将用户的角色加密并缓存在 cookie 中。虽然缓存有助于减少往返数据存储以获得用户角色信息的次数,但它也带来一个问题:cookie 中的角色与在服务器端数据存储中最近更新的角色集不匹配。当大量更改角色成员身份时,这种差异可能特别容易产生问题。对于此应用程序,通过将 <roleManager>元素的 cacheRolesInCookie属性设置为 false 来禁用 cookie 缓存:

<roleManager enabled="true" 
cacheRolesInCookie="false”/>

现在每次处理经过身份验证的请求时,都将重新从数据存储中读取角色,并将其加载到用户的安全用户对象中。

在为用户分配角色之前,您必须创建角色。使用 Roles 类的 Create-Role 方法可以做到这一点。逐个应用程序存储角色,因此为该应用程序创建的角色与其他应用程序的角色完全无关。

Roles.CreateRole(“Admin”) 
Roles.CreateRole(“Editor”) 
Roles.CreateRole(“Reviewer”)

一旦创建了角色,就可以进行所需的维护任务,例如获得应用程序中所有角色的列表。GetAllRoles方法返回一个 String 对象数组,包含以前所创建角色的名称:

Dim allRoles() As String = Roles.GetAllRoles

既然角色已实际存在,就可以将用户分配给角色。为了一次将一个用户分配给一个角色,请使用 AddUserToRole方法:

Roles.AddUserToRole(UserList.SelectedValue, RolesList.SelectedValue)

如果想将多个用户同时添加给某个角色,可以使用 AddUsersToRole方法。如果想将一个用户添加给多个角色,可以使用 AddUserToRoles 方法。将多个用户同时添加给多个角色,可以使用 AddUsersToRole 方法。

ASP.NET 2.0 角色管理还添加了一种新功能,该功能解决 ASP.NET 1.1 中微不足道但却让人感到不便的限制 — 不用编写读取基础数据源的数据访问代码就能够获得某个用户所有角色的列表。

Roles.GetRolesForUser(User.Identity.Name)

图 6-13 显示如何将这些功能集中在一个简单的用户界面中,可以使用该界面创建用户,创建角色,将用户分配给角色,然后以用户的身份登录,并查看应用程序中所有的角色,以及单个用户所属的全部角色。


6-13 正在使用的成员身份、配置文件和角色。

小结

用户管理是一项几乎普遍存在的 Web 开发任务,它有许多已经确立的构建模式,包括使用用户名和密码的身份验证;存储用户配置文件信息(如姓名和地址);以及基于角色的授权。ASP.NET 1.1 提供一些对用户管理的支持 — 例如通用身份验证模型和内置区分经过身份验证的用户和匿名用户的功能 — 并提供一些对基于角色的授权的支持。但是在 ASP.NET 1.1 中,为常见用户管理任务创建与维护逻辑和数据存储的重担落到了开发人员身上。包括对凭据、配置文件和角色进行数据访问在内的相似代码已经由无数的开发人员反复编写过。最后,ASP.NET 2.0 支持将常见用户管理的琐碎工作直接内置在 ASP.NET Web 平台中。

应用程序:新的 Web 控件

此应用程序演示 ASP.NET 2.0 中一些新的 Web 控件,例如为新的 ASP.NET 2.0 成员身份系统提供预构建用户界面元素的新安全控件。

新概念

ASP.NET 2.0 中有许多新控件 — 多得实在无法在一个示例应用程序中逐一介绍。这些新控件包括新数据源控件、新数据绑定控件、新安全控件、新 Web 部件控件、新导航控件以及 ASP.NET 1.1 控件中新的移动识别功能。此应用程序重点介绍最适合一般 Web 开发的新控件 — 希望它们可以满足 Web 开发人员的一些最直接的需要。

安全控件

任何利用身份验证来验证用户身份或限制访问受保护资源的 Web 站点都需要某种用户界面来完成所有不同的身份验证任务,例如登录、注册(创建新帐户)、显示登录状态、显示个性化的注销链接以及重新设置密码。在 ASP.NET 1.1 中,所有这些用户界面组件都需要自己创建。在 ASP.NET 2.0 中,有一组新的安全控件,使用它们甚至无需编写一行代码就可以为站点添加基本的安全功能。

Visual Studio 2005 预览版包含的安全控件如下:

¦Login:这是一种绑定到 ASP.NET 成员身份功能的复合控件。它包含用于输入用户名和密码的 TextBox控件以及一个“Submit”按钮。该控件还包含许多可自定义的功能,包括设置可能出现的各种文本段(如指令文本、登录失败文本和控件标题文本)的格式。还可以选择让控件显示指向注册与密码恢复页的超级链接。Login控件甚至包含对用户名和密码进行所要求的内置字段验证。(可以禁用验证。)可以编写代码来支持 Login控件 — 例如,在 Authenticate 事件处理程序中 — 但这完全没有必要。即使通过绝对零代码最简单地使用 Login控件,也可以提供一个完整的工作身份验证界面。

¦ CreateUserWizard:该控件提供一个向导样式的界面,用于在成员身份系统中创建新用户。CreateUserWizard远不只是一个用于输入用户名和密码的简单窗体。它包括对其所有字段的字段验证,包括可选的对电子邮件地址进行的常规表达式验证。它提示用户两次输入建议的密码,并确认这两个密码是否匹配。如果基础成员身份提供程序支持安全问题,那么 CreateUserWizard控件还可以收集用户的安全问题和回答。此应用程序的“演练”一节还说明如何为向导添加自定义步骤,从而在注册的过程中收集用户的其他信息。

¦LoginName:该控件是显示用户名的占位符。如果当前用户是匿名的(即没有经过身份验证),则该控件不呈现任何输出。LoginName公开一个 FormatString属性,使用该属性,您不仅可以显示用户名,而且可以显示更多其他的信息。例如,可以将 FormatString设置为 ”Welcome, {}",当 UserTwo 登录时,这会产生 ”Welcome, UserTwo” 的输出。

¦LoginView:该控件提供可以在其中为匿名用户和经过身份验证的用户创建不同内容的模板。LoginView控件也是角色识别的,因此实际上您可以为具有不同角色的经过身份验证的用户创建不同的模板。

¦ PasswordRecovery:该控件提供用户界面和相应的功能,以帮助用户检索或重新设置其密码。该控件有三种视图:Username、Question 和 Success。Username 视图允许用户输入必须检索或重新设置密码的用户名。Question 视图提示用户在注册的过程中输入问题的回答。Success 视图在通过电子邮件提交密码后显示一条成功消息。可用于密码检索的选项由用来提供成员身份服务的 Membership 提供程序部分决定。ASP.NET 2.0 包含用于 Microsoft Access 和 Microsoft SQL Server 的提供程序。这两种提供程序都支持密码检索和安全问题。其他的提供程序(如第三方提供程序或您自己创建的提供程序)可能不完全支持这些功能。另外,默认情况下,这两个内置提供程序都存储密码哈希值。(明文和加密的文本是另外的选项。)无法用密码哈希值来确定原始密码,因此默认情况下,唯一可用的恢复方案就是重新设置密码,并通过电子邮件将新密码发送给用户。

¦LoginStatus:该控件根据当前用户的登录状态产生一个 Login 或 Logout 超级链接:匿名用户看见一个 Login 链接,而经过身份验证的用户看见一个 Logout 链接。当然,超级链接的实际文本与链接的目标文本一样都是可自定义的。Logout 链接可以配置为在用户注销后执行以下三项操作中的一项:刷新当前页,将用户重定向到登录页,或将用户重定向到其他页。

¦ ChangePassword:该复合控件为用户提供用于更改其密码的界面。默认情况下要求用户输入一次当前密码和两次新密码。和其他的安全控件一样,ChangePassword 可以完全自定义。

站点地图控件

新的 SiteMapPath控件提供内置在 ASP.NET 2.0 中部分新的站点地图解决方案。SiteMapPath 控件提供在许多 Web 站点上非常流行的“Breadcrumeb”导航界面。在 ASP.NET 1.1 中,您必须创建 Breadcrumeb 导航的所有逻辑或购买第三方解决方案。SiteMapPath控件只要求 Web 站点有一个名为 web.sitemap 的 XML 文件,该文件定义站点中页的层次结构。SiteMapPath控件确定当前页在站点地图层次结构中的位置,并自动创建 Breadcrumeb 导航。如果 SiteMapPath控件放置在没有包含在站点地图文件中的 Web 窗体上,SiteMapPath自然不呈现 Breadcrumeb 踪迹。

ASP.NET 2.0 还包含一个新的 SiteMapDataSource控件,该控件读取 web.sitemap 文件并充当其他控件(如 TreeView)的数据源。此应用程序的“演练”一节将说明如何创建 web.sitemap 文件,如何使用 SiteMapPath控件,以及如何使用 SiteMapDataSource控件创建站点地图树。

Substitution 控件(缓存后替换)

输出缓存是一种保存 ASP.NET Web 窗体输出并重用相同的输出来满足对同一页面多个请求的方法。当可以使用输出缓存时,它对于 Web 站点的性能非常有利。在 ASP.NET 1.1 中,无法更改单个用户缓存页的内容,由于一小部分页内容必须针对每个用户进行自定义(例如,个性化问候或用户购物车中商品的数量),因此许多 Web 页不能从输出缓存中受益。

页面片段缓存是一种缓存 Web 窗体上用户控件输出的方法。如果可以将 Web 窗体设计成一组用户控件,那么在控制必须个性化的页元素的同时,可以获得输出缓存的性能优势。但是页面片段缓存使用起来非常麻烦,因为必须将页面分解成用户控件以允许进行甚至是最简单的每个请求自定义。ASP.NET 2.0 引入一种从输出缓存中受益以及可以为每个用户进行自定义的新方法。这种新方法称为缓存后替换

缓存后替换通过将 Substitution 控件放置在 Web 窗体上,然后提供共享(静态)回调方法名来实现,ASP.NET 可以调用该方法来获得注入到缓存输出中的内容。虽然缓存后替换显然不能像纯粹的输出缓存那样提高性能,但调用回调方法的额外成本直接受回调方法性能特征的制约。

演练

本演练说明如何使用 ASP.NET 2.0 中新的安全 Web 控件,还演示如何使用新的站点地图控件,以及如何使用新 Substitution 控件进行缓存后替换。

安全控件

ASP.NET 2.0 引入七种新的安全控件来满足 Web 应用程序中最常见的身份验证和用户管理要求。在构建新的 Web 应用程序或将现有应用程序迁移到 ASP.NET 时,很少的时间投入(以发现这七种控件的功能)可以节省更多的时间。

Login 控件虽然它不是最复杂的安全控件,但可以认为 Login控件是核心安全控件,因为它提供实际收集证书并根据成员身份数据库对其进行验证的界面。Login控件包含相当数量的属性,这些属性允许您配置控件的显示和行为。图 6-14 显示一些可以在 Properties 窗口中编辑的 Login控件属性。


6-14 Login 控件的 Properties 窗口。

成功验证用户的证书后,可以配置 Login控件以将用户重定向到一个特定页。也可以在尝试登录失败后,配置 Login控件刷新当前页,或将用户重定向到站点的登录页。还可以更改该控件显示的所有文本,包括标签、指令、帮助文本、失败消息和控件标题。

此应用程序使用 Login控件的大部分默认设置。其 login.aspx 页上 Login控件的 ASPX 标记如下所示:

<asp:Login ID="Login1”Runat="server” 
CreateUserText="Register for a new account" 
CreateUserUrl="Register.aspx” 
PasswordRecoveryText="Forgot your password?” 
PasswordRecoveryUrl="PasswordRecovery.aspx”> 
</asp:Login>

CreateUserUrl 属性告诉 Login控件提供一个指向用户注册页的超级链接。CreateUserText 属性指定该超级链接的显示文本。同样,PasswordRecoveryUrl属性告诉 Login控件提供一个超级链接,指向用户可以在其中重新设置其密码的页面。PasswordRecoveryText 属性指定该超级链接的显示文本。图 6-15 显示登录尝试失败后 Internet Explorer 中的 Login控件。


6-15 登录尝试失败后运行时的 Login 控件。

CreateUserWizard控件要登录,首先必须创建一个帐户。正如在前一个应用程序中所看到的,可以通过 Membership.CreateUser方法完成该任务,但是这需要您构建自己的用户界面,并至少编写一行代码。使用 CreateUserWizard,只需向 Web 窗体添加一个控件并设置一些属性。该控件负责其余的细节。

实际上,CreateUserWizard是一个非常强大的控件,因为它允许您为向导添加自己的自定义步骤。在此应用程序中,已经为向导添加了新步骤以询问新用户所喜欢的颜色,然后使用内置的 ASP.NET 用户配置文件功能进行存储。

CreateUserWizard允许配置关于其外观和行为的几乎所有方面 — 从标签和提示到验证电子邮件地址所用的常规表达式。需要为该控件设置的一个属性是 ContinueDestinationPageUrl,它决定在成功创建一个新帐户后,CreateUserWizard控件将用户重定向到何处。下面的 ASPX 清单是定义此应用程序中 Register.aspx 页的 CreateUserWizard控件的标记。请注意,<asp:WizardStep>元素用来定义向导中的自定义步骤。

<asp:CreateUserWizard ID="CreateUserWizard1”Runat="server" 
ContinueDestinationPageUrl="default.aspx" 
OnContinueClick="CreateUserWizard1_ContinueClick"> 
<WizardSteps> 
<asp:WizardStep Runat="server”ID="SignStep” 
Title="What is your favorite color?” 
StepType="Step"> 
What is your favorite color?<br /> 
<asp:TextBox Runat="server”ID="txtColor"></asp:TextBox> 
</asp:WizardStep> 
</WizardSteps> 
</asp:CreateUserWizard>

图 6-16、6-17 和 6-18 显示用户创建新帐户所要经历的步骤。请注意,向导中的自定义步骤(图 6-17)询问用户喜欢的颜色。


6-16 显示 ConfirmPasswordCompareErrorMessage 的 CreateUserWizard 控件。


6-17 CreateUserWizard 自定义向导步骤。


6-18 CreateUserWizard 最后成功的步骤。

当用户单击最后成功步骤上的“Continue”按钮时,CreateUserWizard控件引发可为其编写事件处理程序的 ContinueClick事件。此时,用户将自动登录(除非您将 LoginCreatedUser属性更改为 false)。在事件处理程序中,您可以在自定义向导步骤中访问控件,并检索用户在创建帐户过程中输入的值。下面的代码说明如何获得对 txtColor控件的引用,并将用户喜欢的颜色保存在用户的配置文件中:

Sub CreateUserWizard1_ContinueClick(ByVal sender As Object, _ 
ByVal e As System.EventArgs) 
Dim txtColor As Control 
txtColor = CreateUserWizard1.FindControl(“txtColor”) 
If Not txtColor Is Nothing Then 
Profile.FavoriteColor = DirectCast(txtColor, TextBox).Text 
Profile.Save() 
End If 
End Sub

LoginName控件LoginName控件是这些新安全控件中最简单的控件。如果当前用户经过身份验证,那么 LoginName控件将显示用户名。如果当前用户没有经过身份验证,那么 LoginName控件将不显示任何输出。下面的 ASPX 标记说明如何在此应用程序中使用 LoginName 控件:

Thank you for using our site, 
<asp:LoginName ID="Loginname1”Font-Bold="true”Runat="server”/>

在本例中,用户名用粗体,但用户名前面的问候语不用粗体。如果问候语和用户名的字体粗细相同,可以按照如下方式将问候语放置在 FormatString属性中:

<asp:LoginName ID="Loginname1” 
FormatString=“Thank you for using our site, {0}" 
Runat="server”/>

LoginView控件如果您曾经创建过一个必须为不同类型的用户呈现不同内容的 Web 页,那么您一定知道该代码相当简单,但是创建和维护起来却非常乏味。即使在 ASP.NET 1.1 中,您也必须做许多工作以将内容放置在可以充当容器(例如,<div runat="server">)的服务器控件中,然后编写逻辑,从而根据查看页面的用户类型显示和隐藏服务器控件。

LoginView控件具有对用户角色的内置支持,因此可以根据角色为经过身份验证的用户创建不同的视图。LoginView控件处理根据当前用户的角色选择适当视图的任务。下面的代码来自于此应用程序的 Default.aspx 页。该示例中的 LoginView控件有 4 种视图:一种用于匿名用户,一种用于经过身份验证的用户,一种用于经过身份验证且具有 Admin 角色的用户,一种用于经过身份验证且具有 NormalUser 角色的用户。

<asp:LoginView ID="Loginview1”Runat="server"> 
<RoleGroups> 
<asp:RoleGroup Roles="Admin"> 
<ContentTemplate> 
Hello, Admin. 
</ContentTemplate> 
</asp:RoleGroup> 
<asp:RoleGroup Roles="NormalUser"> 
<ContentTemplate> 
Ah, a normal user 
</ContentTemplate> 
</asp:RoleGroup> 
</RoleGroups> 
<LoggedInTemplate> 
<div align="center"> 
Thank you for using our site, 
<asp:LoginName ID="Loginname1”Font-Bold="true” 
Runat="server”/> 
</div> 
</LoggedInTemplate> 
<AnonymousTemplate> 
Welcome to our Web site. You are not 
currently logged in. 
<a href="login.aspx">Click here</a> to sign up 
for a free account. 
</AnonymousTemplate> 
</asp:LoginView>

LoginView控件的一个重要功能是,创建的所有模板都不会将 Visual Studio 2005 中的可视化设计图面弄乱,因此 Web 窗体的设计时表示不会像在使用多个 <div runat="server"> 或 <asp:Panel>控件时那样乱七八糟。图 6-19 显示设计时 Visual Studio 2005 中 Default.aspx 的 LoginView控件。


6-19 设计时的 LoginView ¿Ø¼þ¡£

此应用程序中的 Default.aspx 页还包含下面的代码。这些代码包括两个 Click事件处理程序,二者都调用 SetRole方法,但传递的角色名不同。SetRole方法创建 Admin 角色和 NormalUser 角色(如果它们不存在)。然后,SetRole方法将当前用户从所有的角色中移除。最后,SetRole方法将当前用户添加到在 roleName参数中指定的角色。之后在呈现 Default.aspx 时,页面上的 LoginView根据用户的当前角色成员身份显示适当的视图。

Sub bntMakeAdmin_Click(ByVal sender As Object, _ 
ByVal e As System.EventArgs) 
SetRole(“Admin”) 
End Sub 
Sub btnMakeNormalUser_Click(ByVal sender As Object, _ 
ByVal e As System.EventArgs) 
SetRole(“NormalUser”) 
End Sub 
Sub SetRole(ByVal roleName As String) 
If Roles.GetAllRoles().Length = 0 Then 
Roles.CreateRole(“Admin”) 
Roles.CreateRole(“NormalUser”) 
End If 
For Each r As String In Roles.GetRolesForUser(User.Identity.Name) 
Roles.RemoveUserFromRole(User.Identity.Name, r) 
Next 
Roles.AddUserToRole(User.Identity.Name, roleName) 
End Sub

PasswordRecovery控件乍一看,PasswordRecovery控件非常简单。功能完整的、启用电子邮件密码恢复功能的解决方案所需的 ASPX 标记只有一行代码:

<asp:PasswordRecovery ID="Passwordrecovery1”Runat="server”/>

使用这一行标记,ASP.NET 2.0 使 Web 站点能够让用户输入其用户名、回答其安全问题,并通过电子邮件将一个新密码发送给他们。和其他的安全控件一样,PasswordRecovery控件高度可配置。图 6-20 和 6-21 显示用户重新设置密码时要经历的步骤。


6-20 密码恢复的第一步。


6-21 密码恢复的第二步。

LoginStatus控件LoginStatus控件不像其他一些安全控件那样复杂,但却非常实用。Web 站点通常在某处有一个将用户带到登录页的登录链接。用户登录后,该链接变成注销链接。在许多 Web 开发环境中,必须编写检查用户登录状态并呈现适当 HTML(例如,通过启用 ASP.NET 1.1 中适当的 HyperLink或 LinkButton控件)的逻辑。在 ASP.NET 2.0 中,所需的只是一个 LoginStatus控件:

<asp:LoginStatus ID="Loginstatus1”Runat="server”/>

可以以几种方式配置 LoginStatus控件。例如,可以声明用户注销后控件应当做什么:刷新页、重定向到另一个页面,或者重定向到登录页。图 6-22 显示匿名用户看到的 Login 链接。图 6-23 显示经过身份验证的用户看到的 Logout 链接。


6-22 呈现给匿名用户的 LoginStatus 控件。


Figure 6-23 呈现给经过身份验证的用户的 LoginStatus 控件。

ChangePassword控件ChangePassword控件为用户提供更改其密码的界面。和其他安全控件一样,可以通过一行 ASPX 标记来使用 ChangePassword,也可以完全自定义 ChangePassword。下面的标记来自于此应用程序的 Default.aspx 页。它只更改控件提供给用户的导航类型,而不是该控件将使用的超级链接的按钮。

<asp:ChangePassword ID="ChangePassword1”Runat="server” 
CancelButtonType="Link” 
ChangePasswordButtonType="Link" 
ContinueButtonType="Link">

6-24 使用导航链接而不是导航按钮的 ChangePassword 控件。

站点地图

站点地图是许多 Web 站点不可缺少的部分。它们可以充当进入到站点任何部分的启动点,并有助于改进搜索引擎排名。站点地图一般保存为 HTML 文档,但是将站点地图保存为 HTML 有许多缺点。一个最大的问题是,站点地图包含可定期更改的分层数据。将这些数据保存在 HTML 中需要在每次添加、移动或删除页面时考虑格式设置和位置问题。在 ASP.NET 2.0 中,可以将站点地图保存在一个名为 web.sitemap 的特殊文件中。此应用程序的 web.sitemap 文件如下所示:

<siteMap> 
<siteMapNode title="Home”description="Home”url="default.aspx”> 
<siteMapNode title="Login” 
description="Home” 
url="login.aspx”/> 
<siteMapNode title="Register” 
description="Home” 
url="register.aspx”/> 
<siteMapNode title="Forgot Password” 
description="Home” 
url="PasswordRecovery.aspx”/> 
<siteMapNode title="Articles” 
description="Articles” 
url="Articles/default.aspx”> 
<siteMapNode title="Article One” 
description="“ 
url="Articles/Article1.aspx”/> 
</siteMapNode> 
</siteMapNode> 
</siteMap>

可以在站点地图中将站点的每一页作为一个 <siteMapNode>元素输入。因为每页都是一个 <siteMapNode>元素,而每个 <siteMapNode>元素都可以包含其他 <siteMapNode> 元素,因此可以轻松地更改 Web 站点的结构,而无需担心将站点地图保存为 HTML 时,Web 站点管理员必须费力解决的缩进和其他格式问题。

由于站点地图存储为 XML,因此可以以多种方式来轻松地使用它。ASP.NET 2.0 中引入的一种新数据源控件是 SiteMapDataSource。SiteMapDataSource自动读取 web.sitemap 并公开这些数据,以供其他控件使用。下面的标记来自于 SiteMap.aspx。它将一个 TreeView控件绑定到 SiteMapDataSource。结果如图 6-25 所示。

<asp:SiteMapDataSource ID="SiteMapDataSource1”Runat="server”/> 
<asp:TreeView ID="TreeView1”Runat="server” 
DataSourceID="SiteMapDataSource1"> 
</asp:TreeView>

6-25 绑定到 SiteMapDataSource 的 TreeView 控件。

许多 Web 站点的另一个重要部分是 Breadcrumb 导航踪迹,它显示您在该 Web 站点页层次结构中的位置。Breadcrumb 导航中显示的分层信息一般与站点地图中的分层数据相同,因此拥有一个根据站点地图创建 Breadcrumb 导航的控件非常有意义。ASP.NET 2.0 中的 SiteMapPath控件读取站点地图,并根据当前页在站点层次结构中的合适位置呈现适当的 Breadcrumb 导航。下面的 ASPX 标记来自于 Articles/Article1.aspx:

<asp:SiteMapPath ID="SiteMapPath1”Runat="server"> 
</asp:SiteMapPath>

当在 Internet Explorer 中查看时,SiteMapPath产生如图 6-26 所示的导航路径。


6-26 呈现在 Article1.aspx 中的 SiteMapPath 控件。

缓存后替换

Substitution控件允许将数据注入到缓存页,实际上它只不过是一个在运行时替换数据的占位符。Substitution 控件唯一令人感兴趣的属性是 MethodName。MethodName属性指定在运行时调用的方法,以获得要在缓存输出中替换占位符的数据。该方法的名称无关紧要,但必须可以共享(静态),并且必须接受 HttpContext对象作为参数。

接下来的 ASPX 标记来自于 PostCacheSubstitution/default.aspx。该页包含一些静态文本和一个名为 cacheTime 的 Label 控件。当激发 Page_Load事件处理程序时,系统将当前的时间输入到 cacheTime的 Text 属性中。该页使用 @OutputCache指令将页面输出缓存 60 秒。这意味着,系统生成该页面的内容(包括 cacheTime Label 控件中的文本)一次,然后为这 60 秒内的后续请求从缓存中检索该页的内容。Page_Load事件处理程序将根据对该页的第一次请求来执行,但直到输出缓存中的内容过期之后(60 秒之后)的首次请求时,它才会再次执行。

Substitution 控件对于输出缓存模型是个例外。每次请求都要调用在 MethodName 属性中指定的回调方法。ASP.NET 将当前的 HttpContext传递给回调方法,因此它可以完全访问在请求实现期间正常可用的各类对象,例如 RequestServerUser。在本例中,回调方法的名称为 SubstituteSubstitute 返回一个字符串,它在输出发送给用户的浏览器之前替换其中的 Substitution 控件:

<%@ page language="VB"%> 
<%@ OutputCache Duration="60”VaryByParam="none”%> 
<script runat="server"> 
Public Shared Function _ 
Substitute(ByVal context As HttpContext) As String 
Return “The time this page was requested was: “+ _ 
Now.ToString(“hh:mm:ss”) 
End Function 
Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) 
cacheTime.Text = Now.ToString(“hh:mm:ss”) 
End Sub 
</script> 
<html> 
<head runat="server"> 
<title>Untitled Page</title> 
</head> 
<body> 
<form id="form1”runat="server"> 
<div> 
This is cached output</div> 
<div> 
The time when the page was cached was: 
<asp:Label ID="cacheTime”Runat="server"></asp:Label> 
</div> 
<div>&nbsp;</div> 
<div> 
<asp:Substitution ID="Substitution1”Runat="server” 
MethodName="Substitute”/> 
</div> 
<div>&nbsp;</div> 
<div>This is cached output</div> 
</form> 
</body> 
</html>

图 6-27 显示首次请求后 10 秒钟刷新浏览器后该 Web 页的输出。请注意,Label 中的时间与 Substitute 方法返回的时间不同。大多数页面内容来自于输出缓存,但是表示“请求该页的时间为:11:17:08”的这一部分内容来自于 Substitution 控件的回调方法。


6-27 使用 Substitution 控件的缓存后替换。

小结

此应用程序只是简要介绍 ASP.NET 2.0 中可用新控件的一些比较浅显的内容。作为 Web 开发人员,您将认识到:这些新控件可能为您节省时间,并且有机会用于所有 Web 站点的常用实现(如用户管理、与安全相关的 UI 以及站点地图)。

应用程序:母版页和主题

此应用程序介绍母版页与主题,ASP.NET 2.0 中的这两种新功能提供内置的外观与模板功能,而 ASP.NET 1.1 显然没有这些功能。

新概念

几乎每个由多个页面组成的实际 Web 站点都要求所有的页面具有一致的外观。如果没有某种基于模板的解决方案,每页都保持相同的图形设计和布局将非常麻烦。使用各种 Web 技术的 Web 开发人员已经尝试了许多不同的方法来创建基于模板 的 Web 应用程序,这些应用程序的创建和更新都非常容易。遗憾的是,这是 ASP.NET 1.1 比较弱的一个方面。

许多 ASP.NET 1.1 开发人员采取一种非常麻烦的方法,他们使用的页眉和页脚用户控件必须放置在每个页面上。虽然在许多情况下这种方法还不错,但它的设计时体验并不理想,这使与图形设计器的交互非常困难,而且可能在尝试创建“Skinable”站点时受到极大的限制,很不灵活。实际上,使用页眉和页脚控件只是为了模拟基于模板的 Web 站点。单页的内容不注入模板 — 单页只是包含在每个页面上都产生一致外观的常用元素和用户控件。

“Skinnable”站点是一种对于相同的内容具有多个表示选项(通过更改字体、颜色、图像、布局,等等)的 Web 站点。

不满足于“页眉和页脚”方法的 ASP.NET 开发人员已经选择了替换方法,例如基于模板的自定义解决方案。实现自定义解决方案的几种常用方法可以非常容易地在 Web 上找到。大多数方法使得图形设计人员可以更加容易地创建可以在运行时选择的模板。虽然比页眉和页脚控件更加灵活,但自定义解决方案的设计时体验仍不理想,并增加管理的复杂性。

理想的解决方案是让图形设计人员使用指定的一个或多个内容区域创建模板。然后当构建单个 Web 页时,只需指向该模板,并指示哪项内容进入模板的哪个内容区域。在构建这些单页时,设计环境应提供可视化反馈,这样就可以知道内容注入模板后页面的最终效果。

母版页

ASP.NET 2.0 中的母版页为 Web 开发人员渴望的真正基于模板的开发提供设计时和运行时支持。下面是使用母版页的基本步骤:

1. 创建母版模板。这可以通过 HTML 图形设计器轻松实现。特定于页的内容的占位符只需使用 <asp:ContentPlaceholder>服务器控件来指定。

2. 使模板成为一个母版页。将一个模板转变成一个 ASP.NET 2.0 母版页与将 @Master指令添加到 ASPX 标记一样简单。

3. 创建单个内容页。可以像平常一样创建单个 Web 窗体,唯一的不同之处在于,必须将内容放置在 <asp:Content>标记内,该标记引用模板中的内容占位符。在使用 Visual Studio 2005 时,可以在创建内容页时指定母版页,并让可视化设计器为您创建 <asp:Content> 标记。

4. 引用母版页。任何 ASP.NET 2.0 Web 窗体都可以通过在 @Page指令中引用一个母版页而成为一个内容页。既可以直接编辑 ASPX 标记,也可以使用该 Web 窗体的 Properties 窗口。

Visual Studio 2005 中的可视化 Web 设计器提供对创建和编辑母版页和内容页的完全支持。一般首先创建母版页,然后在将新的 Web 窗体添加到应用程序时引用该母版页。此应用程序的“演练”一节将说明如何完成该任务。

主题

母版页使得控制 Web 站点的布局和定义常用的可视化元素(例如,左上角的公司徽标或每页左侧的导航栏)变得非常简单。但是 Web 站点还有其他的可视化元素需要控制,例如文本框使用的字体和颜色,或者数据网格的格式设置。一般使用级联样式表 (CSS) 来集中控制 Web 用户界面的许多方面。Web 站点中的每一页都可以引用相同的级联样式表,这使您能够通过更改一个文件而影响整个 Web 站点。但对于 ASP.NET 开发人员而言,级联样式表并不是完整的解决方案。例如,诸如 GridView这样的复杂控件有许多可视化元素,使用 CSS 不能轻松地控制它们。如果您想让每个 GridView都使用相同的字体和颜色,就必须显式设置每个 GridView 的格式。

ASP.NET 2.0 中的主题提供一种控制服务器控件表示的新方法,而没有放弃在级联样式表中的现有投入。ASP.NET 2.0 中的一个主题是包含样式表和 .skin 文件的文件夹。这些 .skin 文件定义 Web 应用程序中服务器控件期望的格式属性。将该主题应用于 Web 窗体时,在 .skin 文件中定义的属性就应用于适当的服务器控件。例如,ASP.NET 2.0 附带的 SmokeAndGlass 主题在 .skin 文件中包含 Label、TextBox、Button 和 LinkButton的如下属性:

<asp:Label runat="server”ForeColor="#585880” 
Font-Size="0.9em”Font-Names="Verdana”/> 
<asp:TextBox runat="server”BackColor="#FFFFFF”BorderStyle="Solid” 
Font-Size="0.9em”Font-Names="Verdana”ForeColor="#585880" 
BorderColor="#585880”BorderWidth="1pt” 
CssClass="theme_textbox”/> 
<asp:Button runat="server”BorderColor="#585880”Font-Bold="true” 
BorderWidth="1pt”ForeColor="#585880”BackColor="#F8F7F4”/> 
<asp:LinkButton runat="server”Font-Size=“.9em”Font-Names="Verdana"/>

当将一个 Label 放置在 Web 窗体上时,它的 ForeColor值为 #585880,并使用 Verdana 字体。Button 或 TextBox将具有相同的 ForeColor。应当注意,.skin 文件中的这些项不具有 ID属性,因为它们不表示服务器控件的实际实例。当在 Web 窗体上创建一个 Label 控件的实例时,它将具有其自己的 ID属性,但被赋予其他的外观属性值,而不考虑在 Web 窗体中为该控件指定的任何属性值。重写主题控件的可视化外观的唯一方法是使用 EnableTheming属性禁用该控件的“Theming”:

<asp:Label EnableTheming="False”ID="lblText”runat="server”Text="A”/>

使用 ASP.NET 2.0 主题,您可以继续使用现有的级联样式表类和内联样式属性。使用 <link> 标记,任何主题页都可以自动引用主题文件夹中的样式表文件 (.css)。然而,需要预先提醒的是,.skin 文件中的设置通常优先于 CSS 设置,因为大多数服务器控件将属性(如 BackColor)呈现为内联样式,它将重写 CSS 类的设置或以前的内联样式。

演练

本演练说明如何使用母版页和主题来轻松提供 Web 站点的一致用户界面。

创建母版页

创建母版页与创建任何其他的 ASP.NET Web 窗体一样容易。只有两点不同:使用 @Master指令而非 @Page,ContentPlaceHolder控件用来指示内容页将注入特定页内容的位置。为了使用 Visual Studio 2005 创建一个新的母版页,可以右键单击解决方案资源管理器中的根节点,并选择“Add New Item”。在“Add New Item”对话框中,选择“Master Page”。图 6-28 显示选中“Master Page”的“Add New Item”对话框。请注意,母版页的文件扩展名为 .master,而不是通常的 .aspx 扩展名。


6-28 “Add New Item”对话框。

可视化 Web 设计器中的设计时体验与使用任何 Web 窗体完全类似。唯一的不同之处是使用 ContentPlaceHolder控件。图 6-29 显示设计视图中的一个母版页,表中有一个 ContentPlaceHolder控件。


6-29 Visual Studio 2005 中设计视图中的母版页。

6-29 中母版页后的 ASPX 标记非常简单。大多数标记是常规的 HTML,用于定义正文中带有表的 HTML 文档。特定于母版页的行为粗体:

<%@ Master Language="VB"%> 
<html> 
<head runat="server"> 
<title>Untitled Page</title> 
</head> 
<body> 
<form id="form1”runat="server"> 
<div align="center"> 
<table width="500" border="0"> 
<tr style="background-color: DarkBlue” 
valign="middle"> 
<!-- Header title row--> 
<td> 
<h2 align="right” 
style="color: white; font-size: 14pt"> 
Master Pages 
</h2> 
</td> 
</tr> 
<tr> 
<td style="background-color: Beige;"> 
<asp:ContentPlaceHolder 
ID="ContentPlaceHolder1”Runat="server"> 
</asp:ContentPlaceHolder> 
</td> 
</tr> 
</table> 
</div> 
</form> 
</body> 
</html>

@Master指令将该 Web 窗体标识为一个母版页。这个特定的母版页只有一个标识为 ContentPlaceHolder1 的内容区域。实际上,ContentPlaceHolder控件的名称非常重要。通过使用 ContentPlaceHolder 的 ID,可以将内容页中的内容控件与母版页中的占位符关联起来。

使用母版页

对于 Visual Studio 2005,使用母版页创建内容页几乎不费吹灰之力。当向 Web 站点添加一个新 Web 窗体时,选中“Add New Item”对话框中的“Select Master Page”复选框,如图 6-30 所示。


6-30 添加内容页的“Add New Item”对话框。

Visual Studio 2005 接下来将提示您从 Web 站点选择一个现有的母版页,如图 6-31 所示。


6-31 为新的 Web 窗体选择一个母版页。

当在可视化 Web 设计器中打开新的内容页时,系统会显示母版页的布局,但是为灰色,这提供页面的最终效果和页特定内容显示位置的快速可视化参考。Visual Studio 2005 为母版页中的每个 ContentPlaceHolder控件创建一个 Content 控件。图 6-32 显示前面使用母版页为此应用程序创建的内容页。


6-32 设计视图中的内容页。

为内容页生成的 ASPX 标记非常简单,因为提供所生成 HTML 文档结构的许多标记都是在母版页中定义的。内容页中只需要两条关键信息:使用的母版页文件和 Content 控件与 ContentPlaceHolder控件之间的映射。下面的 ASPX 标记显示图 6-32 所示的页面的源代码:

<%@ page language="VB”masterpagefile="~/MasterPage.master”%> 
<asp:Content ID="Content1” 
ContentPlaceHolderID="ContentPlaceHolder1” 
Runat="server"> 
This is content in the content file that replaces 
the content in the placeholder in the master page. 
</asp:Content>

在前面的标记中,在 @Page指令中使用 MasterPageFile属性来指定母版页。在 Content 控件中,使用 ContentPlaceHolderID属性将内容页中的 Content1 映射到母版页中的 ContentPlaceHolder1

通过设置 Web 窗体的 MasterPageFile属性,可以在运行时选择一个新的母版页。只有当新母版页包含的 ContentPlaceHolder控件对应于内容页中 Content 控件引用的所有占位符时,才可以使用新母版页。必须在初始化该页之前更改 MasterPageFile。例如,可以在 Page_PreInit事件处理程序中使用下面的代码来选择替换的母版页,每加载页 5 次选择一个(平均):

Sub Page_PreInit(ByVal sender As Object, ByVal e As System.EventArgs) 
Dim rnd As New System.Random 
If rnd.Next(1, 5) = 1 Then 
Me.MasterPageFile = “~/MasterPage2.master" 
End If 
End Sub

但是,如果 MasterPage2.master 没有像最初的母版页文件那样定义相同的 ContentPlaceHolder控件,这些代码将失败。

您可能会发现,与在运行时动态选择母版页相比,以编程方式操作母版页更为实用。ASP.NET 2.0 中的母版页不是静态模板。母版页是服务器上对象实例化的类,它们公开一组完整的事件。实际上,MasterPage类是从 System.Web.UI.UserControl继承的,并公开与 UserControl 相同的事件,包括 InitLoad 和 PreRender事件。而且,可以向母版页添加自己的自定义属性和方法,内容页可以使用它们与母版页进行交互。考虑下面添加到此应用程序母版页中的 Property

Public Property SubTitle() As String 
Get 
Return lblSubTitle.Text 
End Get 
Set(ByVal value As String) 
lblSubTitle.Text = value 
End Set 
End Property

SubTitle属性是 Label 控件 Text 属性的包装,该属性被添加到母版页。根据内容页,下面的代码可用于设置母版页的 SubTitle属性:

Dim m As MasterPage_master 
m = DirectCast(Master, MasterPage_master) 
m.SubTitle = “Now Showing: Content page at “+ Now.ToString()

内容页的 Master 属性是一种由 System.Web.UI.Page类公开的属性。Master 属性返回一个 MasterPage 对象,必须将该对象转换为派生的母版页类的实际类型。保存为 FileName.master 的母版页文件的类名为 FileName_master,因此本例中的派生类型为 MasterPage_master。一旦正确地进行了类型转换,就可以以一种类型安全的方式访问母版页的自定义属性和方法。

如果对直接访问母版页中的控件感兴趣,可以使用 FindControl方法来获得对目标控件的引用(如果它存在)。下面的代码使用 Master.FindControl 直接访问副标题 Label

Dim lblSubTitle As Control 
lblSubTitle = Master.FindControl(“lblSubTitle”) 
If Not lblSubTitle Is Nothing Then 
If TypeOf lblSubTitle Is Label Then 
DirectCast(lblSubTitle, Label).Text = “Hello world" 
End If 
End If

Visual Studio 2005 的文档声明以下内容,可以从内容页的 Master 属性直接访问母版页的自定义公共属性。例如,Master.UserLoginName = ¡°ethan¡±,其中 UserLoginName是母版页的自定义属性。在本书所使用的 Visual Studio 2005 预览版中,Master 属性始终返回一个 System.Web.UI.MasterPage对象,因此访问 Master.UserLoginName将引起编译错误。必须将 Master 转换为母版页的类型,然后按照前一个示例的说明访问自定义属性。一种实现这种类型转换的简单方法是:DirectCast(Master, MasterPage_master).UserLoginName。

使用主题

可以使用 @Page指令的 theme 属性来控制主题,也可以在 web.config 中使用 <pages>元素为整个 Web 站点指定主题。用 @Page指令选择主题非常简单:

<%@ page language="VB”Theme="BasicBlue”%>

除了需要将设置放在 web.config 文件中而不是 @Page 指令中之外,为整个站点指定主题真的非常简单:

<?xml version="1.0”encoding="UTF-8”?> 
<configuration> 
<system.web> 
<pages theme="BlackAndWhite”/> <compilation debug="false”/> 
<globalization requestEncoding="utf-8”responseEncoding="utf-8”/> 
</system.web> 
</configuration>

在此应用程序中,BlackAndWhite 主题是一个自定义主题,该主题将黑色用于一些常用控件的背景色,而将白色用于前景色。图 6-33 显示在浏览器中查看时的最终效果:


6-33 Internet Explorer 中使用 BlackAndWhite 主题的内容页。

创建自定义主题

使用 ASP.NET 2.0 中的预构建主题来彻底检验这种新技术是可行的,但这对于实际开发而言并不很实用,因为每个站点都要求其自己的视觉效果与公司的品牌策略保持一致,或者实现图形设计师的意图。幸运的是,创建 ASP.NET 主题非常容易。首先,在 Web 站点文件夹中创建一个 Themes 文件夹。然后,在 Themes 文件夹中创建一个与主题具有相同名称(如 BlackAndWhite)的文件夹。接下来,创建 .css 和 .skin 文件,您可以在其中定义服务器控件和其他 HTML 的几乎任何可视化特征。图 6-34 显示某个 Web 站点项目的 .skin 文件,以及可以在解决方案资源管理器中看到的 BlackAndWhite 主题文件夹。


6-34 在 Visual Studio 2005 中编辑 .skin 文件。

小结

在 ASP.NET 2.0 之前,由于对健壮的 Web 站点模板和外观的支持有限,Microsoft Web 开发人员处于不利的状况。许多开发人员依靠“页眉和页脚用户控件”方法来保持 Web 站点外观的一致性,但让他们感到失望的是,设计时体验并不理想,并且难以将 Web 图形设计器完成的工作集成到用户控件中,从而创建具有良好外观的 Web 站点。ASP.NET 2.0 中引入母版页和主题使得创建基于模板的 Web 站点简单而又灵活。Visual Studio 2005 中的设计时体验非常特别。将母版页与主题相结合,使得创建具有一致外观的专业 Web 页非常容易,并且无需牺牲开发人员的工作效率或站点的长期可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值