65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (19投票s)

2006 年 5 月 22 日

5分钟阅读

viewsIcon

219681

downloadIcon

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.IResourceProviderSqlResourceProvider 类。

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# 代码在自己的命名空间中实现,包含 sealedinternal 类。除了良好的面向对象编程之外,编写 sealed 类还具有理论上的性能优势。我说“理论上”,因为我不能说我注意到了任何性能优势(或进行了任何基准测试),但这说得通。.NET 运行时可以进行优化,因为它知道 sealed 类永远不会被继承。

我将类和数据库结构设计成可以填充 ASPNET_GLOBALIZATION_RESOURCES 表中的数据,但将 CULTURE_NAME 设为 null,这会成为我的默认值。在 SqlResourceProvider 的 `GetObject()` 方法中,会尝试获取特定区域性的资源。如果返回 null,则会尝试获取具有 NULL 区域性的资源。之后,没有“回退”机制,这只是一个花哨的说法,意味着当尝试检索资源且 ResourceProvider 找不到资源时,调用 ASP.NET 代码将抛出错误。

由于找不到资源提供程序模型的任何文档,我不清楚提供程序是如何以及何时被调用的。我注意到我必须关闭浏览器并重新启动才能让提供程序从 SQL Server 加载新数据。似乎在 ASP.NET 请求资源后,它会将其缓存,直到浏览器关闭。

我希望您觉得这段代码很有用。我知道它可以扩展(并改进)以处理各种情况。

© . All rights reserved.