在首次公诸于众以来的短短四年中,ASP.NET业已成为在Windows倾力支持的服务器上运行的Web应用程序的金科玉律,并且将runat="server"添加到供世界各地Web开发人员使用的词汇表中。它还为人们所认识的Web编程的前景提供了一些启示:Web编程将以能够呈现HTML和脚本以及激发事件的服务器端控件为中心。
在Microsoft .NET Framework的下一个主要版本中,ASP.NET 2.0将摆脱某些不完善的状况并发展为一种完全成熟的产品。它的目标是将完成常见Web编程任务所需的代码数量减少70%或更多。尽管该目标非常宏大,但是由于多种多样的新增服务、控件和功能(它们一定会使ASP.NET 2.0像ASP.NET 1.x之于ASP一样,使ASP.NET 1.x显著提高),该目标是可以实现的。
这里,我将为您粗略介绍一些期望在ASP.NET 2.0中看到的内容,对于几个精选的领域进行深入讨论并且提供一些示例程序来强调关键的功能。所有代码示例均针对ASP.NET 2.0的预测试版本生成和测试的,可能需要对某些代码示例进行修改,以便用于第一个测试版本。
母版页(Master Pages)
ASP.NET 1.x中最突出的缺点之一是它缺少对页面模板的支持。欠缺的是定义其他页面可以继承的“母版页”能力。开发人员通过使用用户控件(它们可以容易地在页面之间复制)创建页面来弥补这一缺陷。在ASP.NET 2.0中,由于加入了一个称作“母版页”的新增功能,所以不再需要这种技巧。想一想“可视化继承”,您就会理解母版页是干什么用的了。首先,您需要定义一个母版页(其中包含您希望在其他页面上显示的内容),并且使用ContentPlaceHolder控件来定义子页面可以将其内容插入的位置。然后,您需要生成子页面—SPX文件,它使用(类似于)如下所示的指令来引用母版页:
<%@ Page MasterPageFile="~/Foo.master" %>
在子页面中,您使用Content控件来填写母版页中的占位符。在浏览器中呈现子页面,出现的内容将是在母版页和子页面中所定义内容的完美组合。
<html>
<body leftmargin="0" topmargin="0" rightmargin="0"
bottommargin="0" marginheight="0" marginwidth="0"
>
<!-- Banner -->
<table cellspacing="0" cellpadding="0"
style="background-image: url('images/stripes.gif');
background-repeat: repeat-x, repeat-y" width="100%"
>
<tr><td align="center">
<span style="font-family: verdana; font-size: 36pt;
font-weight: bold; color: white"
>
Master Pages
</span><br>
<span style="font-family: verdana; font-size: 10pt;
font-weight: normal; color: white"
>
This banner provided by Master.master
</span>
</td></tr>
</table>
<!-- Placeholder for content between banner and footer -->
<form id="Content">
<asp:ContentPlaceHolder ID="Main" RunAt="server" />
</form>
<!-- Footer -->
<table width="100%"><tr><td align="center">
<span style="font-family: verdana; font-size: 8pt; color: red">
Copyright (c) 2004 by Me Inc. All rights reserved<br>
This footer provided by Master.master
</span>
</td></tr></table>
</body>
</html>
Master.master
<%@ Page MasterPageFile="~/Master.master" %>
<asp:Content ContentPlaceHolderID="Main" RunAt="server">
<table width="100%" height="256px"><tr><td align="center">
<h2>This content provided by Subpage.aspx</h2>
</td></tr></table>
</asp:Content>
代码1 Subpage.aspx
代码1中所示的应用程序使用母版页来定义出现在每个页面上的页眉和页脚。子页面通过将Content控件插入母版页的ContentPlaceHolder中,从而在页眉和页脚之间插入内容。您应该注意匹配的ID和ContentPlaceHolderID,以及母版页中的@ Master指令。
母版页在ASP.NET对象模型中得到了充分支持。System.Web.UI.Page类的特点是具有一个名为Master的新属性,该属性使子页面可以用编程方式来引用它的母版页以及在其中定义的控件。母版页可以嵌套,并且可以包含能够在子页面中重写的默认内容。
This is default content that will appear in subpages unless explicitly overridden
</asp:ContentPlaceHolder>
此外,应用程序可以在Web.config中指定默认的母版页,如下所示:
<system.web>
<pages masterPageFile="~/Foo.master" />
</system.web>
</configuration>
单个子页面享有重写默认母版页和指定它们自己的母版页的自由。
最精彩的部分是Visual Studio 2005中对母版页的支持。当加载子页面时,IDE将显示母版页中定义的内容的灰色、只读版本,以及子页面中定义的内容的全色、完全可编辑版本。区分这两者很容易,并且如果要编辑属于母版页的内容,那么您需要做的全部事情只是在IDE中打开母版页。
有关母版页的更多深入内容,请参阅相关的文章。
数据源控件
数据绑定在ASP.NET 1.x中占据着显著位置。几行位置恰当的数据绑定代码可以取代大量查询数据库,并使用对Response.Write方法的重复调用,从而将查询结果转换为HTML内容的ASP代码。
下面的DataSource1.aspx页使用ASP.NET 2.0数据绑定来显示SQL Server Pubs数据库的一部分:
<body>
<form runat="server">
<asp:SqlDataSource ID="Titles" RunAt="server"
ConnectionString="server=localhost;database=pubs;Integrated
Security=SSPI"
SelectCommand="select title_id, title, price from titles"
/>
<asp:DataGrid DataSourceID="Titles" RunAt="server" />
</form>
</body>
</html>
SqlDataSource控件定义了数据源以及对它执行的查询,并且DataGrid的DataSourceID属性指向SqlDataSource。当加载该页时,SqlDataSource控件就会执行查询,并将结果提供给DataGrid。
当然,实际操作中的数据绑定很少会如此简单。假设您希望缓存查询结果,或者使用在其他控件中选择的项目来参数化数据库查询。图2 中的页使用一个SqlDataSource,用Northwind的Customers表中列出的国家/地区来填充一个下拉列表,并使用另一个SqlDataSource,用在该下拉列表中选择的国家/地区中的一系列客户来填充一个DataGrid。请注意元素,它指示DataGrid的SqlDataSource从下拉列表中获取@country的值。还请注意绑定到该下拉列表的SqlDataSource中的EnableCaching和CacheDuration属性。这些声明将SELECT DISTINCT查询的结果缓存60秒。
上述示例只是粗浅地探讨了可以通过数据源控件完成的工作。例如,您可以使用存储过程,可以使用从查询字符串、用户输入、会话状态和Cookie中提取的值来参数化查询,并且可以指定控件是否应该使用DataSet或DataReader。因为数据源控件包含数据适配器的功能,所以您甚至可以使用数据源控件来更新数据库。随着ASP.NET 2.0最终发布日期的迫近,读者有望看到大量有关数据源控件的文章。
尽管我所讨论的是数据绑定主题,但您还应该知道ASP.NET 2.0支持简化的数据绑定语法。ASP.NET开发人员可以找到与下面这个给人深刻印象的表达式类似的表达式:
<%# DataBinder.Eval (Container.DataItem, "title") %>
在ASP.NET 2.0中,可以按如下形式写一个相同的表达式:
<%# Eval("title") %>
除了Eval运算符以外,ASP.NET 2.0还支持名为XPath和XPathSelect(它们使用指向XML文档中数据的XPath表达式)的运算符。
主题与外观(skins)
如果您不使用属性来丰富控件,那么ASP.NET页面可能会显得非常单调。问题在于:迄今为止,一次只能应用一个属性,并且您无法通过成组地设置控件的可视属性来赋予控件主题。ASP.NET 2.0提供了一项新功能,即输入主题和外观。该功能简化了生成精美页面的任务。
要查看主题和外观的工作方式,请将以下指令添加到DataSource2.aspx中,如代码2 所示:
<body>
<form runat="server">
<asp:SqlDataSource ID="Countries" RunAt="server"
ConnectionString="server=localhost;database=northwind;
Integrated Security=SSPI"
SelectCommand="select distinct country from customers order by country"
EnableCaching="true" CacheDuration="60"
/>
<asp:SqlDataSource ID="Customers" RunAt="server"
ConnectionString="server=localhost;database=northwind;
Integrated Security=SSPI"
SelectCommand="select * from customers where country=@country"
>
<SelectParameters>
<asp:ControlParameter Name="Country"
ControlID="MyDropDownList"
PropertyName="SelectedValue"
/>
</SelectParameters>
</asp:SqlDataSource>
<asp:DropDownList ID="MyDropDownList" DataSourceID="Countries"
DataTextField="country" AutoPostBack="true" RunAt="server"
/>
<asp:DataGrid DataSourceID="Customers" RunAt="server" />
</form>
</body>
</html>
代码2 DataSource2.aspx
现在刷新该页面。结果如图1中所示。
图1 主题和外观的工作方式
接下来,赋予该页面完全不同的外观,方法是:添加一个元素,并且更改@ Page指令以指示要使用的页面主题:
@ Page指令的新Theme属性以声明方式将主题应用到页面中。还可以使用Page类的Theme属性以编程方式应用主题。主题是外观的集合,而外观是应用于一个控件类型的一组可视属性。BasicBlue和SmokeAndGlass是ASP.NET 2.0附带的几个预定义(或全局)主题中的两个。您可以在Microsoft.NET\Framework\...\ ASP.NETClientFiles\Themes下的子目录中找到它们。
您可以定义自己的主题和外观,并且自定义地将它们部署到应用程序Themes目录的子目录中。每个子目录构成一个主题,并且主题名称与子目录名称相同。一个主题子目录包含一个或多个.skin文件以及该主题使用的任何其他资源(例如,图像文件和样式表)。实际的外观定义包含在.skin文件中,并且非常类似于用来在ASPX文件中声明控件实例的标记。
为说明这一点,请在包含DataSource2.aspx的文件夹中创建一个名为Themes的子目录。在该Themes目录中,创建一个名为ShockingPink的子目录。将以下内容复制到ShockingPink目录中的新建的.skin文件中:
<asp:DataGrid runat="server" BackColor="#CCCCCC" BorderWidth="2pt"
BorderStyle="Solid" BorderColor="#CCCCCC" GridLines="Vertical"
HorizontalAlign="Left"
>
<HeaderStyle ForeColor="white" BackColor="hotpink" />
<ItemStyle ForeColor="black" BackColor="white" />
<AlternatingItemStyle BackColor="pink" ForeColor="black" />
</asp:DataGrid>
接下来,将DataSource2.aspx中的@ Page指令更改为:
<%@ Page Theme="ShockingPink" %>
上述操作的结果无疑是曾经创建过的最为华丽的Web页之一。
ShockingPink.skin定义了DropDownList和DataGrid控件的默认外观。外观文件不必与它们所属的主题具有相同的名称(尽管它们通常如此)。一个主题可以包含多个.skin文件,而单个.skin文件可以为任意数量的控件类型定义属性。此外,ASP.NET还可以区分默认外观和非默认外观。根据定义,缺少SkinID属性的外观是默认外观。非默认外观包含可以通过控件标记中的SkinID属性引用的SkinID。
新增控件
ASP.NET 2.0将引入大约50种新的控件类型,以便帮助您生成丰富的用户界面,同时使您无须应付HTML、客户端脚本和浏览器文档对象模型(DOM)的各种变幻莫测的行为。“新增控件”提要栏列出了直至本文撰写时规划的新控件(Web部件控件除外)。
DynamicImage控件简化了在Web页中显示动态生成的图像的任务。在过去,开发人员经常编写自定义HTTP处理程序来处理动态图像生成,甚至更糟糕的是,处理ASPX文件中生成的图像。DynamicImage使得这两种技术都过时了。代码4 中的代码使用DynamicImage控件来绘制饼图。关键语句是那个将图像位分配给控件的ImageBytes数组的语句。
DynamicImage控件利用了新增的ASP.NET 2.0图像生成服务。另一种访问图像生成服务的方法是,在ASP.NET 2.0的全新ASIX文件中动态生成图像。本文随附的示例文件(可从MSDN Magazine Web站点上获得)包含一个名为DynamicImage.asix的文件,它展示了ASIX文件的要素。要运行该文件,请将DynamicImage.asix复制到Web服务器的某个虚拟目录中,然后在浏览器中激活该文件。
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="System.IO" %>
<html>
<body>
<asp:DynamicImage ID="PieChart" DynamicImageType="ImageBytes"
RunAt="server"
/>
</body>
</html>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
// Create a bitmap and draw a pie chart
Bitmap bitmap = new Bitmap (240, 180, PixelFormat.Format32bppArgb);
Graphics g = Graphics.FromImage (bitmap);
DrawPieChart (g, Color.White, new decimal[]
{ 100.0m, 200.0m, 300.0m, 400.0m }, 240, 180);
g.Dispose();
// Attach the image to the DynamicImage control
MemoryStream stream = new MemoryStream ();
bitmap.Save (stream, ImageFormat.Gif);
bitmap.Dispose();
PieChart.ImageBytes = stream.ToArray ();
}
void DrawPieChart (Graphics g, Color bkgnd, decimal[] vals, int width, int height)
{
// Erase the background
SolidBrush br = new SolidBrush (bkgnd);
g.FillRectangle (br, 0, 0, width, height);
br.Dispose ();
// Create an array of brushes
SolidBrush[] brushes = new SolidBrush[6];
brushes[0] = new SolidBrush (Color.Red);
brushes[1] = new SolidBrush (Color.Yellow);
brushes[2] = new SolidBrush (Color.Blue);
brushes[3] = new SolidBrush (Color.Cyan);
brushes[4] = new SolidBrush (Color.Magenta);
brushes[5] = new SolidBrush (Color.Green);
// Sum the inputs
decimal total = 0.0m;
foreach (decimal val in vals)
total += val;
// Draw the chart
float start = 0.0f;
float end = 0.0f;
decimal current = 0.0m;
for (int i=0; i<vals.Length; i++)
{
current += vals[i];
start = end;
end = (float) (current / total) * 360.0f;
g.FillPie (brushes[i % 6], 0.0f, 0.0f, width, height,
start, end - start);
}
// Clean up and return
foreach (SolidBrush brush in brushes)
brush.Dispose ();
}
</script>
代码4 DynamicImage.aspx
ASP.NET 2.0中首次亮相的另一个有趣且非常有用的控件是MultiView控件。与View控件搭配使用时,MultiView可用来创建包含多个逻辑视图的页面。一次只能显示一个视图(其索引被分配给MultiView的ActiveViewIndex属性的那个视图),但您可以通过更改活动视图索引来切换视图。对于使用选项卡或其他控件来让用户在逻辑页之间进行导航的页面而言,MultiViews是非常理想的。
代码5 中的页面使用一个MultiView来显示Pubs数据库Titles表的两个不同视图:其中一个用GridView呈现,另一个用DetailsView呈现。视图切换是通过从下拉列表中进行选择来完成的。请注意,标记中的AllowPaging属性使用户可以浏览DetailsView中的记录。
<html>
<body>
<form runat="server">
<asp:SqlDataSource ID="Titles" RunAt="server"
ConnectionString="server=localhost;database=pubs;
Integrated Security=SSPI"
SelectCommand="select title_id, title, price from titles"
/>
<asp:DropDownList ID="ViewType" AutoPostBack="true"
OnSelectedIndexChanged="OnSwitchView" RunAt="server"
>
<asp:ListItem Text="GridView" Selected="true" RunAt="server" />
<asp:ListItem Text="DetailsView" RunAt="server" />
</asp:DropDownList>
<asp:MultiView ID="Main" ActiveViewIndex="0" RunAt="server">
<asp:View RunAt="server">
<asp:GridView DataSourceID="Titles" RunAt="server" />
</asp:View>
<asp:View RunAt="server">
<asp:DetailsView DataSourceID="Titles" AllowPaging="true"
RunAt="server"
/>
</asp:View>
</asp:MultiView>
</form>
</body>
</html>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
if (IsPostBack)
DataBind ();
}
void OnSwitchView (Object sender, EventArgs e)
{
Main.ActiveViewIndex = ViewType.SelectedIndex;
}
</script>
代码5 MultiView.aspx
GridView和DetailsView控件
DataGrid是ASP.NET中最受欢迎的控件之一,但在某些方面,它也成为自己成功的牺牲品:如此丰富的功能,以至于让ASP.NET开发人员不满足于此,而是希望它能提供更多功能。DataGrid控件在ASP.NET 2.0中并没有发生太大变化,只是添加了两个分别名为GridView和DetailsView的新控件,它们提供了通常要求DataGrid控件所具有的功能,并且还加入了一些属于它们自己的新功能。
GridView呈现HTML表的方式与DataGrid一样,但与DataGrid不同的是,GridView可以完全依靠自己来分页和排序。GridView还支持比DataGrid种类更为丰富的列类型(在GridView用语中称为字段类型),并且它们具有更为智能的默认呈现行为,能够自动呈现Boolean值(例如,通过复选框)。GridView也可以容易地与DetailsView搭配使用,以创建主-从视图。GridView控件的主要缺陷是:像DataGrid一样,它通过将信息传回到服务器来完成它该做的大部分工作。
代码6 中的页面结合使用了GridView和DetailsView,以创建Pubs数据库的Titles表的简单主-从视图。SqlDataSource控件为其他控件提供数据,而绑定到DetailsView控件的SqlDataSource中的SelectParameter使DetailsView能够显示GridView中当前选择的记录。可以通过单击GridView的Select按钮(该按钮因标记中的AutoGenerateSelectButton="true"属性而存在)来选择记录。
<html>
<body>
<form runat="server">
<asp:SqlDataSource ID="Titles1" RunAt="server"
ConnectionString="server=localhost;database=pubs;Integrated Security=SSPI"
SelectCommand="select title_id, title, price from titles"
/>
<asp:SqlDataSource ID="Titles2" RunAt="server"
ConnectionString="server=localhost;database=pubs;Integrated
Security=SSPI"
SelectCommand="select title_id, title, price from titles where
title_id=@title_id"
>
<SelectParameters>
<asp:ControlParameter Name="title_id"
ControlID="MyGridView"
PropertyName="SelectedValue"
/>
</SelectParameters>
</asp:SqlDataSource>
<table><tr><td>
<asp:GridView ID="MyGridView" DataSourceID="Titles1"
Width="100%" RunAt="server" AutoGenerateColumns="false"
SelectedIndex="0" AutoGenerateSelectButton="true"
DataKeyNames="title_id"
>
<Columns>
<asp:BoundField HeaderText="Title ID"
DataField="title_id"
/>
<asp:BoundField HeaderText="Book Title" DataField="title" />
<asp:BoundField HeaderText="Price" DataField="price"
DataFormatString="{0:c}" NullDisplayText="TBD"
/>
</Columns>
</asp:GridView>
</td></tr>
<tr><td>
<asp:DetailsView DataSourceID="Titles2" RunAt="server"
AutoGenerateRows="false" Width="100%"
>
<Fields>
<asp:BoundField HeaderText="Title ID"
DataField="title_id"
/>
<asp:BoundField HeaderText="Book Title"
DataField="title"
/>
<asp:BoundField HeaderText="Price" DataField="price"
DataFormatString="{0:c}" NullDisplayText="TBD"
/>
</Fields>
</asp:DetailsView>
</td></tr></table>
</form>
</body>
</html>
代码6 MasterDetail.aspx
请注意GridView和DetailsView控件中用于定义字段类型的和元素。这些元素实际上等效于DataGrid控件中的元素。表1列出了受支持的字段类型。特别重要的是ImageField和DropDownListField,它们都可以有效地削减目前开发人员为在DataGrid中包含图像和数据绑定下拉列表而编写的大部分代码。
字段类型 | 描述 |
---|---|
AutoGeneratedField | 默认字段类型 |
BoundField | 绑定到数据源指定列 |
ButtonField | 显示一个按钮、图片按钮或者链接按钮 |
CheckBoxField | 显示一个复选框 |
CommandField | 显示一个用于选择或者编辑的按钮 |
DropDownListField | 显示一个下拉列表 |
HyperLinkField | 显示一个超级链接 |
ImageField | 显示一个图片 |
TemplateField | 内容由HTML模板来定义 |
表1 GridView and DetailsView字段类型
新增的管理功能
ASP.NET 1.x的另一个明显的缺陷(已经在ASP.NET 2.0中得到修复)是根本没有用于管理Web站点的接口(无论是声明性接口还是编程接口)。在过去,更改配置设置意味着启动记事本并编辑Machine.config或Web.config,但现在不再需要这么做了。ASP.NET 2.0具有一个完善的管理API,它简化了读取和写入配置设置的任务。它还包括一个管理GUI,您可以通过在浏览器中请求Webadmin.axd来显示该GUI,如图8所示。
图2 管理GUI
尽管在撰写本文时尚不完善,但Webadmin.axd被设计为使您可以配置ASP.NET 2.0中包含的各种服务(如成员身份和角色管理服务)、查看Web站点统计信息以及应用安全设置。
成员身份服务
ASP.NET 2.0中新增的最佳功能之一是新的成员身份服务,它提供了用于创建和管理用户帐户的易于使用的API。ASP.NET 1.x大规模引入了窗体身份验证,但仍然要求您编写相当数量的代码来执行实际操作中的窗体身份验证。成员身份服务填补了ASP.NET 1.x窗体身份验证服务的不足,并且使实现窗体身份验证变得比以前简单得多。
成员身份API通过两个新的类公开:Membership和MembershipUser。前者包含了用于创建用户、验证用户以及完成其他工作的静态方法。MembershipUser代表单个用户,它包含了用于检索和更改密码、获取上次登录日期以及完成类似工作的方法和属性。例如,下面的语句采用用户名和密码作为参数,并返回true或false来指示它们是否有效。它取代了对ASP.NET 1.x应用程序中、使用Active Directory?或后端数据库来验证凭据的简易方法的调用,如下所示:
下面的语句返回一个MembershipUser对象,该对象表示用户名为“jeffpro”的用户:
以下语句检索一个已注册用户的电子邮件地址(假设记录了电子邮件地址):
成员身份服务所管理的用户名、密码和其他数据存储在哪里?像ASP.NET 2.0中的几乎所有状态管理服务一样,成员身份是基于提供程序的。提供程序是使服务可以与物理数据源进行交互的模块。ASP.NET 2.0将包含Microsoft Access数据库、SQL Server数据库和Active Directory的成员身份提供程序,并且还可能包含其他数据存储的成员身份提供程序。
默认情况下,成员身份服务使用Access提供程序,并将成员身份数据存储在应用程序的Data子目录中名为AspNetDB.mdb的文件中。可以通过Web.config的部分选择备用提供程序。可以让Webadmin.axd修改Web.config,而不必自己进行修改。下面的内容节选自Web.config(在Webadmin.axd创建了一个名为WhidbeyLogin的SQL Server?数据库,并将成员身份服务配置为使用该数据库之后):
<providers>
<add name="WhidbeyLogin"
type="System.Web.Security.SqlMembershipProvider, ..."
connectionStringName="webAdminConnection632112624221191376"
applicationName="/Whidbey" requiresUniqueEmail="false"
enablePasswordRetrieval="true" enablePasswordReset="false"
requiresQuestionAndAnswer="false"
passwordFormat="Encrypted"
/>
</providers>
</membership>
connectionStringName属性引用了Web.config的新部分中的连接字符串。ASP.NET 2.0将包含加密这部分的Web.config以保护数据库连接字符串的能力。
Webadmin.axd的用途并不仅限于创建数据库和选择成员身份提供程序:它还可以用于创建用户、管理凭据以及其他用途。在Webadmin.axd和成员身份API之间,还存在用于管理站点注册用户的声明性手段和编程手段。这是从ASP.NET 1.x向前迈出的一大步,它在很大程度上将凭据管理的问题留给用户自己去处理。
登录控件
成员身份服务本身就显著减少了验证登录和管理用户所需的代码量,此外还有一系列称为登录控件的新控件使窗体身份验证变得更加容易。登录控件可以与成员身份服务配合使用,也可以不与其配合使用,但它们与该服务之间的集成性非常好,以至于当登录控件与成员身份服务一起使用时,一些基本任务(例如,验证用户名和密码以及用电子邮件发送遗忘的密码)通常不必编写任何代码就可以完成。“新增控件”提要栏包含了计划随ASP.NET 2.0一起提供的一系列登录控件。
图3 Login控件
图3中所示的Login控件是登录控件系列的核心控件。除了提供具有高度可自定义性的UI以外,它还能够调用Membership.ValidateUser来验证用户名和密码。Login控件还可以调用FormsAuthentication.RedirectFromLoginPage,将用户重定向到他们在被重定向到登录页时尝试到达的页面。然后,FormsAuthentication.RedirectFromLoginPage将发出身份验证Cookie。在本文稍后的部分中,您将看到Login和其他登录控件的工作方式。
角色管理器
如果不支持基于角色的安全性,那么成员身份服务和登录控件将是不完善的。在ASP.NET 1.x中,要将窗体身份验证与角色结合起来,需要编写代码以将角色信息映射到各个传入的请求。ASP.NET 2.0中新的角色管理器(它可以与成员身份服务配合使用,也可以不与其配合使用)取消了对此类代码的需求,并且简化了基于角色授予用户访问各种资源权限的任务。
角色管理是基于提供程序的,它通过Web.config启用。角色管理器通过新的Roles类来公开API,该类公开了名为CreateRole、DeleteRole和AddUserToRole等的方法。值得注意的是,您或许永远不需要调用这些方法,因为Webadmin.axd完全能够创建角色、将用户分配给角色以及完成其他任务。一旦启用,基于角色的安全性就能够使用所提供的角色信息以及Web.config文件中的URL身份验证指令来工作—这与ASP.NET 1.x中您已经熟悉的URL身份验证相同。
既然您已经熟悉了成员身份服务、登录控件以及ASP.NET角色管理器,那么您或许希望看到同时使用这三者的示例。本文可下载的代码示例包含一个两页的应用程序,它演示了Visual Studio 2005样式的窗体身份验证。要部署该应用程序并对其进行测试,请首先将PublicPage.aspx、LoginPage.aspx和Web.config复制到Web服务器的某个虚拟目录中。在该虚拟目录中创建一个名为Secure的子目录,然后将ProtectedPage.aspx和其他Web.config文件复制到该子目录中。
启动Webadmin.axd,并且将站点配置为使用窗体身份验证,将成员身份和角色服务配置为使用您选择的提供程序。同时,还要创建名为Bob和Alice的用户以及名为Manager和Developer的角色。将Bob指定为Manager角色,将Alice指定为Developer角色。(我不会列出所有步骤,因为在您使用任一方法阅读本文之前,它们很可能会改变。幸运的是,Webadmin.axd相当直观,并且它具有向导,可以引导您完成设置过程。)
接下来,在浏览器中激活PublicPage.aspx,并单击“View Secret Message”按钮以查看ProtectedPage.aspx。ASP.NET会将您重定向到LoginPage.aspx,该页面使用Login控件来请求用户名和密码。使用Bob的用户名和密码登录。ProtectedPage.aspx应该显示在浏览器窗口中,因为Bob是Manager,并且可以通过Secure目录中的Web.config文件将访问权授予经理。请注意LoginName控件显示的用户名以及LoginStatus控件显示的“Log out”链接。最后,关闭浏览器,然后重新打开它并重新激活PublicPage.aspx。单击“View Secret Message”并以Alice的身份登录。这一次,您将无法到达ProtectedPage.aspx,因为Alice不是经理。
我使用了一个类似的应用程序来讲述ASP.NET 1.x中的窗体身份验证,但1.x版本要求编写非常多的代码。2.0版本因其简洁而不同寻常,尤其是没有任何用于验证在登录窗体中键入的凭据、或者将用户名映射到角色的代码。如果您仍然不相信,请尝试用ASP.NET 1.x实现同一应用程序!此外,请确保检验Webadmin.axd对Web.config所作的更改。除了其他内容以外,您还应该看到一个启用角色Manager并且可能会指定角色管理提供程序的元素。
您可能很想知道角色Manager是否经过了到数据库(在其中,角色被存储到每个请求中)的往返行程。值得庆幸的是,答案是“否”。它将角色编码到Cookie中,并且为了保密而将它们加密。如果一个用户对应了众多的角色(这种情况不太可能发生),以至于无法将这些角色编码到一个Cookie中,则该Cookie将包含一个最近使用角色的列表,并且仅在迫不得已时才查询数据库。
个性化
另一个新增的服务是个性化,它提供了一种现成的解决方案,用于解决存储站点用户的个性化设置问题。目前,此类设置通常存储在Cookie、后端数据库或这两者中。无论这些设置存储在何处,ASP.NET 1.x都不能提供什么帮助。这需要由您来设置和管理后端数据存储,以及使用经过身份验证的用户名、Cookie或其他某种机制来关联个性化数据。
ASP.NET 2.0个性化服务使得存储各个用户的设置以及随意检索这些设置变得非常容易。该服务基于用户配置文件—您可以使用新的元素在Web.config中予以定义。下面的代码节选自Web.config:
<properties>
<add name="Theme" />
<add name="Birthday" Type="System.DateTime" />
<add name="LoginCount" Type="System.Int32" defaultValue="0" />
</properties>
</profile>
它定义了一个包含三个属性的配置文件:一个名为Theme的字符串,一个名为Birthday的DateTime值,以及一个名为LoginCount的整数。后面这个属性被赋予默认值0。
在运行时,您可以使用页面的Profile属性(该属性引用包含该配置文件中定义的属性的动态编译类的实例)来访问当前用户的这些属性。例如,下列语句可从当前用户的配置文件中读取属性值:
DateTime birthday = Profile.Birthday;
int logins = Profile.LoginCount;
还可以将值赋予配置文件属性:
Profile.Birthday = new DateTime (1959, 9, 30);
Profile.LoginCount = Profile.LoginCount + 1;
个性化服务的一个明显优势是强类型化。另一个优势在于个性化数据是按需读写的。请将此与会话状态(无论是否使用,都会将其加载并保存到每个请求中)进行对比。但是,个性化服务的最大优势可能在于您不必显式地将数据存储在任何位置;系统会替您完成该工作,并且它会永久性地存储数据,以便数据在您需要时随时可用。配置文件不会像会话那样超时。
那么,个性化数据存储在哪里呢?这要依具体情况而定。个性化服务基于提供程序,因此您可以将其配置为使用任何可用的提供程序。ASP.NET 2.0将至少附带两个个性化提供程序:一个用于Access,另一个用于SQL Server。如果您不另行指定,则个性化服务将使用Access提供程序,默认情况下,该提供程序会将个性化数据存储在本地Data\AspNetDB.mdb中。您可以通过修改Web.config(手动或使用Webadmin.axd)来改用SQL Server数据库。如果您不希望将个性化数据存储在Access数据库或SQL Server数据库中,则可以编写自己的提供程序。
默认情况下,ASP.NET使用经过身份验证的用户名作为所存储的个性化数据的键,但您也可以将其配置为支持匿名用户。首先,通过将以下语句添加到Web.config中来启用匿名标识:
<anonymousIdentification enabled="true" />
然后,将allowAnonymous="true"添加到您要为匿名用户存储的配置文件属性中:
<name="Theme" allowAnonymous="true" />
现在,Theme属性可以作为个性化设置使用,而无论站点的调用方是否经过了身份验证。
默认情况下,匿名标识使用Cookie来标识回返用户。由支持的属性可以用各种方式来配置这些Cookie。例如,您可以指定Cookie名称,并指明是否应该将该Cookie的内容加密。您还可以将个性化服务配置为使用无Cookie的匿名标识,因此它将依靠URL Munging来标识回返用户。甚至还存在一个自动检测选项:如果请求浏览器支持Cookie,则使用Cookie;如果不支持,则使用URL Munging。
要查看个性化的工作方式,请运行本文随附的下载资料中的Personalize.aspx示例。它会让站点的每个访问者选择一个主题,然后记录该主题,并且每当该访问者返回时都将应用该主题。请注意,该主题是在页面的PreInit事件(它是一个新事件,它的激发时间甚至早于Init)中以编程方式应用于该页面的。
在您运行该示例之前,需要启用匿名标识,并定义一个包含名为Theme的字符串属性的配置文件。以下代码行显示了执行上述两项任务的Web.config文件:
<system.web>
<anonymousIdentification enabled="true" />
<profile>
<properties>
<property name="Theme" allowAnonymous="true" />
</properties>
</profile>
</system.web>
</configuration>
SQL缓存依赖性
ASP.NET 1.x中令人遗憾地缺少的另一项功能是数据库缓存依赖性。可以将ASP.NET应用程序缓存中放置的项目与其他缓存项目联系起来,或者与文件系统中的对象联系起来,但不能与数据库实体联系起来。ASP.NET 2.0通过引入SQL缓存依赖性来纠正这一由于疏忽而造成的错误。
SQL缓存依赖性由新的SQLCacheDependency类的实例表示。它们的用法非常简单。下面的语句将一个名为ds的数据集插入到应用程序缓存中,并且在该数据集和Northwind数据库的Products表之间创建依赖性:
如果Products表的内容改变,则ASP.NET会自动删除该数据集。
SQL缓存依赖性还可以与ASP.NET输出缓存配合使用。下面的指令指示ASP.NET缓存来自包含页面的输出,直至Products表的内容改变或者满60秒为止(满足任一条件即可):
SqlDependency="Northwind:Products"
%>
SQL缓存依赖性适用于SQL Server 7.0、SQL Server 2000和即将问世的SQL Server 2005。对于SQL Server 2005,无需进行任何准备;但必须将SQL Server 7.0和SQL Server 2000数据库配置为支持SQL缓存依赖性。准备工作涉及到创建数据库触发器,以及创建一个特殊的表,以供ASP.NET在确定是否已经发生更改时参考。该表由一个后台线程使用可配置的轮询间隔(默认为5秒钟)来定期轮询。在SQL Server 2005中,要检测更改,既不需要特殊的表,也不需要轮询。此外,SQL Server 2005缓存依赖性可以在行级应用,而SQL Server 7.0和SQL Server 2000缓存依赖性在表级工作。您可以使用Aspnet_regsqlcache.exe工具或Webadmin.axd来准备数据库,以使其支持SQL缓存依赖性。
新的动态编译模型
ASP.NET 1.x中引入的众多创新之一是:系统能够在首次访问您的代码时对其进行编译。但是,只有页面能够被自动编译,并且辅助类(如数据访问组件)必须单独编译。
ASP.NET 2.0扩展了动态编译模型,以便能够自动编译几乎所有的组件。bin目录仍然保留以便实现向后兼容性,但它现在添加了名为Code和Resources的目录。Code目录中的C#和Visual Basic文件以及Resources目录中的RESX和RESOURCE文件被ASP.NET自动编译并缓存在系统子目录中。此外,落入Code目录中的Web服务描述语言(WSDL)文件被编译为Web服务代理,而XML架构定义语言(XSD)文件被编译为类型化数据集。通过Web.config,还可以扩展这些目录以支持其他文件类型。
预编译并且在不带源代码的情况下进行部署
提到动态编译,与ASP.NET 1.x有关的最常见问题之一是:是否可以预编译页面,以避免在首次访问页面时发生的编译延迟?尽管该问题本身在某种程度上无关紧要(延迟非常小,并且延迟的开销被成千上万甚至数以百万的后续请求所分摊),但Microsoft仍然感到有必要采取相应的措施来减轻开发人员的担忧。这一“措施”就是能够通过提交对名为precompile.axd的幻像资源的请求,来预编译应用程序中的所有页面。
但预编译并不仅限于此。另一个经常被请求的功能是:能够将整个应用程序预编译为可以在不带源代码的情况下进行部署的托管程序集(该功能在宿主方案中尤其有用)。ASP.NET 2.0包含一个名为Aspnet_compiler.exe的新的命令行工具,它能够执行预编译并且在不带源代码的情况下进行部署;Visual Studio 2005将包含类似的功能。下面的命令将预编译Web1目录中的应用程序,并且在不带源代码的情况下将其部署到Web2:
Aspnet_compiler -v /web1 -p c:\web1 c:\web2
之后,目标目录将包含空的ASP.NET文件(ASPX、ASCX、ASIX等等)以及源目录中存在的所有静态内容(如HTML文件、.config文件和图像文件)的副本。在不带源代码的情况下进行部署并不会为您的知识产权提供牢不可破的保护,因为聪明的ISP仍然可以通过反编译生成的程序集来弄清楚应用程序的来龙去脉,但是,它确实对一般的代码窃取者设置了更大的阻碍。
新的代码分隔模型
ASP.NET 1.x支持两种编程模型:内联模型—HTML和代码共存于同一个ASPX文件中;代码隐藏模型—它将HTML分隔到ASPX文件中,并将代码分隔到源代码文件(例如,C#文件)中。ASP.NET 2.0引入了第三个模型:一种新的代码隐藏形式,它依赖于Visual C#和Visual Basic .NET编译器中的不完全类支持。新的代码隐藏解决了原来的代码隐藏中存在的一个恼人的问题:传统的代码隐藏类必须包含受保护的字段,这些字段的类型和名称需要映射到ASPX文件中声明的相应控件。
代码7 显示了新的代码隐藏模型的工作方式。Hello.aspx包含页面的声明部分,Hello.aspx.cs包含代码。您应该注意@ Page指令中的CompileWith属性。此外,请注意MyPage类中缺少的字段(它们提供到ASPX文件中声明的控件的映射)。旧样式的代码隐藏仍然受支持,但新样式将是今后的首选编程模型。一点都不奇怪,Visual Studio 2005天生就支持新的代码分隔模型。
Hello.aspx
<%@ Page CompileWith="Hello.aspx.cs" ClassName="MyPage" %>
<html>
<body>
<form runat="server">
<asp:TextBox ID="Input" RunAt="server" />
<asp:Button Text="Test" OnClick="OnTest" RunAt="server" />
<asp:Label ID="Output" RunAt="server" />
</form>
</body>
</html>
Hello.aspx.cs
using System;
partial class MyPage
{
void OnTest (Object sender, EventArgs e)
{
Output.Text = "Hello, " + Input.Text;
}
}
代码7 Codebehind模型
客户端回调管理器
ASP.NET 2.0中我最喜欢的功能之一就是由新的客户端回调管理器提供的“轻量级回发”功能。在过去,ASP.NET页面必须回发给服务器才能调用服务器端代码。回发是低效的,因为它们将包含由页面控件生成的所有回发数据。它们还强制页面刷新,从而导致不雅观的闪烁。
ASP.NET 2.0引入了客户端回调管理器,它使页面无需完全回发就可以回调到服务器。回调是异步的,并且通过XML-HTTP来完成。它们不包含回发数据,并且不会强制页面刷新。(在服务器端,页面像平常一样执行至PreRender事件,但在即将呈现任何HTML之前停止。)它们确实需要支持XML-HTTP协议的浏览器(这通常意味着Microsoft Internet Explorer 5.0或更高版本)。
使用客户端回调管理器涉及三个步骤。首先,调用Page.GetCallbackEventReference以获取对某个特定函数(可以从客户端脚本中调用该函数,以执行到服务器的XML-HTTP回调)的引用。ASP.NET提供了该函数的名称和实现。其次,在客户端脚本中编写一个将在回调返回时调用的方法。方法名称是传递给GetCallbackEventReference的参数之一。第三,在页面中实现ICallbackEventHandler接口。该接口包含一个方法—RaiseCallbackEvent,当回调发生时,该方法将在服务器端调用。RaiseCallbackEvent所返回的字符串将被返回到第二步所述的方法。
代码8中的代码显示了客户端回调的工作方式,并且演示了它们的一个非常实际的用途。该页面显示了一个请求姓名和地址的窗体。在Zip Code字段中键入378xx或379xx邮政编码,然后单击Autofill按钮,City字段中将显示一个名称。值得注意的是,该页面会返回到服务器以获取城市名称,但它使用客户端回调而不是完全回发来完成此工作。在实际操作中,它可以找到某个数据库以将邮政编码转换为城市名称。请注意,该页面并不像页面在回发到服务器时通常所做的那样进行重新绘制。相反,更新是快速且简洁的!
<html>
<body>
<h1>Please Register</h1>
<hr>
<form runat="server">
<table>
<tr>
<td>First Name</td>
<td><asp:TextBox ID="FirstName" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>Last Name</td>
<td><asp:TextBox ID="LastName" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>Address 1</td>
<td><asp:TextBox ID="Address1" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>Address 2</td>
<td><asp:TextBox ID="Address2" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>City</td>
<td><asp:TextBox ID="City" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>State</td>
<td><asp:TextBox ID="State" RunAt="server" /></td>
<td></td>
</tr>
<tr>
<td>Zip Code</td>
<td><asp:TextBox ID="Zip" RunAt="server" /></td>
<td><asp:Button ID="AutofillButton" Text="Autofill"
RunAt="server" /></td>
</tr>
</table>
</form>
</body>
</html>
<script language="javascript">
// Function called when callback returns
function __onCallbackCompleted (result, context)
{
// Display the string returned by the server's RaiseCallbackEvent
// method in the input field named "City"
document.getElementById ('City').value = result;
}
</script>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
// Get callback event reference (e.g., "__doCallback (...)")
string cbref = GetCallbackEventReference (this,"document.getElementById ('Zip').value",
"__onCallbackCompleted", "null", "null");
// Wire the callback event reference to the Autofill button with
// an onclick attribute (and add "return false" to event reference
// to prevent a postback from occurring)
AutofillButton.Attributes.Add ("onclick",cbref + "; return false;");
}
// Server-side callback event handler
string ICallbackEventHandler.RaiseCallbackEvent (string arg)
{
if (arg.StartsWith ("378"))
return "Oak Ridge";
else if (arg.StartsWith ("379"))
return "Knoxville";
else
return "Unknown";
}
</script>
代码8callback.aspx
验证组
验证控件是ASP.NET 1.x中更为卓越的创新。诸如RequiredFieldValidator和RegularExpressionValidator之类的控件使开发人员能够在客户端和服务器上进行更为智能的输入验证,而不必成为客户端脚本编写和浏览器DOM方面的专家。遗憾的是,版本1.x验证控件存在一个致命的缺陷,即:没有一种比较好的方法来将这些控件组合在一起,以便页面的一个部分上的验证程序可以重写该页面其他部分上的验证程序,并且无论其他验证程序的状态如何,都可以使回发发生。
该问题由ValidationGroups1.aspx阐明,它包含在您可以针对本文下载的示例中。页面的设计者预计用户能够填写一组TextBox并回发到服务器,而不必同时填写另一个组,但它并不按此方式工作。除非所有输入字段都被填充,否则验证程序将抱怨不休,如图12所示。
图4 ASP.NET 1.x中的验证控件
ASP.NET 2.0中的新验证组功能一劳永逸地解决了该问题。现在,可以使用ValidationGroup属性来组合验证控件。可以用相同的方式将按钮控件分配给组,并且当一个组中的所有验证程序都对输入感到满意时,它们才允许回发发生,当然,前提是回发是由验证程序同一组中的控件生成的。ValidationGroups2.aspx演示了该技术(参见代码9)。从表面上看,该页面与ValidationGroups1.aspx完全相同。但在内部,它们却完全不同。现在,您可以填写任一组TextBox,并且通过单击TextBox验证组中的按钮进行回发。
<body>
<form runat="server">
<h1>New Users</h1>
<table>
<tr>
<td>User Name</td>
<td><asp:TextBox ID="NewUserName" RunAt="server" /></td>
<td><asp:RequiredFieldValidator ValidationGroup="NewUsers"
ControlToValidate="NewUserName" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td>Password</td>
<td><asp:TextBox ID="NewPassword1" TextMode="Password"
RunAt="server" /></td>
<td><asp:RequiredFieldValidator ValidationGroup="NewUsers"
ControlToValidate="NewPassword1" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td>Retype Password</td>
<td><asp:TextBox ID="NewPassword2" TextMode="Password"
RunAt="server" /></td>
<td><asp:RequiredFieldValidator ValidationGroup="NewUsers"
ControlToValidate="NewPassword2" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td>E-Mail Address</td>
<td><asp:TextBox ID="NewEMail" RunAt="server" /></td>
<td><asp:RequiredFieldValidator ValidationGroup="NewUsers"
ControlToValidate="NewEMail" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td></td>
<td><asp:Button ValidationGroup="NewUsers"
Text="Create Account" OnClick="OnCreateAccount"
RunAt="server" /></td>
<td></td>
</tr>
</table>
<hr>
<h1>Existing Users</h1>
<table>
<tr>
<td>User Name</td>
<td><asp:TextBox ID="UserName" RunAt="server" /></td>
<td><asp:RequiredFieldValidator
ValidationGroup="ExistingUsers"
ControlToValidate="UserName" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td>Password</td>
<td><asp:TextBox ID="Password" TextMode="Password"
RunAt="server" /></td>
<td><asp:RequiredFieldValidator
ValidationGroup="ExistingUsers"
ControlToValidate="Password" ErrorMessage="Required"
RunAt="server" /></td>
</tr>
<tr>
<td></td>
<td><asp:Button ValidationGroup="ExistingUsers"
Text="Log In" OnClick="OnLogIn" RunAt="server" /></td>
<td></td>
</tr>
</table>
</form>
</body>
</html>
<script language="C#" runat="server">
void OnCreateAccount (Object sender, EventArgs e) {}
void OnLogIn (Object sender, EventArgs e) {}
</script>
代码9 ValidationGroups2.aspx
跨页面发送
有关ASP.NET 1.x的最多抱怨是只允许页面回发到其本身。在版本2.0中,这一点通过引入跨页面发送而得到改变。要设置跨页面发送,您需要使用导致回发发生的控件的PostBackUrl属性来指定目标页面,如PageOne.aspx中的以下代码所示:
<body>
<form runat="server">
<asp:TextBox ID="Input" RunAt="server" />
<asp:Button Text="Test" PostBackUrl="PageTwo.aspx"
RunAt="server"
/>
</form>
</body>
</html>
当被单击时,PageOne.aspx中的按钮将回发到PageTwo.aspx:
<body>
<asp:Label ID="Output" RunAt="server" />
</body>
</html>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
TextBox input = (TextBox) PreviousPage.FindControl ("Input");
Output.Text = "Hello, " + input.Text;
}
</script>
PageTwo.aspx使用Page类的新PreviousPage属性来获取对起始页面的引用。对FindControl的简单调用将返回对PageOne.aspx中声明的TextBox的引用,以便可以检索用户的输入。
默认情况下,System.Web.UI.Page.PreviousPage返回对引起回发的页面的弱类型化引用。但是,如果PageOne.aspx是唯一能够向PageTwo.aspx发送的页面,则PageTwo.aspx可以使用新的@ PreviousPageType指令来获取对PageOne.aspx的强类型化访问,如下面的代码所示:
...
小结
ASP.NET 2.0还包含其他我尚未讨论的新功能。例如,内置的站点计数器服务使您能够记录站点使用情况的统计信息,并且在Webadmin.axd中或者在您自己的自定义GUI中查看它们。新的Web组件子系统提供了一个用于构建SharePoint服务器样式门户的框架。
现在正是学习ASP.NET 2.0的最佳时机,因为要在目前规划能够在将来轻松升级的体系结构,需要知道哪些功能即将问世(以及哪些功能不会问世)。您的ASP.NET 1.x应用程序无需修改就可以在2.0版本上运行,因为Microsoft已经允诺新平台将向后兼容旧平台。但是,未来将属于ASP.NET 2.0,而这一未来将意味着更加丰富的功能和更少的代码。有什么理由不接受它呢?