Extending the Resource Application Block

New to this version is the ability to create your own EntLibContrib.Resource custom resource managers. However, a clear distinction must be made between EntLibContrib.Resource ones and System.Resources ones. An EntLibContrib.Resource resource manager is essentially a wrapper for a System.Resources one. Creating custom EntLibContrib.Resource resource managers allows you to employ a completely different resource management and provider model not even based on System.Resources resource managers if you desire. Creating custom System.Resoures resource managers however, is usually an essential part of creating new custom providers for the provider model supplied with the Resource Application Block.

Creating a Custom EntLibContrib.Resource.ResourceManager

The essential ingredient is to implement the EntLibContrib.Resource.IResourceManager interface and the best place to start is to see how the existing EntLibContrib.Resource.ResourceManager is written.

[C#]
public interface IResourceManager
{
	/// <summary>
	/// Gets the resource manager instance name.
	/// </summary>
	/// <value>The name.</value>
	string Name
	{
		get;
	}

	/// <summary>
	/// Gets or sets the culture info.
	/// </summary>
	/// <value>A <see cref="CultureInfo" /> object.</value>
	/// <remarks>The CultureInfo describes the language to use with respect to resources returned by the ResourceManager.</remarks>
	CultureInfo CultureInfo
	{
		get;
		set;
	}

	/// <summary>
	/// Gets the string resource for the given key.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <returns>string resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	string GetString(string key);

	/// <summary>
	/// Gets the string resource for the given key and culture.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <param name="culture">The culture.</param>
	/// <returns>string resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	string GetString(string key, CultureInfo culture);

	/// <summary>
	/// Gets the object resource for the given key.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <returns>object resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	object GetObject(string key);

	/// <summary>
	/// Gets the object resource for the given key and culture.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <param name="culture">The culture.</param>
	/// <returns>object resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	object GetObject(string key, CultureInfo culture);

	/// <summary>
	/// Gets the unmanaged memory stream resource for the given key.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <returns>an unmanaged stream resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	UnmanagedMemoryStream GetStream(string key);

	/// <summary>
	/// Gets the unmanaged memory stream resource for the given key and culture.
	/// </summary>
	/// <param name="key">The resource key.</param>
	/// <param name="culture">The culture.</param>
	/// <returns>an unmanaged stream resource value</returns>
	/// <remarks>this method is exposed for compatibility reasons only,
	/// it is recommended that you use indexers when you can.</remarks>
	UnmanagedMemoryStream GetStream(string key, CultureInfo culture);

	/// <summary>
	/// Gets the resource set.
	/// </summary>
	ResourceSet GetResourceSet();

	/// <summary>
	/// Gets the resource set.
	/// </summary>
	/// <param name="culture">The culture.</param>
	ResourceSet GetResourceSet(CultureInfo culture);

	/// <summary>
	/// Applies resources to all localizable properties of an object.
	/// </summary>
	/// <param name="value">The object itself.</param>
	/// <param name="objectName">the name of the object.</param>
	void ApplyResources(object value, string objectName);

	/// <summary>
	/// Applies resources to all localizable properties of an object.
	/// </summary>
	/// <param name="value">The object itself.</param>
	/// <param name="objectName">the name of the object.</param>
	/// <param name="culture">The culture.</param>
	void ApplyResources(object value, string objectName, CultureInfo culture);

	/// <summary>
	/// Releases all resources.
	/// </summary>
	void ReleaseAllResources();

	/// <summary>
	/// Indexes the Resource by key
	/// </summary>
	/// <param name="key">key to retrieve from resource</param>
	/// <value>String resource value</value>
	/// <overloads>Resource index to look up a named resource using a string key</overloads>
	/// <remarks>
	/// 	<para>If a match is not possible, with the given key, then a null reference is returned.</para>
	/// 	<para>The current thread culture is used to determine the most appropriate language, unless overidden by the CultureInfo property</para>
	/// </remarks>
	/// <example>
	/// This example gets a string message from a resource with a configured name of "Resource Manager".
	/// <code>
	/// public string GetMessage()
	/// {
	///	ResourceManager rm = ResourceFactory.GetResourceManager("Resource Manager");
	///	string s = rm["MSG_HELLO_WORLD"];
	///	return s;
	/// }
	/// </code>
	/// This might return something like "Hello World"
	/// </example>
	string this[string key] { get; }

	/// <summary>
	/// Overloaded indexer allows for embedded markers to be replaced with the arguments passed in args
	/// </summary>
	/// <param name="key">key to retrieve from resource</param>
	/// <param name="args">a variable number of arguments used as replacement strings</param>
	/// <value>String resource value with embedded markers replaced</value>
	/// <remarks>
	/// 	<para>markers are embedded in the format <c>{n}</c> where n is a sequential integer matching the index of the <c>args</c> parameter</para>
	/// 	<para>If a match is not possible, with the given key, then a null reference is returned</para>
	/// 	<para>The current thread culture is used to determine the most appropriate language, unless overidden by the CultureInfo property</para>
	/// </remarks>
	/// <example>
	/// This example gets a string message from resource and replaces a marker from a resource with a configured name of "Message Resource Manager".
	/// <code>
	/// public string GetReplaceMessage()
	/// {
	/// 	ResourceManager rm = ResourceFactory.GetResourceManager(Message Resource Manager);
	/// 	string replaceString = "Stevenage";
	/// 	string s = rm["MSG_HELLO_WORLD", replaceString];
	/// 	return s;
	/// }
	/// </code>
	/// If the message was "Hello {0}", This would return "Hello Stevenage"
	/// </example>
	[SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional")]
	string this[string key, params object[] args] { get; }

	/// <summary>
	/// Overloaded indexer to look up a named resource using a string key, object type and return an object
	/// </summary>
	/// <param name="key">key to retrieve from resource</param>
	/// <param name="type">the type of the resource used to verify the type of resource</param>
	/// <value>Object resource value</value>
	/// <remarks>
	/// 	<para>If a match is not possible, with the given key and type, then a null reference is returned</para>
	/// 	<para>The current thread culture is used to determine the most appropriate language, unless overidden by the CultureInfo property</para>
	/// 	<para>The <see cref="Type" /> parameter is used to verify the type of the returned object</para>
	/// </remarks>
	/// <example>
	/// This example gets an object from resource and casts it as an integer from a resource with a configured name of "Object Resource Manager".
	/// <code>
	/// public int GetResourceFlag()
	/// {
	/// 	ResourceManager rm = ResourceFactory.GetResourceManager("Object Resource Manager");
	/// 	Object o = rm["INT_MAX_MESSAGE_LENGTH", System.Int32];
	/// 	return (int)o;
	/// }
	/// </code>
	/// </example>
	/// <example>
	/// This example gets an audio object from resource and casts it as an <see cref="UnmanagedMemoryStream" /> from a resource with a configured name of "UMS Resource Manager".
	/// <code>
	/// public UnmanagedMemoryStream GetResourceAudio()
	/// {
	/// 	ResourceManager rm = ResourceFactory.GetResourceManager("UMS Resource Manager");
	/// 	Object o = rm["AUDIO_INTRO", System.IO.UnmanagedMemoryStream];
	/// 	return (UnmanagedMemoryStream)o;
	/// }
	/// </code>
	/// </example>
	[SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional")]
	object this[string key, Type type] { get; }
}

Creating a Custom Resource Provider

If you want to create your own resource providers there are two things you need to build. A custom System.Resources.ResourceManager 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 the Custom System.Resources.ResourceManager

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 the 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 does not contain any default entries in order to comply with the Enterprise Library standard for custom providers, however, you will need to ensure that some kind of base name is catered for.
  • 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 all the parameters that you need to pass to your custom resource provider constructor, 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. Also one of these custom attributes needs to be dedicated to the collection of a resource base name.

Finally you must register your custom resource provider type. Click on the ellipsis button next to the TypeName property 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 configuring, you should be able to use resources from your custom resource manager in exactly the same way as any other resource manager. You will need to access the Attributes collection from the CustomResourceProviderData object to get at your constructor parameters, performing any type conversion necessary, before creating instances of your custom resource provider.

Last edited Dec 22, 2009 at 7:38 PM by ewdev, version 11

Comments

No comments yet.