ASP.NET 用户配置 Part.3(自定义用户配置提供程序)
用户配置模型可以很好的嵌入 ASP.NET 网页。但是它的可配置性并不是非常好。你可能需要创建一个自定义的用户配置提供程序,原因如下:
- 需要将用户配置信息存储在其他类型的数据库里。比如,Oracle。
- 希望用户配置数据对其他程序也是有效的。分析 PropertyValuesString 和 PropertyValuesBinary 字段中的信息枯燥且易出错,如果要在其他程序或查询中获取信息,存储在一个字段彼此独立的表中会是一个很好的方案。
- 当保存或获取用户配置数据时,需要实现一些额外的逻辑。比如,验证、缓存、记录、加密或者压缩。
1. 自定义的用户配置提供程序类
为了实现自定义用户配置提供程序,需要继承 System.Web.Profile 命名空间的 ProfileProvider 抽象类;ProfileProvider 类又继承自 System.Configuration 命名空间的 SettingsProvider 抽象类;而 SettingsProvider 类则从 System.Configuration.Provider 命名空间的 ProviderBase 抽象类集成二来。基于此,你需要实现 SettingsProvider 类和 ProviderBase 类的成员。总的来讲,有一大堆的成员需要实现,之后才可以编写自定义的用户配置提供程序。
但是,这些方法并不是同等重要的。比如,你可以创建一个基本的提供程序,它通过实现 2 个或者 3 个这样的方法来保存和获取用户配置信息。其他许多方法提供了 ProfileManager 类的一些功能,比如删除用户配置或搜索不活动的用户配置。
所有这些方法都是非常容易实现的(仅仅需要一些基本的 ADO.NET 代码),但是,正确的实现每一个方法需要相当数量的代码。
用户配置提供程序的抽象成员
类 | 成 员 | 描 述 |
*ProviderBase | Name | 只读属性,返回当前提供程序的名称(在 web.config 中设置) |
*ProviderBase | Initialize() | 从 web.config 中获得配置元素,这些元素初始化了这个提供程序。这个方法给了你一个机会来读取自定义的设置,并将这些信息存储在成员变量中 |
*SettingsProvider | ApplicationName | 程序名(在 web.config 中设置),允许你区分存储在同一个数据库中的不同程序的用户 |
*SettingsProvider | GetPropertyValues() | 为一个单独用户获取用户配置信息。当一个网页访问 Page.Profile 属性时,这个方法会自动被调用。该方法可得到程序中定义的所有用户配置属性的列表,你必需为每个属性都返回一个值 |
*SettingsProvider | SetPropertyValues() | 更新一个用户的用户配置信息。当用户配置信息被修改后,在页面请求结束的时候这个方法会自动被调用。 |
ProfileProvider | DeleteProfiles() | 删除一个或多个用户的用户配置记录 |
ProfileProvider | DeleteInactiveProfiles() | 删除指定时间以来未被访问过的用户配置记录 |
ProfileProvider | GetAllProfiles() | 返回一组用户配置记录的信息。这个方法必须支持分页功能。 |
ProfileProvider | GetAllInactiveProfiles() | 查找指定时间以来未被访问过的用户配置记录 |
ProfileProvider | FindProfilesByUserName() | 基于一个或多个(如果你支持通配符匹配)用户名获取用户配置信息。但实际的用户配置信息不会被返回,只有一些标准的信息,比如最后活动时间等会被返回 |
ProfileProvider | FindInactiveProfilesByUserName() | 和上面类似,但以指定时间为基准进行查找 |
ProfileProvider | GetNumberOfInactiveProfiles() | 计算自从指定时间以来没有被访问过的记录个数 |
2. 设计 FactoredProfileProvider
FactoredProfileProvider 将属性值存储在一个数据库表中的一些列字段中,而不是一个单独的数据块中。这样其他程序和在不同查询中使用这些值就非常容易了。
从本质上讲,FactoredProfileProvider 解锁了用户配置表格,这样它就不再使用一个专有的模式了。唯一的缺点是,如果不修改数据库模式,你就不能修改用户配置或者将信息添加到用户配置中。
当实现一个自定义的用户配置程序时,你需要决定解决方案的通用性。比如,你决定使用 System.IO.Compression 命名空间中的类来实现压缩;或者使用 System.Security.Cryptography 命名空间中的类来实现加密。
FactoredProfileProvider 的基本思想是通过两个存储过程实现两个比较关键的功能(获取和更新用户配置信息)。这就带来了很大的灵活性,因为你可以在任何时候通过修改存储过程来使用不同的表、字段名、数据类型乃至序列化方式。
下面是在应用程序中使用 FactoredProfileProvider 的例子:
<profile defaultProvider="FactoredProfileProvider">
<providers>
<clear />
<add name="FactoredProfileProvider" type="FactoredProfileProvider" connectionStringName="SqlServices"
updateUserProcedure="Users_Update" getUserProcedure="Users_GetByUserName"/>
</providers>
<properties>
...
</properties>
</profile>
除了预期的特性(name、type、connectionStringName)之外,<add> 标签包含了两个新特性:updateUserProcedure 和 getUserProcedure。
这种设计允许你将 FactoredProfileProvider 用于任意的数据库表。但如何将属性映射到对应的列上呢?你可以采取很多方案来完成这个功能,但 FactoredProfileProvider 选择了一个捷径。当更新记录时,它只是假定你定义的每一个用户配置属性和存储过程参数的名字是一一对应的。
这种设计与 SqlDataSource 和 ObjectDataSource 控件的设计方式非常相似。虽然它强制你在两个存储过程中遵循一定的约定,但它没有其他限制。比如,更新存储过程可以将信息插入到任意表中的一系列字段中。
3. 编码实现 FactoredProfileProvider
创建 FactoredProfileProvider 类,并继承自 ProfileProvider:(所有在本例中没有实现的方法,都只简单的包含一行抛出异常的代码)
public class FactoredProfileProvider : ProfileProvider
{...}
1. 初始化
FactoredProfileProvider 需要跟踪一些基本信息,比如程序的提供名称、连接字符串和连个存储过程,且这些信息通过只读属性显现:
private string name;
public override string Name { get { return name; } }
private string connectionStringName;
public string ConnectionStringName { get { return connectionStringName; } }
private string updateProcedure;
public string UpdateProcedure { get { return updateProcedure; } }
private string getProcedure;
public string GetProcedure { get { return getProcedure; } }
为了设置这些详细信息,需要对方法 Initialize() 进行覆盖。你会收到一个包含了 <add> 元素中所有特性的集合。如果这些必需的信息缺失,就会引发异常。导入命名空间(System.Collections.Specialized):
/// <summary>
/// 初始化提供程序
/// </summary>
/// <param name="name">该提供程序的友好名称</param>
/// <param name="config">名称/值对的集合,表示在配置中为该提供程序指定的、提供程序特定的属性</param>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
this.name = name;
// Initialize values from web.config.
this.connectionString =
ConfigurationManager.ConnectionStrings[config["connectionStringName"]].ConnectionString;
if (string.IsNullOrEmpty(this.connectionString))
{
throw new HttpException("You must supply a connection string.");
}
this.updateProcedure = config["updateUserProcedure"];
if (string.IsNullOrEmpty(this.updateProcedure))
{
throw new HttpException("You must specify a stored procedure to use for updates.");
}
this.getProcedure = config["getUserProcedure"];
if (string.IsNullOrEmpty(this.getProcedure))
{
throw new HttpException("You must specify a stored procedure to use for retrieving user records.");
}
}
2. 读取用户配置信息
当网页访问任意的用户配置信息时,ASP.NET 会调用 GetPropertyValues() 方法,它会传递两个参数:
- SettingsContext 对象:包含诸如当前用户名之类的信息
- SettingsPropertyCollection 对象:包含程序中定义的所有用户配置属性的集合(可以被访问的)
你需要返回一个包含相应值的 SettingsPropertyValueCollection 集合。
public override SettingsPropertyValueCollection GetPropertyValues(
SettingsContext context, SettingsPropertyCollection collection)
{
// This collection will store the retrieved values.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
// ADO.NET code
SqlConnection conn = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(getProcedure, conn);
cmd.CommandType = CommandType.StoredProcedure;
// 这段代码唯一不可配置的假设是存储过程接受一个名为 @UserName 的参数
// 你可以添加其他配置特性来使这个参数名可配置化
cmd.Parameters.AddWithValue("@UserName", (string)context["UserName"]);
try
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
reader.Read();
foreach (SettingsProperty property in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(property);
if (reader.HasRows)
{
value.PropertyValue = reader[property.Name];
}
values.Add(value);
}
reader.Close();
}
finally
{
conn.Close();
}
return values;
}
3. 更新用户配置信息
在 SetPropertyValues() 方法中更新用户配置属性的任务,这时更新存储过程会被使用,每一个提供的值会被传递给有相同名字的参数。
public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection)
{
SqlConnection conn = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(updateProcedure, conn);
cmd.CommandType = CommandType.StoredProcedure;
foreach (SettingsPropertyValue value in collection)
{
cmd.Parameters.AddWithValue(value.Name, value.PropertyValue);
}
cmd.Parameters.AddWithValue("@UserName", (string)context["UserName"]);
try
{
conn.Open();
cmd.ExecuteNonQuery();
}
finally
{
conn.Close();
}
}
4. 测试 FactoredProfileProvider
为了测试这个例子,至少需要创建一个含有 Users 表和这两个存储过程的数据库。下图展示了一个提供地址信息的 Users 表:
一个简单的名为 Users_GetByUserName 的存储过程从这个表中查询用户信息:
create procedure Users_GetByUserName
@UserName varchar(50)
as
select * from Users where UserName = @UserName
Users_Update 存储过程一开始检查指定的用户是否存在。如果用户不存在,一条新的记录会使用配置信息被创建。如果用户存在,这个记录会被更新。这个设计和 SqlProfileProvider 是相吻合的。
CREATE PROCEDURE Users_Update
(
@UserName varchar(50),
@AddressName varchar(50),
@AddressStreet varchar(50),
@AddressCity varchar(50),
@AddressState varchar(50),
@AddressZipCode varchar(50),
@AddressCountry varchar(50)
)
AS
declare @Match int
select @Match = count(*) from Users where UserName = @UserName
if (@Match = 0)
insert into Users
(UserName, AddressName, AddressStreet, AddressCity,
AddressState, AddressZipCode, AddressCountry)
values
(@UserName, @AddressName, @AddressStreet, @AddressCity,
@AddressState, @AddressZipCode, @AddressCountry)
if (@Match = 1)
update Users set
UserName = @UserName,
AddressName = @AddressName,
AddressStreet = @AddressStreet,
AddressCity = @AddressCity,
AddressState = @AddressState,
AddressZipCode = @AddressZipCode,
AddressCountry = @AddressCountry
where UserName = @UserName
为了使用这个表,需要配置 FactoredProfileProvider,指定要使用的存储过程,然后定义要访问的 Users 表的所有字段即可:
<profile defaultProvider="FactoredProfileProvider">
<providers>
<clear />
<add name="FactoredProfileProvider"
type="FactoredProfileProvider"
connectionStringName="SqlServices"
updateUserProcedure="Users_Update"
getUserProcedure="Users_GetByUserName" />
</providers>
<properties>
<add name="AddressName"/>
<add name="AddressStreet"/>
<add name="AddressCity"/>
<add name="AddressState"/>
<add name="AddressZipCode"/>
<add name="AddressCountry"/>
</properties>
</profile>
向 web.config 添加一个连接字符串,可以添加一个页面进行测试了。页面的2个按钮代码如下:
protected void btnGet_Click(object sender, EventArgs e)
{
txtName.Text = Profile.AddressName;
txtStreet.Text = Profile.AddressStreet;
txtCity.Text = Profile.AddressCity;
txtZip.Text = Profile.AddressZipCode;
txtState.Text = Profile.AddressState;
txtCountry.Text = Profile.AddressCountry;
}
protected void btnSave_Click(object sender, EventArgs e)
{
Profile.AddressName = txtName.Text;
Profile.AddressStreet = txtStreet.Text;
Profile.AddressCity = txtCity.Text;
Profile.AddressZipCode = txtZip.Text;
Profile.AddressState = txtState.Text;
Profile.AddressCountry = txtCountry.Text;
}
测试效果如下:
最后,完整的自定义程序代码如下,并附上了一些详细的注释:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Profile;
using System.Collections.Specialized;
using System.Configuration;
using System.Data.SqlClient;
using System.Data;
public class FactoredProfileProvider : ProfileProvider
{
private string name;
public override string Name { get { return name; } }
private string connectionString;
public string ConnectionString { get { return connectionString; } }
private string updateProcedure;
public string UpdateProcedure { get { return updateProcedure; } }
private string getProcedure;
public string GetProcedure { get { return getProcedure; } }
/// <summary>
/// 初始化提供程序
/// </summary>
/// <param name="name">该提供程序的友好名称</param>
/// <param name="config">名称/值对的集合,表示在配置中为该提供程序指定的、提供程序特定的属性</param>
public override void Initialize(string name, NameValueCollection config)
{
this.name = name;
// Initialize values from web.config.
this.connectionString =
ConfigurationManager.ConnectionStrings[config["connectionStringName"]].ConnectionString;
if (string.IsNullOrEmpty(this.connectionString))
{
throw new HttpException("You must supply a connection string.");
}
this.updateProcedure = config["updateUserProcedure"];
if (string.IsNullOrEmpty(this.updateProcedure))
{
throw new HttpException("You must specify a stored procedure to use for updates.");
}
this.getProcedure = config["getUserProcedure"];
if (string.IsNullOrEmpty(this.getProcedure))
{
throw new HttpException("You must specify a stored procedure to use for retrieving user records.");
}
}
/// <summary>
/// 返回指定应用程序实例的 [属性/值] 集合,本例中是用户配置提供程序实例
/// </summary>
/// <param name="context">包含 UserName、IsAuchenticated 两个属性</param>
/// <param name="collection">用户配置中 properties 标签属性的集合</param>
/// <returns>获取数据库的值,再结合这些属性,重新构造成 [属性/值] 的名值对集合返回</returns>
public override SettingsPropertyValueCollection GetPropertyValues(
SettingsContext context, SettingsPropertyCollection collection)
{
// This collection will store the retrieved values.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
// ADO.NET code
SqlConnection conn = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(getProcedure, conn);
cmd.CommandType = CommandType.StoredProcedure;
// 这段代码唯一不可配置的假设是存储过程接受一个名为 @UserName 的参数
// 你可以添加其他配置特性来使这个参数名可配置化
cmd.Parameters.AddWithValue("@UserName", (string)context["UserName"]);
try
{
conn.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
reader.Read();
foreach (SettingsProperty property in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(property);
if (reader.HasRows)
{
value.PropertyValue = reader[property.Name];
}
values.Add(value);
}
reader.Close();
}
finally
{
conn.Close();
}
return values;
}
/// <summary>
/// 设置属性时被调用,更新数据库
/// </summary>
/// <param name="context">同上</param>
/// <param name="collection">遍历这个 [属性/值] 集合,向存储过程添加参数</param>
public override void SetPropertyValues(SettingsContext context,
SettingsPropertyValueCollection collection)
{
SqlConnection conn = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(updateProcedure, conn);
cmd.CommandType = CommandType.StoredProcedure;
foreach (SettingsPropertyValue value in collection)
{
cmd.Parameters.AddWithValue(value.Name, value.PropertyValue);
}
cmd.Parameters.AddWithValue("@UserName", (string)context["UserName"]);
try
{
conn.Open();
cmd.ExecuteNonQuery();
}
finally
{
conn.Close();
}
}
public FactoredProfileProvider() { }
public override int DeleteInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
{
throw new NotImplementedException();
}
public override int DeleteProfiles(string[] usernames)
{
throw new NotImplementedException();
}
public override int DeleteProfiles(ProfileInfoCollection profiles)
{
throw new NotImplementedException();
}
public override ProfileInfoCollection FindInactiveProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override ProfileInfoCollection FindProfilesByUserName(ProfileAuthenticationOption authenticationOption, string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override ProfileInfoCollection GetAllInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override ProfileInfoCollection GetAllProfiles(ProfileAuthenticationOption authenticationOption, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
public override int GetNumberOfInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
{
throw new NotImplementedException();
}
public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
}