ASP.NET 2.0 自定义 SQL Server 资源提供程序






4.79/5 (19投票s)
2006 年 5 月 22 日
5分钟阅读

219681

1830
如何创建自己的 ASP.NET 2.0 自定义资源提供程序,以 SQL Server 替换资源文件 (resx)。
引言
我正在开发一个中型的 ASP.NET 2.0 Web 应用程序,该应用程序需要国际化/全球化。ASP.NET 2.0 中默认的国际化方法使用 XML .resx 资源文件来存储特定语言的资源。总的来说,.aspx 文件和 .resx 文件之间存在一对多的关系。每个新的 .aspx 文件都需要一个或多个 .resx 文件。随着 Web 应用程序的增长,.resx 文件的开发和维护将成为一个问题。将资源存储在数据库(如 SQL Server)中不是很好吗?幸运的是,ASP.NET 2.0 的功能是可扩展的,您可以自己创建资源提供程序。
我找不到太多关于如何编写自己的资源提供程序的“官方”文档(如果您找到了,请告诉我!)。但我发现了一些不错的博客示例,并以此为基础。这里有一个非常好的 Microsoft Access Provider 示例,我以此为基础创建了自己的提供程序。我的示例可能不完全适合您的情况,但您可以将我的示例作为起点。
假设
本文档假定您已具备以下条件。首先,您对 ASP.NET 和默认的 .resx 资源文件实现方式有所了解。如果您需要回顾 ASP.NET 2.0 全球化,请查看 ASP.NET 2.0 QuickStart 教程。其次,您精通 C#。最后,您对 SQL 和 SQL Server 有基本的了解。
让我们开始编码...
您首先创建一个继承自 System.Web.Compilation.ResourceProviderFactory
的类。
public sealed class SqlResourceProviderFactory : ResourceProviderFactory
{
public SqlResourceProviderFactory()
{
}
public override IResourceProvider
CreateGlobalResourceProvider(string classKey)
{
return new SqlResourceProvider(null, classKey);
}
public override IResourceProvider
CreateLocalResourceProvider(string virtualPath)
{
virtualPath = System.IO.Path.GetFileName(virtualPath);
return new SqlResourceProvider(virtualPath, null);
}
}
ASP.NET 将调用此对象的各种方法。其中一个方法用于本地资源,另一个方法用于全局资源。到目前为止很简单。接下来,我们需要创建一个实现 System.Web.Compilation.IResourceProvider
的 SqlResourceProvider
类。
private sealed class SqlResourceProvider : IResourceProvider
{
private string _virtualPath;
private string _className;
private IDictionary _resourceCache;
private static object CultureNeutralKey = new object();
public SqlResourceProvider(string virtualPath, string className)
{
_virtualPath = virtualPath;
_className = className;
}
private IDictionary GetResourceCache(string cultureName)
{
object cultureKey;
if (cultureName != null)
{
cultureKey = cultureName;
}
else
{
cultureKey = CultureNeutralKey;
}
if (_resourceCache == null)
{
_resourceCache = new ListDictionary();
}
IDictionary resourceDict = _resourceCache[cultureKey] as IDictionary;
if (resourceDict == null)
{
resourceDict = SqlResourceHelper.GetResources(_virtualPath,
_className, cultureName, false, null);
_resourceCache[cultureKey] = resourceDict;
}
return resourceDict;
}
object IResourceProvider.GetObject(string resourceKey, CultureInfo culture)
{
string cultureName = null;
if (culture != null)
{
cultureName = culture.Name;
}
else
{
cultureName = CultureInfo.CurrentUICulture.Name;
}
object value = GetResourceCache(cultureName)[resourceKey];
if (value == null)
{
// resource is missing for current culture, use default
SqlResourceHelper.AddResource(resourceKey,
_virtualPath, _className, cultureName);
value = GetResourceCache(null)[resourceKey];
}
if (value == null)
{
// the resource is really missing, no default exists
SqlResourceHelper.AddResource(resourceKey,
_virtualPath, _className, string.Empty);
}
return value;
}
IResourceReader IResourceProvider.ResourceReader
{
get
{
return new SqlResourceReader(GetResourceCache(null));
}
}
}
好的,我们来研究提供程序的具体细节。这里最重要的 `GetObject()` 方法,因为这是 ASP.NET 调用以获取特定区域性(语言)的资源的方法。我们还需要创建另一个实现 System.Resources.IResourceReader
的类。坦白说,我不太确定为什么需要它,但 ASP.NET 在 Web 应用程序的生命周期的某个时刻肯定会调用它。
private sealed class SqlResourceReader : IResourceReader
{
private IDictionary _resources;
public SqlResourceReader(IDictionary resources)
{
_resources = resources;
}
IDictionaryEnumerator IResourceReader.GetEnumerator()
{
return _resources.GetEnumerator();
}
void IResourceReader.Close()
{
}
IEnumerator IEnumerable.GetEnumerator()
{
return _resources.GetEnumerator();
}
void IDisposable.Dispose()
{
}
}
到目前为止,我们只创建了连接 ASP.NET 的基础代码。我们仍然需要实现从 SQL Server 读取资源的类。但在那之前,让我们创建将用于存储资源数据的 SQL Server 表。我将它设计得很简单,因此没有包含此表的主键和索引信息。`RESOURCE_OBJECT`、`RESOURCE_NAME` 和 `CULTURE_NAME` 列应该包含在主键或唯一索引中,因为我们每个 ASP 页面每个区域性只需要一个资源。
CREATE TABLE ASPNET_GLOBALIZATION_RESOURCES
(
RESOURCE_OBJECT NVARCHAR(255), -- VIRTUAL PATH OR CLASS NAME
RESOURCE_NAME NVARCHAR(128),
RESOURCE_VALUE NVARCHAR(1000),
CULTURE_NAME NVARCHAR(50)
)
我选择将所有资源(本地和全局)存储在同一个表中。这个表很容易分成两个。为了方便维护,我宁愿将所有资源数据放在一个表中。`RESOURCE_OBJECT` 列存储 .aspx 文件(用于本地资源)或类名(用于全局资源)。`CULTURE_NAME` 列存储标识区域性/语言的字符串,如 en-US、fr-CA 或 es-MX。`RESOURCE_NAME` 和 `RESOURCE_VALUE` 列存储特定区域性和资源对象的名称/值对。还值得注意的是,我的实现只存储字符串数据。如果您需要存储二进制数据(如图像文件),则需要相应地修改此示例。
数据库访问代码
这是执行实际 SQL Server 数据库访问的类。您会注意到 `SqlResourceProvider` 类中的 `GetObject()` 方法使用这个静态类。
internal static class SqlResourceHelper
{
public static IDictionary GetResources(string virtualPath,
string className, string cultureName,
bool designMode, IServiceProvider serviceProvider)
{
SqlConnection con = new SqlConnection(
System.Configuration.ConfigurationManager.
ConnectionStrings["your_connection_string"].ToString());
SqlCommand com = new SqlCommand();
//
// Build correct select statement to get resource values
//
if (!String.IsNullOrEmpty(virtualPath))
{
//
// Get Local resources
//
if (string.IsNullOrEmpty(cultureName))
{
// default resource values (no culture defined)
com.CommandType = CommandType.Text;
com.CommandText = "select resource_name, resource_value" +
" from ASPNET_GLOBALIZATION_RESOURCES" +
" where resource_object = @virtual_path" +
" and culture_name is null";
com.Parameters.AddWithValue("@virtual_path",virtualPath);
}
else
{
com.CommandType = CommandType.Text;
com.CommandText = "select resource_name, resource_value" +
" from ASPNET_GLOBALIZATION_RESOURCES " +
"where resource_object = @virtual_path " +
"and culture_name = @culture_name ";
com.Parameters.AddWithValue("@virtual_path", virtualPath);
com.Parameters.AddWithValue("@culture_name", cultureName);
}
}
else if (!String.IsNullOrEmpty(className))
{
//
// Get Global resources
//
if (string.IsNullOrEmpty(cultureName))
{
// default resource values (no culture defined)
com.CommandType = CommandType.Text;
com.CommandText = "select resource_name, resource_value" +
" from ASPNET_GLOBALIZATION_RESOURCES " +
"where resource_object = @class_name" +
" and culture_name is null";
com.Parameters.AddWithValue("@class_name", className);
}
else
{
com.CommandType = CommandType.Text;
com.CommandText = "select resource_name, resource_value " +
"from ASPNET_GLOBALIZATION_RESOURCES where " +
"resource_object = @class_name and" +
" culture_name = @culture_name ";
com.Parameters.AddWithValue("@class_name", className);
com.Parameters.AddWithValue("@culture_name", cultureName);
}
}
else
{
//
// Neither virtualPath or className provided,
// unknown if Local or Global resource
//
throw new Exception("SqlResourceHelper.GetResources()" +
" - virtualPath or className missing from parameters.");
}
ListDictionary resources = new ListDictionary();
try
{
com.Connection = con;
con.Open();
SqlDataReader sdr = com.ExecuteReader(CommandBehavior.CloseConnection);
while (sdr.Read())
{
string rn = sdr.GetString(sdr.GetOrdinal("resource_name"));
string rv = sdr.GetString(sdr.GetOrdinal("resource_value"));
resources.Add(rn, rv);
}
}
catch (Exception e)
{
throw new Exception(e.Message, e);
}
finally
{
if (con.State == ConnectionState.Open)
{
con.Close();
}
}
return resources;
}
public static void AddResource(string resource_name,
string virtualPath, string className, string cultureName)
{
SqlConnection con =
new SqlConnection(System.Configuration.ConfigurationManager.
ConnectionStrings["your_connection_string"].ToString());
SqlCommand com = new SqlCommand();
StringBuilder sb = new StringBuilder();
sb.Append("insert into ASPNET_GLOBALIZATION_RESOURCES " +
"(resource_name ,resource_value," +
"resource_object,culture_name ) ");
sb.Append(" values (@resource_name ,@resource_value," +
"@resource_object,@culture_name) ");
com.CommandText = sb.ToString();
com.Parameters.AddWithValue("@resource_name",resource_name);
com.Parameters.AddWithValue("@resource_value", resource_name +
" * DEFAULT * " +
(String.IsNullOrEmpty( cultureName) ?
string.Empty : cultureName ));
com.Parameters.AddWithValue("@culture_name",
(String.IsNullOrEmpty(cultureName) ? SqlString.Null : cultureName));
string resource_object = "UNKNOWN **ERROR**";
if (!String.IsNullOrEmpty(virtualPath))
{
resource_object = virtualPath;
}
else if (!String.IsNullOrEmpty(className))
{
resource_object = className;
}
com.Parameters.AddWithValue("@resource_object", resource_object);
try
{
com.Connection = con;
con.Open();
com.ExecuteNonQuery();
}
catch (Exception e)
{
throw new Exception(e.ToString());
}
finally
{
if (con.State == ConnectionState.Open)
con.Close();
}
}
此类中的 `GetResources()` 方法执行实际的 SQL Server 数据库访问。代码很简单——构建一个 SELECT
语句,执行它,然后用名称/值对加载一个 ListDictionary
对象。您会注意到两个未使用的参数——`designMode` 和 `serviceProvider`。您可以将 SQL Server 提供程序编写成 Visual Studio 2005 可以调用它来预先填充您的数据库。我没有这样做。如果您对此感兴趣,可以查看我在本文开头提到的链接。
`AddResource()` 方法将在数据库中创建资源。当资源丢失时,我从 `SqlResourceProvider` 类中调用此方法。这只是一种自动发现任何丢失资源并填充 ASPNET_GLOBALIZATION_RESOURCES
表的方法。
最后,要让 ASP.NET 使用您的新类,您需要修改 web.config。
<system.web>
<globalization resourceProviderFactoryType=
"YourNameSpace.SqlResourceProviderFactory" />
</system.web>
结论
C# 代码在自己的命名空间中实现,包含 sealed
和 internal
类。除了良好的面向对象编程之外,编写 sealed
类还具有理论上的性能优势。我说“理论上”,因为我不能说我注意到了任何性能优势(或进行了任何基准测试),但这说得通。.NET 运行时可以进行优化,因为它知道 sealed
类永远不会被继承。
我将类和数据库结构设计成可以填充 ASPNET_GLOBALIZATION_RESOURCES
表中的数据,但将 CULTURE_NAME
设为 null,这会成为我的默认值。在 SqlResourceProvider
的 `GetObject()` 方法中,会尝试获取特定区域性的资源。如果返回 null,则会尝试获取具有 NULL
区域性的资源。之后,没有“回退”机制,这只是一个花哨的说法,意味着当尝试检索资源且 ResourceProvider 找不到资源时,调用 ASP.NET 代码将抛出错误。
由于找不到资源提供程序模型的任何文档,我不清楚提供程序是如何以及何时被调用的。我注意到我必须关闭浏览器并重新启动才能让提供程序从 SQL Server 加载新数据。似乎在 ASP.NET 请求资源后,它会将其缓存,直到浏览器关闭。
我希望您觉得这段代码很有用。我知道它可以扩展(并改进)以处理各种情况。