Extending the Resource Application Block
If you want to create your own resource providers there are two things you need to build. A
custom resource manager to handle the reading and extracting of resources from your storage device and a
custom resource provider to create instances of your custom resource manager and marshall configuration to the manager. Details on how these work can be found in the sections
Managers and
Providers. The following sections describe the main aspects to achieving this. You could go further and integrate your new provider into the
Configuration Console but that is beyond the scope of this documentation. The best way to start is to take the source code from one that already exists and derive your one using the code as an example however, the following guide may help to give you a kick-start.
Creating a Custom Resource Manager
There are several components to building your own resource manager, however, the first one to consider is the
Resource Manager itself. The following is a checklist of things you need to know:
- Your resource manager must inherit from the ExtendedComponentResourceManager. Alternatively, if your resource storage device is based on a file then there is a higher level abstraction called the FileResourceManager that you could use instead. The FileResourceManager will handle resource sets that take a file path name and a base name for you, in addition to inheriting from the ExtendedComponentResource Manager.
- Your resource manager must handle a mandatory string basename, any other properties are purely dependant on your requirements. The basename is used to segregate your resources into sub-sets.
- Override the InternalGetResourceSet() method; this is done for you if you are using the FileResourceManager base class. The following is a code sample taken from the DataResourceManager. The main feature is the creation of a custom ResourceSet and the handling of the fall-back mechanism. Recursive programming is employed to achieve this:
[C#]
/// <summary>
/// Provides the implementation for finding a <see cref="T:System.Resources.ResourceSet"></see>.
/// </summary>
/// <param name="culture">The <see cref="T:System.Globalization.CultureInfo"></see> to look for.</param>
/// <param name="createIfNotExists">If true and if the <see cref="T:System.Resources.ResourceSet"></see> has not been loaded yet, load it.</param>
/// <param name="tryParents">If the <see cref="T:System.Resources.ResourceSet"></see> cannot be loaded, try parent
/// <see cref="T:System.Globalization.CultureInfo"></see> objects to see if they exist.</param>
/// <returns>
/// The specified <see cref="T:System.Resources.ResourceSet"></see>.
/// </returns>
/// <exception cref="T:System.Resources.MissingManifestResourceException">The database contains no resources or fallback resources for the culture
/// given and it is required to look up a resource. </exception>
protected override ResourceSet InternalGetResourceSet (CultureInfo culture, bool createIfNotExists, bool tryParents)
{
DataResourceSet resourceSet = null;
// check the resource set cache first
if (ResourceSets.Contains(culture.Name))
resourceSet = (DataResourceSet)ResourceSets[culture.Name];
else
{
// create a new resource set
resourceSet = new DataResourceSet(database, baseName, culture);
// check the number of resources returned
if (resourceSet.Count == 0)
{
// try the parent culture if not already at the invariant culture
if (tryParents)
{
if (culture.Equals(CultureInfo.InvariantCulture))
throw new MissingManifestResourceException(database.ConnectionStringWithoutCredentials +
Environment.NewLine + this.baseName + Environment.NewLine + culture.Name);
// do a recursive call on this method with the parent culture
resourceSet = this.InternalGetResourceSet(culture.Parent, createIfNotExists, tryParents) as DataResourceSet;
}
}
else
{
// only cache the resource if the createIfNotExists flag is set
if (createIfNotExists)
ResourceSets.Add(culture.Name, resourceSet);
}
}
return resourceSet;
}
The next component that you need is a custom
ResourceSet. Again there are a number of things you need to know:
- Your custom ResourceSet must inherit from the CommonResourceSet. This resource set base class adds a few extra features to the standard ResourceSet base class
- Your constructor must take at least a string basename and a CultureInfo culture plus any other custom parameters that you need.
- You will need to override the GetDefaultReader(), GetDefaultWriter(), CreateDefaultReader() and CreateDefaultWriter() methods. The following is an example taken from the DataResourceSet:
[C#]
/// <summary>
/// Returns the preferred resource reader class for this kind of <see cref="T:System.Resources.ResourceSet"></see>.
/// </summary>
/// <returns>
/// Returns the <see cref="T:System.Type"></see> for the preferred resource reader for this kind of
/// <see cref="T:System.Resources.ResourceSet"></see>.
/// </returns>
public override Type GetDefaultReader ()
{
return typeof(DataResourceReader);
}
/// <summary>
/// Returns the preferred resource writer class for this kind of <see cref="T:System.Resources.ResourceSet"></see>.
/// </summary>
/// <returns>
/// Returns the <see cref="T:System.Type"></see> for the preferred resource writer for this kind of
/// <see cref="T:System.Resources.ResourceSet"></see>.
/// </returns>
public override Type GetDefaultWriter ()
{
return typeof(DataResourceWriter);
}
/// <summary>
/// Creates the default resource reader.
/// </summary>
/// <returns>IResourceReader instance</returns>
public override IResourceReader CreateDefaultReader()
{
return new DataResourceReader(database, baseName, cultureInfo);
}
/// <summary>
/// Creates the default resource writer.
/// </summary>
/// <returns>IResourceWriter instance</returns>
public override IResourceWriter CreateDefaultWriter()
{
return new DataResourceWriter(database, baseName, cultureInfo);
}
Finally you will need custom
System.Resources.IResourceReader and
System.Resources.IResourceWriter classes. These classes are where you implement your code to read from and write to your resource storage device.
The
IResourceReader requires that you implement a
GetEnumerator() method that provides an enumerator for your set of resources in your resource store. Don't forget that these should be filtered by your
basename and
culture. The following is an example taken from the
DataResourceReader:
[C#]
/// <summary>
/// Returns an <see cref="T:System.Collections.IDictionaryEnumerator"></see> of the resources for this reader.
/// </summary>
/// <returns>
/// A dictionary enumerator for the resources for this reader.
/// </returns>
public IDictionaryEnumerator GetEnumerator ()
{
Hashtable resources = new Hashtable();
DbCommand loadItemsCommand = database.GetStoredProcCommand("GetResources");
database.AddInParameter(loadItemsCommand, "@BaseName", DbType.String, baseName);
database.AddInParameter(loadItemsCommand, "@Culture", DbType.String, cultureName);
using(IDataReader resourceItems = database.ExecuteReader(loadItemsCommand))
{
while (resourceItems.Read())
{
string name = resourceItems["Name"].ToString();
object value = DeserializeValue(resourceItems["Value"]);
if (useDataNodes)
{
ResourceDataNode resourceDataNode;
string typeName = resourceItems["Type"].ToString();
if (typeName == typeof(ResourceFileRef).AssemblyQualifiedName)
resourceDataNode = new ResourceDataNode(name, (ResourceFileRef)value);
else
resourceDataNode = new ResourceDataNode(name, value);
resourceDataNode.Comment = resourceItems["Comment"].ToString();
resources.Add(name, resourceDataNode);
}
else
resources.Add(name, value);
}
}
return resources.GetEnumerator();
}
The
IResourceWriter requires that you implement the
AddResource() method with three overloads to write resources to your resource store. An additional
Generate() method exists to commit the added resources to storage. The following is an example taken from the
DataResourceWriter:
[C#]
/// <summary>
/// Adds a named resource of type <see cref="IResourceDataNode"></see> to the list of resources to be written.
/// </summary>
/// <param name="resourceDataNode">The resource data node.</param>
/// <exception cref="T:System.ArgumentNullException">The resourceDataNode parameter is null. </exception>
public void AddResource(IResourceDataNode resourceDataNode)
{
if (resourceDataNode == null)
throw new ArgumentNullException("resourceDataNode");
AddResource(resourceDataNode.Name, (object)resourceDataNode);
}
/// <summary>
/// Adds a named resource of type <see cref="T:System.Object"></see> to the list of resources to be written.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="value">The value of the resource.</param>
/// <exception cref="T:System.ArgumentNullException">The name parameter is null. </exception>
public void AddResource(string name, object value)
{
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("name");
resources.Add(name, value);
}
/// <summary>
/// Adds an 8-bit unsigned integer array as a named resource to the list of resources to be written.
/// </summary>
/// <param name="name">Name of a resource.</param>
/// <param name="value">Value of a resource as an 8-bit unsigned integer array.</param>
/// <exception cref="T:System.ArgumentNullException">The name parameter is null. </exception>
public void AddResource (string name, byte[] value)
{
AddResource(name, (object)value);
}
/// <summary>
/// Adds a named resource of type <see cref="T:System.String"></see> to the list of resources to be written.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="value">The value of the resource.</param>
/// <exception cref="T:System.ArgumentException">The name parameter is null. </exception>
public void AddResource (string name, string value)
{
AddResource (name, (object)value);
}
/// <summary>
/// Writes all the resources added by the <see cref="M:System.Resources.IResourceWriter.AddResource(System.String,System.String)">
/// </see> method to the output file or stream.
/// </summary>
public void Generate ()
{
if (resources.Count > 0)
{
using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
DbCommand clearCommand = database.GetStoredProcCommand("ClearResource");
database.AddInParameter(clearCommand, "@BaseName", DbType.String, baseName);
database.AddInParameter(clearCommand, "@Culture", DbType.String, cultureName);
database.ExecuteNonQuery(clearCommand);
foreach (DictionaryEntry resource in resources)
{
string typeName;
byte[] valueBytes;
object comment;
ResourceDataNode resourceDataNode = resource.Value as ResourceDataNode;
if (resourceDataNode == null)
{
typeName = resource.Value.GetType().AssemblyQualifiedName;
valueBytes = SerializationUtility.ToBytes(resource.Value);
comment = DBNull.Value;
}
else
{
if (resourceDataNode.FileRef == null)
{
typeName = resourceDataNode.TypeName;
valueBytes = SerializationUtility.ToBytes(resourceDataNode.Value);
}
else
{
typeName = (resourceDataNode.FileRef).GetType().AssemblyQualifiedName;
valueBytes = SerializationUtility.ToBytes(resourceDataNode.FileRef);
}
comment = resourceDataNode.Comment;
}
DbCommand insertCommand = database.GetStoredProcCommand("SetResource");
database.AddInParameter(insertCommand, "@BaseName", DbType.String, baseName);
database.AddInParameter(insertCommand, "@Culture", DbType.String, cultureName);
database.AddInParameter(insertCommand, "@Name", DbType.String, (string)resource.Key);
database.AddInParameter(insertCommand, "@Type", DbType.String, typeName);
database.AddInParameter(insertCommand, "@MimeType", DbType.String, DBNull.Value);
database.AddInParameter(insertCommand, "@value", DbType.Binary, valueBytes);
database.AddInParameter(insertCommand, "@Comment", DbType.String, comment);
database.ExecuteNonQuery(insertCommand);
}
resources.Clear();
transactionScope.Complete();
}
}
}
Note: both the resource reader and writer can make use of another object type called a
ResourceDataNode. This type is optional, based on a boolean
UseDataNodes switch, and is used to handle extra data items, such as a
comment, a
type name and a
file reference, in addition to a
key and a
value.
Creating a Custom Resource Provider
The
Resource Provider is responsible for creating resource manager instances and marshalling your set of parameters. The following is a checklist of things you need to know:
- Your resource provider must inherit from the ResourceProvider abstract class.
- Your resource provider class must be decorated with the ConfigurationElementType attribute and a parameter of typeof(CustomResourceProviderData)
[C#]
[ConfigurationElementType(typeof(CustomResourceProviderData))]
- If you are going to configure your provider using the CustomResourceProvider configuration type then your constructor must take a single NameValueCollection parameter. This means that your values are all strings so you may have to deserialize these values into their proper types in order to use them. The NameValueCollection will always contain an entry with a key name of resourceBaseName where the value is your mandatory base name.
- The abstract class implements the IResourceProvider interface that requires you to implement the CreateResourceManager() method that returns a ExtendedComponentResourceManager derived object. The following is an example taken from the DataResourceProvider:
[C#]
/// <summary>
/// Create a Resource Manager to manage the resource for an assembly resource provider
/// </summary>
/// <returns>
/// A <see cref="ExtendedComponentResourceManager"/> instance
/// </returns>
/// <remarks>
/// This method makes a type of <see cref="ExtendedComponentResourceManager"/> instance publicly available
/// based on the database instance and base name retrieved during Initialisation
/// </remarks>
public override ExtendedComponentResourceManager CreateResourceManager()
{
// Check for first time use of resource manager
if ( ResourceManager == null )
{
// Generate a Resource Manager
ResourceManager = new DataResourceManager(database, ResourceBaseName);
ResourceManager.IgnoreCase = true;
}
return ResourceManager;
}
Configuring your Custom Extension
See the section on
Configuration for details on how to configure a
CustomResourceProvider. The important thing to note is that for any parameters that you need to pass to your custom resource provider constructor, in addition to the resource base name, will be configured through the
Attributes collection:
Note: these custom attribute values are static. If you need different attribute value choices then you will need to implement a custom resource provider for each attribute value choice.
The next thing to do is to enter a
resource base name:
Note: the
ResourceBaseName will be added to the
Attributes collection automatically with a key name of
resourceBaseName.
Finally you must register your custom resource provider type. Click on the
ellipsis button to load the
type selector, you will not be able to load any assembly that does not contain custom resource provider types:

Now you are done and should be able to use resources from your custom resource manager in exactly the same way as any other resource manager.