ASP.NET 用户配置 Part.2(SQLProfileProvider)
SqlProfileProvider 允许将用户配置信息存储到一个 SQL Server 7.0 或者后续版本的数据库中。其实,你可以选择在任何数据库中创建用户配置表,但是,你无法修改其他任何的数据库架构。你只能操作指定的表名、列名、序列化方式。
你需要完成下面这些步骤使用用户配置:
- 创建用户配置表(SQL Server Express 版本,这步会自动发生)
- 配置用户配置提供程序
- 定义一些用户配置属性
- 激活验证功能
- 在网页代码中使用用户配置
1. 创建用户配置表
如果没有使用 SQL Server Express ,那么必须手动创建用户配置表。需要使用带有 -A p 命令行选项的 aspnet_regsql.exe 工具,需要你提供的信息是:服务器地址(-S)、数据库(-d)、连接数据的验证信息(-U 用户名 -P 密码)或者使用当前 Windows 帐号(-E)。
这是一个使用当前 Windows 帐号登录到数据库,然后在当前机器上创建默认的 aspnetdb 数据库的例子:
aspnet_regsql.exe -A p -E
用户配置使用的数据库
aspnet_Applications | 列出数据库中具有记录的所有网页程序。几个 ASP.NET 程序使用同一个 aspntdb 数据库是可用的,因此,需要将用户信息加以区分,这样对于每一个应用程序来讲用户配置都是不同的。注册用户配置提供程序时可分别设置不同的程序名。或者共享用户配置(设置相同的程序名) |
aspnet_Profile | 存储指定用户的用户配置信息。每个记录包含一个单独用户的完整配置信息。PropertyNames 字段列出属性的名称,PropertyValuesString 和 PropertyValuesBinary 字段列出所有属性的数据。每天记录还包含了最后更新的日期和时间 LastUpdateDate |
aspnet_SchemaVersions | 列出存储用户配置信息的被支持的架构。 |
aspnet_Users | 列出了用户名,并将其映射到 aspnet_Applications 表中的某个程序上。同时记录了最后请求的日期和时间(LastActivityDate)和这条记录是否属于某个匿名用户自动创建的(IsAnonymous) |
用户配置使用的数据库存储过程
aspnet_Applications_CreateApplications | 检查 aspnet_Applications 表中是否存在指定程序名,如果需要就创建它 |
aspnet_CheckSchemaVersion | 使用 aspnet_SchemaVersions 表检查指定架构版本是否支持某个功能(比如用户配置) |
aspnet_Profile_GetProfiles | 从 aspnet_Profile 表中获取某个指定 Web 程序的所有用户配置记录的用户名和更新时间,但不会返回实际的用户配置数据 |
aspnet_Profile_GetProperties | 获得指定用户名的用户配置信息。这个信息不会经过任何的分析处理。仅仅是返回底层的 4 个字段 |
aspnet_Profile_SetProperties | 为指定的用户设置用户配置信息。需要指定 3 个字段的值。用户配置中,没有任何方法可单独更新一个属性 |
aspnet_Profile_GetNumberOfInactiveProfiles | 返回指定的时间窗口内没有使用的用户配置记录 |
aspnet_Profile_DeleteInactiveProfiles | 移除指定的时间窗口内没有使用的用户配置记录 |
aspnet_Users_CreateUser | 为特定用户在 aspnet_Users 表中创建一条新记录。 |
aspnet_Users_DeleteUser | 从 aspnet_Users 表中移除一个指定的用户 |
2. 配置提供程序
有了数据库,可以通过 web.config 文件注册 SqlProfileProvider 了:配置一个连接字符串;在 <profile> 节中移除所有现存提供程序(使用 <clear>);添加一个新的 System.Web.Profile.SqlProfileProvider 实例:
<configuration>
<connectionStrings>
<add name="SqlServices" connectionString="data source=localhost;integrated security=true;catalog=aspnetdb;"/>
</connectionStrings>
<system.web>
<profile defaultProvider="">
<providers>
<clear />
<add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider"
connectionStringName="SqlServices" applicationName="TestApplication"/>
</providers>
</profile>
</system.web>
</configuration>
3. 定义用户配置属性
在 aspnet_Profile 表中存储数据之前,必须明确定义属性。在 web.config 文件中的 <profile> 节中添加 <properties> 元素,为想要存储的用户信息添加 <add> 标签,至少要为这个属性提供一个名称:
<system.web>
<profile defaultProvider="SqlProvider">
<properties>
<add name="FirstName"/>
<add name="LastName"/>
...
通常,还需要提供数据类型(默认字符串),可以指定任何可以序列化的 .NET 类 作为类型:
<properties>
<add name="FirstName" type="String"/>
<add name="LastName" type="String"/>
<add name="DateOfBirth" type="DateTime"/>
</properties>
用户配置属性特征
name | 属性名称 |
type | 任何 .NET 可序列化的属性类型 |
serializeAs | 指定序列化这个值时使用的格式(二进制、字符串、Xml 或者 ProviderSpecific) |
readOnly | 是否只读属性 |
defaultValue | 设置一个默认值 |
allowAnonymous | 指定这个属性可否用在匿名用户的用户配置中,默认 false |
provider | 默认情况下,所有属性由 <profile> 元素指定的提供程序管理。但你也可以为不同的属性指定不同的提供程序 |
4. 使用用户配置属性
用户配置存储在指定用户的记录中,所以在读取或写入信息之前需要对当前用户进行验证。可以使用任何类型的验证系统(Windows、表单、自定义验证系统)。因此,你需要添加一条授权规则阻止匿名用户对你计划要用到用户配置信息的页面或目录进行访问。
<system.web>
<authentication mode="Windows" />
<authorization>
<deny users="?"/>
</authorization>
通过当前页面的 Profile 属性可以访问用户配置信息。ASP.NET 会通过 System.Web.ProfileBase 派生一个新类来代表用户配置。此类包装了用户配置设置的一个集合。ASP.NET 为每一个 Profile 属性向该类中添加一个强类型的属性。这些强类型的属性只调用 ProfileBase 基类的 GetPropertyValue() 和 SetPropertyValue() 来读取并设置相应的用户配置值。
这个页面第一次运行,没有用户配置信息可以获取,也没有使用数据库连接。但是单击按钮后,用户信息会被获取:
protected void cmdShow_Click(object sender, EventArgs e)
{
lbl.Text = "First Name: " + this.Profile.FirstName + "<br />";
lbl.Text += "Last Name: " + this.Profile.LastName + "<br />";
lbl.Text += "Date of Birth: " + this.Profile.DateOfBirth.ToShortDateString();
}
如果单击设置用户配置数据按钮,用户配置信息会基于当前控件的值设置:
protected void cmdSet_Click(object sender, EventArgs e)
{
this.Profile.FirstName = txtFirst.Text;
this.Profile.LastName = txtLast.Text;
this.Profile.DateOfBirth = Calendar1.SelectedDate;
}
<form id="form1" runat="server">
<div>
<table >
<tr>
<td style="width: 99px" >
First Name:
</td>
<td >
<asp:TextBox ID="txtFirst" runat="server">Harriet</asp:TextBox></td>
</tr>
<tr>
<td style="width: 99px" >
Last Name:</td>
<td >
<asp:TextBox ID="txtLast" runat="server">Smythe</asp:TextBox></td>
</tr>
<tr>
<td style="width: 99px; height: 182px" >
Date of Birth:
</td>
<td style="height: 182px" >
<asp:Calendar ID="Calendar1" runat="server"></asp:Calendar>
</td>
</tr>
</table>
<br />
<br />
<asp:Button ID="cmdShow" runat="server" OnClick="cmdShow_Click" Text="Show Profile Data" />
<asp:Button ID="cmdSet" runat="server" OnClick="cmdSet_Click" Text="Set Profile Data" /><br />
<br />
<br />
<div style="background-color:Gray; border-right: 2px solid; border-top: 2px solid; border-left: 2px solid; border-bottom: 2px solid;">
<asp:Label ID="lbl" runat="server" EnableViewState="False" Font-Bold="True"></asp:Label></div>
</div>
</form>
5. 用户配置序列化
现在看看用户配置数据库中存储的字段值的形式。字段 PropertyNames 给出需要从 PropertyValueString 字段解析出每一个值的信息:
FirstName:S:0:7:LastName:S:7:6:DateOfBirth:S:13:95:
“冒号:”作为分隔符;基本的格式如下:
PropertyName:StringOrBinarySerialization:StartingCharacterIndex:Length:
可用在 web.config 文件中添加 serializeAs 特性来修改任何用户配置属性的序列化格式:
String | 将类型转换为一个字符串表示。 |
Xml | 使用 System.Xml.XmlSerialization.XmlSerializer 将类型转换为 XML 表示,与 Web 服务使用的类相同 |
Binary | 将类型转换为一个对应的二进制表示。只有 .NET 通过 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 才可以理解。最紧凑,但最不灵活。另外,二进制数据存储在 PropertyValuesBinary 字段中 |
ProviderSpecific | 执行自定义程序中实现的自定义序列化 |
查看现在的 PropertyValueString 是这样的:
HarrietSmythe<?xml version="1.0" encoding="utf-16"?><dateTime>2013-04-19T16:47:05.6157314+08:00</dateTime>
更改序列化设置:
<properties>
<add name="FirstName" type="String" serializeAs="Xml"/>
<add name="LastName" type="String" serializeAs="Xml"/>
<add name="DateOfBirth" type="DateTime" serializeAs="String"/>
</properties>
再次查看 PropertyValueString 的值:
<?xml version="1.0" encoding="utf-16"?><string>Harriet</string><?xml version="1.0" encoding="utf-16"?><string>Smythe</string>04/19/2013 17:26:57
当你修改了用户配置属性或者修改了其序列化方式后,会发生什么呢?
- 添加或删除属性仅会产生很小的影响。ProfileModule 模块会忽略表中提供而没有在 web.config 中定义的属性
- 如果 web.config 文件中存在,而表中不存在,ProfileModule 模块会使用默认的值
更大的变化,比如,重命名属性、修改某个属性的数据类型呢?都会在你试图读取用户配置信息时产生异常。更糟糕的是,用户配置信息的序列化格式是私有的,没有简单的办法将现存的用户配置数据移植到新的用户配置结构中。
不是所有类型都能被序列化为任意格式:
- 一个没有提供无參构造函数的类,不能以 XML 模式序列化
- 没有 Serializable 特性的类,不能以二进制的模式序列化
6. 用户配置组
如果有大量的用户配置设置并且一些设置从逻辑上来讲彼此相关,那么用户配置组可以很好的管理:
<properties>
<group name="Preferences">
<add name="LongDisplayMode" defaultValue="true" type="Boolean"/>
<add name="ShowSummary" defaultValue="true" type="Boolean"/>
</group>
<group name="Address">
<add name="Name" type="String"/>
<add name="Street" type="String"/>
<add name="City" type="String"/>
<add name="ZipCode" type="String"/>
<add name="Country" type="String"/>
</group>
</properties>
现在,在代码中可以通过组的名称来访问这些属性了。比如下面国家信息的访问:
lblCountry.Text = Profile.Address.Country;
分组管理只是完整的自定义结构或者类的一般替代方案。
7. 用户配置和自定义的数据类型
在用户配置中使用自定义的类非常容易,创建一个包装你所需要的信息的类即可。你可以使用公有成员变量或完整的属性过程。后者虽然较长,但却是更好的选择,因为它保证这个类将来可以支持数据绑定,并且给了你添加属性过程代码的灵活性。
下面是上例中稍短的一个 Adress 类,为了简明起见,使用了自动熟悉:
[Serializable()]
public class Adress
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public string State { get; set; }
public string Country { get; set; }
public Adress() { }
public Adress(string name, string street, string city, string zipCode,
string state, string country)
{
Name = name;
Street = street;
City = city;
ZipCode = zipCode;
State = state;
Country = country;
}
}
可以将这个类放在 App_Code 目录中。现在,在 web.config 文件中添加一个使用它的属性:
<properties>
name="Address" type="Address"/>
</properties>
现在,可以这样管理地址信息了:
this.Profile.Address = new Address("Joe Pasta", "34 Parkside Ave", "New York",
"10002", "New York", "U.S.A");
lbl.Text = "You are in " + this.Profile.Address.Country;
7.1 自定义类型序列化
默认情况下,所有的自定义数据类型都使用 XmlSerializer 的 XML 序列化方式来进行序列化。这个类在序列化能力上有限制,它仅从公共属性或成员变量中将值复制到一个纯粹的 XML 格式中。运行上面的代码会发现存储的 PropertyValuesString 字段值为:
<?xml version="1.0" encoding="utf-16"?>
<Address xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Joe</Name>
<Street>34 Parkside Ave</Street>
<City>New York</City>
<ZipCode>10002</ZipCode>
<State>New York</State>
<Country>U.S.A</Country>
</Address>
通过在类中添加来自 System.Xml.Serialization 命名空间的特性可以改变序列化后的 XML 的表现。比如,使用 XmlElement 重命名用于保存属性的 XML 元素;使用 XmlAttribute 确保属性被存储为 XML 特性而不是 XML 元素;使用 XmlIgnore 来防止属性值被一起序列化。
为 Address 类的属性添加如下的特性:
[System.Xml.Serialization.XmlElement("FullName")]
public string Name { get; set; }
[System.Xml.Serialization.XmlAttribute()]
public string Street { get; set; }
[System.Xml.Serialization.XmlAttribute()]
public string City { get; set; }
[System.Xml.Serialization.XmlAttribute("PostCode")]
public string ZipCode { get; set; }
public string State { get; set; }
[System.Xml.Serialization.XmlIgnore()]
public string Country { get; set; }
这个修改导致下面的变化:
- Name 属性序列化后被重命名为 FullName
- Street、City、ZipCode 被序列化为 XML 节点的特性,ZipCode 还被重命名为 PostCode
- Country 属性值没有被序列化
<?xml version="1.0" encoding="utf-16"?>
<Address Street="34 Parkside Ave" City="New York" PostCode="10002">
<FullName>Joe Pasta</FullName>
<State>New York</State>
</Address>
要获得更多信息,可以参考一些 MSDN 的文档。例如下面地址的文章示例:
http://msdn.microsoft.com/zh-cn/library/system.xml.serialization.xmlserializer(v=VS.80).aspx
反序列化类型时,XmlSerializer 必须能够找到一个没有参数的公共构造函数。此外,你的属性不能是只读的。如果违反了二者中的一个,反序列化过程就会失败。
这点其实很容易理解。反序列化时,XmlSerializer 寻找你的无參构造函数,并为所有的属性进行赋值,然后创建了 Profile 实例。如果你的类添加了构造函数,那么就必须显式的添加一个无參构造函数。
如果修改 web.config 为二进制序列化模式,.NET 会使用一种完全不同的方式:
<add name="Address" type="Address" serializeAs="Binary"/>
这种情况下去,ProfileModule 模块需要 BinaryFormatter 的帮助。BinaryFormatter 可以序列化任意类的全部公有内容和私有内容,只要这个类含有 Serializable 特性。此外,它所继承或引用的任何类也必须是可序列化的。
7.2 自动保存
用来保存用户配置信息的 ProfileModule 模块无法在复杂数据类型情况下自动判断信息的改变。这意味着 ProfileModule 会在每次请求结束时都保存用户配置信息,每次都会!
这显然增加了不必要的负载。优化这一行为有许多选择。
- 将相关的用户配置属性设为只读(如果你知道它从不会改变):例如,<add name="IdentityCard" readOnly="true"/>
- 禁止自动保存功能:<profile defaultProvider="SqlProvider" automaticSaveEnabled="false">
如果禁用了自动保存,那么在适当的时机你需要显式调用 Profile.Save() 提交修改。通常,这是最方便的,你可以很容易的在代码中找出用户配置被修改的地方。
最后一个选择,就是在 global.asax 文件中处理 ProfileModule.ProfileAutoSaving 事件。你可以检查是否真的需要保存,不是必需的话可以取消。可以在内存中保存原先的用户配置数据,然后在 ProfileAutoSaving 事件中用这些数据与当前对象比较。但是这样的做法实在太笨而且效率低。
更好的做法是让页面设立一个标志来跟踪是否做过修改。见下图:
这个页面所有的文本框都会以相同的事件处理程序处理 TextChanged 事件:
protected void txt_TextChanged(object sender, EventArgs e)
{
// Page.Context 提供 HttpContext 对象。HttpContext.Items 集合提供临时存储数据的地方
// 这些数据会在以后同一个回传中使用
this.Context.Items["AddressDirtyFlag"] = true;
}
注意,这种方式存储的值只能在当前的请求过程中存在。本例中不会产生什么问题。因为用户只有两个选择:拒绝修改或应用修改。但如果你创建了另一个页面,用户可以通过好几个步骤来进行修改,然后在最后一步提交修改,那么你需要更多的工作维护这个标志。(在会话状态或者视图状态中存储这个标志是行不通的,因为 global.asax 文件中的 ProfileAutoSaving 事件触发时,它们是无效的)
以下是你需要的事件处理程序,可以用来判断当修改发生时是否允许自动保存:
void Profile_ProfileAutoSaving(object sender, ProfileAutoSaveEventArgs e)
{
if (e.Context.Items["AddressDirtyFlag"] == null ||
(bool)e.Context.Items["AddressDirtyFlag"] == false)
{
e.ContinueWithProfileAutoSave = false;
}
}
总的来说,对于复杂情况下,禁用掉自动保存功能,而强迫页面使用 Profile.Save() 方法保存信息通常是最容易的。
8. 用户配置 API
虽然页面会自动为当前用户获取用户配置信息,但这并不妨碍你获取其他用户的信息并修改它们。
利用两个类可以做到这点。ProfileBase 对象(Page.Profile 属性提供)包含一个 GetProfile() 方法,通过用户名获取特定用户配置信息:
protected void cmdShow_Click(object sender, EventArgs e)
{
ProfileCommon profile = this.Profile.GetProfile(txtUserName.Text);
lbl.Text = "This user lives in " + profile.Address.Country;
}
ProfileCommon 对象在 .NET 库中是找不到的。它是 .NET 动态产生的类,作为 Web 应用程序保存用户配置信息。ProfileCommon 对象唯一的区别是不能自动保存修改,你必需手动调用 Save() 方法。
System.Web.Profile 命名空间中的 ProfileManager 类提供了非常有用的静态方法,这些方法有许多与 ProfileInfo 类打交道。ProfileInfo 该类提供关于某个用户配置的信息。
ProfileInfo 类包含 UserName、LastUpdateDate、LastActivityDate(最后活动日期)、Size(以字节为单位的用户配置大小)、IsAnonymous(是否属于匿名用户) 属性,但它并不提供实际的值。
ProfileManager 方法
DeleteProfile() | 删除指定用户的用户配置 |
DeleteProfiles() | 同时删除多个用户配置,需提供一个用户名数组 |
DeleteInAcitveProfiles() | 删除自某个时间以来没有使用过的用户配置。时间你指定,同时还需要一个来自 ProfileAuthenticationOption 枚举,决定何种类型的用户配置要被删除:All、Anonymous、Authenticated |
GetNumberOfProfiles() | 返回数据源中用户配置记录的数目 |
GetNumberOfInActiveProfiles() | 返回指定某个时间以来没有使用的用户配置的数目 |
GetAllInActiveProfiles() | 返回指定某个时间以来没有使用的用户配置的信息 |
GetAllProfiles() | 返回所有用户配置数据,有几个重载的版本可指定条件 |
FindProfilesByUserName() | 获取匹配自定用户名的用户配置集合。SqlProfileProvider 使用 LIKE 条件来查询,这意味着你可以使用通配符,比如 user% 会得到 user1、user2… |
FindInactiveProfilesByUserName() | 返回指定时间以来没有使用过的用户配置信息 |
举个例子,如果要为当前用户删除用户配置,只需一条代码:
ProfileManager.DeleteProfile(User.Identity.Name);
显示所有的用户,不包括匿名用户:
protected void Page_Load(object sender, EventArgs e)
{
this.GridView1.DataSource = ProfileManager.GetAllProfiles(ProfileAuthenticationOption.Authenticated);
this.GridView1.DataBind();
}
9. 匿名用户配置
至今为止,本文所有的例子都假定在访问或者保存任何用户配置信息之前用户都已经过了验证,通常情况是这样的。但有时为新的,不知名的用户创建一个临时的用户配置也是非常有用的。(比如,大多数电子商务网站允许新用户在注册之前向他们的购物车中添加商品)
ASP.NET 的匿名验证功能填补了这一空白。基本思路就是自动为每个匿名用户创建一个随机的标识,这个随机的标识将用户配置信息存储在数据库中,虽然没有提供用户 ID。用户 ID 通过客户端 cookie 来进行跟踪(或者在 URL 里面,如果你激活了无 cookie 模式)。一旦 cookie 消失了(比如,这个匿名用户关闭然后又重新打开浏览器),当前的匿名会话会丢失,然后一个新的匿名会话被创建。
匿名验证功能会潜在的遗留很多失效的用户配置,因此默认此功能是被禁止的,但你可以这样激活:
<system.web>
<anonymousIdentification enabled="true"/>
</system.web>
对每一个用户配置属性进行标记,通过添加 allowAnonymous 特性并设为 true,这样属性就会为匿名用户保留。这样可以限制匿名用户只保留一些基本信息,而通过验证的客户才可以使用更大的对象。
<properties>
<add name="Address" type="Address" allowAnonymous="true"/>
...
</properties>
anonymousIdentification 元素提供了众多可选特性。可设置 cookie 名称和过期时间、是否只在 SSL 连接的情况下发放 cookie、是否使用 cookie 保护、配置支持无 cookie ID 跟踪等,下面是个例子:
<anonymousIdentification enabled="true" cookieName=".ASPANONYMOUS" cookieTimeout="43200"
cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true"
cookieProtection="All" cookieless="UseCookies"/>
另外,多余的无效的匿名用户配置信息可以通过存储过程 aspnet_Profile_DeleteInactiveProfiles 执行定期删除。
移植匿名用户
当先前的匿名用户登录后,如何处理他的用户配置信息?比如,在一个电子商务网站,用户极可能先选几个商品,然后注册或登录来完成购物。你需要保证购物车的信息从匿名用户的配置中复制到相应的已验证用户配置中。
ASP.NET 通过 ProfileModule.MigrateAnonymous 事件提供了一个解决方案。任何时候只要一个匿名标识有效且当前用户已经过验证,这个事件就会触发(在 global.asax 文件中处理)。
基本思路是:传递事件参数提供的匿名 ID 加载匿名用户的用户配置;手动转移到当前用户的用户配置中去,转移多少属性你可以自行决定;最后,清除匿名用户配置;还要清除匿名标识(这样就不会再次触发 MigrateAnonymous 事件了)。
void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e)
{
// Get the anonymous profile.
ProfileCommon anonProfile = Profile.GetProfile(e.AnonymousID);
// Copy information to the authenticated profile but only if there's information there.
if (anonProfile.Address.Name!=null&&anonProfile.Address.Name!="")
{
Profile.Address = anonProfile.Address;
}
// Delete the anonymous profile from the database.
// (You could decide to skip this step to increase performance)
System.Web.Profile.ProfileManager.DeleteProfile(e.AnonymousID);
// Remove the anonymous identifier.
AnonymousIdentificationModule.ClearAnonymousIdentifier();
}
需要小心处理这个任务。如果激活了匿名标识,每次用户登录都会触发此事件,即使这个用户没有在匿名配置中输入任何信息。如果不仔细,就会把为匿名用户配置的空白数据覆盖到用户真实的已保存的用户配置。总之,这里的处理有时会非常的繁琐和复杂。