DbResourceManager


The first of our custom resource managers is a resource manager that loads resources from a database. We approach this resource manager in two stages: reading from the database and, later in this chapter, writing to the database. The second writing phase won't be necessary for everyone, so if you intend to maintain your resources database yourself, you can skip the writing stage.

The first decision that you need to make when writing any resource manager is which class to inherit from. If you intend your resource managers to be used in Visual Studio 2005 Windows Forms applications, you should inherit from ComponentResourceManager because it contains the vital ApplyResources method used by the InitializeComponents method. For all other scenarios, the Component ResourceManager offers nothing beyond ResourceManager; to avoid dragging in unnecessary baggage, you should inherit from ResourceManager. In this chapter, I have chosen to inherit from ComponentResourceManager only so that the resource managers have the broadest possible appeal, but feel free to change this decision.

The basic implementation of our DbResourceManager (missing one method, which we shall come to) can be seen here:

 public class DbResourceManager: ComponentResourceManager {     private string baseNameField;     private static string connectionString =         "server=localhost;database=CustomResourceManagersExample;"+         "trusted_connection=true";     public static string ConnectionString     {         get {return connectionString;}         set {connectionString = value;}     }     protected virtual void Initialize(         string baseName, Assembly assembly)     {         this.baseNameField = baseName;         ResourceSets = new Hashtable();     }     public DbResourceManager(string baseName, Assembly assembly)     {         Initialize(baseName, assembly);     }     public DbResourceManager(string baseName)     {         Initialize(baseName, null);     }     public DbResourceManager(Type resourceType)     {         Initialize(resourceType.Name, resourceType.Assembly);     } } 


DbResourceManager inherits from ResourceManager. As there is no resource manager interface and no resource manager base class, we are forced to inherit from a working implementation of a resource manager. In this example, we want most of the ResourceManager methods intact, so this isn't so terrible. DbResourceManager has three constructors, which all call the protected Initialize method. These constructors match three of the ResourceManager constructors quite deliberately. I have taken the approach that the resource manager constructors should maintain as many common constructor signatures as possible. This commonality allows us to create a resource manager provider class later in this chapter. So even though the assembly parameter isn't used, it is still accepted. You might notice, though, that there is no constructor that allows us to pass a type for the resource set class. As the very purpose of this class is to change this type, it defeats the purpose of the class to pass this parameter.

The Initialize method assigns the baseName parameter to the private base-NameField field and initializes the protected ResourcesSets field (inherited from ResourceManager) to a Hashtable.

You can also see from this initial implementation that DbResourceManager has a private static string field called connectionString, which is exposed through a public static string property called ConnectionString. This is the connection string that is used to connect to the database. Strictly speaking, this should be a parameter passed to the DbResourceManager constructors, not a static property. If it were passed as a parameter and stored in an instance field, you would be able to have different resource managers that use different databases within the same application. I chose not to adopt this approach because (1) it would require a constructor signature that is specific to DbResourceManager and, therefore, would make it awkward to construct a DbResourceManager generically, and (2) I felt that it was unlikely that a single application would use two different resource databases simultaneously.

The only other method to implement is the InternalGetResourceSet method:

 protected override ResourceSet InternalGetResourceSet(     CultureInfo cultureInfo, bool createIfNotExists, bool tryParents) {     if (ResourceSets.Contains(cultureInfo.Name))         return ResourceSets[cultureInfo.Name] as ResourceSet;     else     {         DbResourceSet resourceSet =             new DbResourceSet(baseNameField, cultureInfo);         ResourceSets.Add(cultureInfo.Name, resourceSet);         return resourceSet;     } } 


This method looks in the ResourceSets Hashtable cache to see if a Resource Set has already been saved and, if it has, returns it. Otherwise, it creates a new DbResourceSet object, adds it to the cache, and returns that. The most obvious difference between this implementation and the ResourceManager implementation is that this implementation creates DbResourceSet objects, whereas the Resource Manager implementation, by default, creates RuntimeResourceSet (a subclass of ResourceSet) objects. Given that we know that ResourceManager has a constructor that accepts a ResourceSet type from which new resource sets can be created, you might wonder why we don't simply pass our DbResourceSet type to the constructor and save ourselves the trouble of overriding the InternalGetResourceSet method. The problem is that the ResourceManager.InternalGetResourceSet method performs both tasks of getting the resource stream and creating a new resource set. We don't want the InternalGetResourceSet method to get the resource stream, so we are forced to override it to prevent this from happening.

Note that there is no need in this class to override the GetString or GetObject methods, as they provide us with the functionality that we need.

The first implementation of our DbResourceSet looks like this:

 public class DbResourceSet: ResourceSet {     public DbResourceSet(         string baseNameField, CultureInfo cultureInfo):         base(new DbResourceReader(baseNameField, cultureInfo))     {     } } 


We will be modifying this class later when we add write functionality. The constructor accepts the baseNameField and culture passed from the InternalGet ResourceSet method. The constructor calls the base class constructor and passes an IResourceReader, which is taken from the newly created DbResourceReader object. If you read through the ResourceSet documentation, you will find two methods, GetdefaultReader and GeTDefaultWriter, which expect to be overridden and to return the Type of the resource reader and writer, respectively. I haven't implemented these yet because they aren't used anywhere in the .NET Framework. However, this is only to illustrate that they aren't necessary, and because you could consider this sloppy programming, I implement them in the second incarnation of this class.

The DbResourceReader looks like this:

 public class DbResourceReader: IResourceReader {     private string baseNameField;     private CultureInfo cultureInfo;     public DbResourceReader(         string baseNameField, CultureInfo cultureInfo)     {         this.baseNameField = baseNameField;         this.cultureInfo = cultureInfo;     }     public System.Collections.IDictionaryEnumerator GetEnumerator()     {     }     public void Close()     {     }     System.Collections.IEnumerator          System.Collections.IEnumerable.GetEnumerator()     {         return this.GetEnumerator();     }     public void Dispose()     {     } } 


The DbResourceReader implements the IResourceReader interface, which looks like this:

 public interface IResourceReader : IEnumerable, IDisposable {     void Close();     IDictionaryEnumerator GetEnumerator(); } 


The IResourceReader interface allows a caller to get an enumerator for the resources and to close the resource when it is finished with it. Because these two operations are distinct, it allows the resource reader to keep the source open and to read from it as needed. This is exactly what the ResourceEnumerator returned from ResourceReader.GetEnumerator does. This is why .resources files are kept open by file-based resource managers. In our database implementation, it doesn't make sense to read the resource item by item, so we have no implementation for the Close method. The DbResourceReader.GetEnumerator method looks like this:

 public System.Collections.IDictionaryEnumerator GetEnumerator() {     Hashtable hashTable = new Hashtable();     using(SqlConnection connection =        new SqlConnection(DbResourceManager.ConnectionString));     {         connection.Open();         using (SqlCommand command = GetSelectCommand(connection))         {             SqlDataReader dataReader = command.ExecuteReader();             while (dataReader.Read())             {                 object resourceValue =                     dataReader["ResourceValue"].ToString();                 object resourceType = dataReader["ResourceType"];                 if (resourceType != null &&                     resourceType.ToString() != String.Empty &&                     resourceType.ToString() != "System.String")                     resourceValue = GetResourceValue((string)                         resourceValue, resourceType.ToString());                 hashTable.Add(dataReader["ResourceName"].ToString(),                     resourceValue);             }             dataReader.Close();         }     }     return hashTable.GetEnumerator(); } 


We create a Hashtable to hold the resource retrieved from the database. We do this so that we can close the connection (or at least return it to the connection pool). Of course, this approach is wasteful if you use a resource manager to retrieve a single string and then discard the resource manager, but hopefully your resource managers are used for retrieving multiple resource values.

We create a new connection passing the static DbResourceManager.ConnectionString. I have chosen to hard-wire the references to SqlClient classes in this example so that it is simple to read and work with all versions of the .NET Framework. If you are using .NET Framework 2.0, you might like to replace these classes with appropriate calls to DbProviderFactory methods.

The DbResourceReader.GetEnumerator method simply opens a connection; creates a data reader; enumerates through the result set, adding the entries to the local Hashtable; closes the connection; and returns the Hashtable's enumerator. The GetresourceValue method is responsible for converting the resource's value from a string to the appropriate primitive, enum, struct, or class, according to the resource's type:

 protected virtual object GetResourceValue(     string resourceValue, string resourceTypeName) {     string className = resourceTypeName.Split(',')[0];     string assemblyName =         resourceTypeName.Substring(className.Length + 2);     Assembly assembly = Assembly.Load(assemblyName);     Type resourceType = assembly.GetType(className, true, true);     if (resourceType.IsPrimitive)         return Convert.ChangeType(resourceValue, resourceType);     else if (resourceType.IsEnum)         return Enum.Parse(resourceType, resourceValue, true);     else     {         // the type is a struct or a class         object[] parameterValues =             StringToParameterValues(resourceValue);         return Activator.CreateInstance(             resourceType, parameterValues);     } } 


So, for example, if the resourceTypeName is "System.Drawing.Content Alignment, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", then the className would be "System.Drawing.ContentAlignment" and the assemblyName would be "System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken= b03f5f7f11d50a3a". The Type, resourceType, would be loaded from the assembly, and resourceType.IsEnum would be true. The string value would then be converted to the ContentAlignment enum using Enum.Parse.

The GetSelectCommand method is:

 public virtual SqlCommand GetSelectCommand(SqlConnection connection) {     SqlCommand command;     if (cultureInfo.Equals(CultureInfo.InvariantCulture))     {         string commandText =             "SELECT ResourceName, ResourceValue, ResourceType "+             "FROM ResourceSets WHERE ResourceSetName=" +             "@resourceSetName AND Culture IS NULL";         command = new SqlCommand(commandText, connection);         command.Parameters.Add("@resourceSetName",             SqlDbType.VarChar, 100).Value = baseNameField;     }     else     {         string commandText =             "SELECT ResourceName, ResourceValue, ResourceType "+             "FROM ResourceSets WHERE ResourceSetName=" +             "@resourceSetName AND Culture=@culture";         command = new SqlCommand(commandText, connection);         command.Parameters.Add("@resourceSetName",             SqlDbType.VarChar, 100).Value = baseNameField;         command.Parameters.Add("@culture",             SqlDbType.VarChar, 20).Value = cultureInfo.ToString();     }     return command; } 


We create a command to retrieve the resources from the database. The command string will be one of the following values, depending on whether a culture is passed:

 SELECT ResourceName, ResourceValue, ResourceType FROM ResourceSets WHERE ResourceSetName=@resourceSetName AND Culture IS NULL SELECT ResourceName, ResourceValue, ResourceType FROM ResourceSets WHERE ResourceSetName=@resourceSetName AND Culture=@culture 


The SQL Server ResourceSets table is created from:

 CREATE TABLE [ResourceSets] ( [ResourceID]        [int] IDENTITY (1, 1) NOT NULL , [ResourceSetName]   [varchar] (50) NOT NULL , [Culture]           [varchar] (10) NULL , [ResourceName]      [varchar] (100) NOT NULL , [ResourceValue]     [varchar] (200) NOT NULL , [ResourceType]      [varchar] (250) , CONSTRAINT [PK_ResourceSets] PRIMARY KEY CLUSTERED ([ResourceID]) ON [PRIMARY] ) ON [PRIMARY] 


Figure 12.5 shows the ResourceSets table filled with the example data.

Figure 12.5. ResourceSets Table with Example Data


The SELECT statement simply retrieves ResourceName and ResourceValue fields for the given culture and the given resource set name. If the ResourceSetName is "CustomResourceManagersExample.Greetings" and the Culture is "en", the result set is shown in the following table.

ResourceName

ResourceValue

GoodMorning

Good Morning

GoodAfternoon

Good Afternoon


And voilàyou have a read-only database resource manager.

One downside of the DbResourceManager that you should consider is that you must decide what you should do if the resource database is unavailable. How will you report an error to the user if the text for the error is in the database that you have failed to connect to? One solution is to extend the DbResourceManager to have a fallback resource manager (preferably an assembly-based resource manager) that it could fall back to for failures in the resource manager itself.





.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net