In my spare time I’m working on a smallish WPF application which will need to be localized. Looking at WPF’s current localization story left me wanting. What I really wanted was good old fashioned .NET resources accessible in xaml.
One option is to generate public resource files using the PublicResXFileCodeGenerator custom tool new to VS 2008. This provides access to the System.Windows.Markup.StaticExtension allowing us to reference a resource in xaml like so
<TextBlock Text="{x:Static src:MyResources.HelloWorld}" />
This is ok but it feels a bit verbose and we may not want to make our resources public. In that case we can create our own ResourceExtension markup extension which will have access to internals (provided it is defined in the same assembly).
internal class ResourceExtension : MarkupExtension
{
private readonly string _key;
public ResourceExtension(string key)
{
if (key == null)
throw new ArgumentNullException("key");
if (key == String.Empty)
throw new ArgumentException("Argument key cannot be empty.");
_key = key;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var propertyInfo = typeof(MyResources).GetProperty(_key,
BindingFlags.Static | BindingFlags.NonPublic);
Debug.Assert(propertyInfo != null, String.Format(CultureInfo.InvariantCulture,
"Unable to find specified key '{0}' in resource file.", _key));
// return null if the key does not exist
return propertyInfo != null ? propertyInfo.GetValue(null, null) : null;
}
}
Then we can use the following xaml
<TextBlock Text="{src:Resource HelloWorld}" />
Now we have fewer characters to type in our markup, our resources are internal, but we can’t reuse this class with the resource type hard coded into the ProvideValue method.
We can refactor the common code to a generic base class like so.
internal abstract class ResourceExtensionBase<T> :
MarkupExtension where T : class
{
protected string Key { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var propertyInfo = typeof(T).GetProperty(Key,
BindingFlags.Static | BindingFlags.NonPublic);
Debug.Assert(propertyInfo != null, String.Format(CultureInfo.InvariantCulture,
"Unable to find specified key '{0}' in resource file.", Key));
// return null if the key does not exist
return propertyInfo != null ? propertyInfo.GetValue(null, null) : null;
}
protected static void ValidateArguments(string key)
{
if (key == null)
throw new ArgumentNullException("key");
if (key == String.Empty)
throw new ArgumentException("Argument key cannot be empty.");
}
}
Then we need to declare the following markup extension for each resource file we need access to
internal class ResourceExtension :
ResourceExtensionBase<MyResources>
{
public ResourceExtension(string key)
{
ValidateArguments(key);
Key = key;
}
}
I wouldn’t say I love this approach. Creating a custom type for each resource file won’t scale well for large projects but it is working well for my small project.
Very cool.
I think there might be an improvement how to load resource:
in ProvideValue method, I changed to
MyResources.ResourceManager.GetString(_key, Thread.CurrentThread.CurrentCulture);
instead of using reflection.
We use reflection in order to use the generic resource type T, rather than type ‘MyResources’