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:

Adding Custom Attributes

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:

Adding a Base Name to the Custom Resource Provider

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:

Registering a Custom Resource Provider Type

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.

Last edited Mar 3, 2008 at 10:39 PM by ewdev, version 13

Comments

No comments yet.