Introduction
There are two elements to this article; firstly the aim was to provide a mechanism of string resource and localisation that did not require the application to be recompiled ever time a literal changed or a new language had to be defined. Secondly the approach needed to offer an MVVM binding technique to make use of the tecnology.
Background
There are plently of good articles on code project that explain string resource and localisation
The particular idea behind this article was to develop a string resource/localisation solution where strings could be changed and new language support added after the application had been released. However I still wanted to use the built in resource handling components as much as possible and in particular the ResourceManager class. From an msdn and visual studio intellisence search I came across the CreateFileBasedResourceManager method. This method allowed external resource files to be imported at runtime. From the msdn article it also mentioned that a custom resource reader could be employed to parse the resources. This seemed perfect but unfortunately the article didn't go into detail on how this could be achived.
I then luckily came across this excellent article on codeproject which demonstrates this very idea.
My own artical is the result of combining the information from both sources together. I've also included a neat binding tip/trick that maintains the MVVM pattern.
Using the code
Firstly there is the custom ResourceSet and ResourceReader objects which deal with the understanding and parsing of the external resource files.
ResourceSet
Here is the defintion of my custom ResourceSet class. Its very basic, it simply returns an instance of my custom resource reader class.
class StringsResourceSet : ResourceSet
{
public StringsResourceSet(string fileName)
: base(new StringResourceReader(fileName))
{ }
public override Type GetDefaultReader()
{
return typeof(StringResourceReader);
}
}
ResourceReader
Here is the definition of my custom ResourceReader class which is actually responsible for reading the string resources.
class StringResourceReader : IResourceReader
{
#region Fields
string _fileName;
#endregion
#region Constructors
public StringResourceReader(string resourceFileName)
{
_fileName = resourceFileName;
}
#endregion
#region IResourceReader Members
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public void Close()
{
}
}
Please note that In my approach the resource files are simply text files with resource key and value separated by an equals sign e.g.
RESOURCEKEY=resourcevalue.
TESTSTRING=this is a test
However by changing the GetEnumerator implementation your resource files can really be in any format you wish. In the previously mentioned article the author has used a sql database.
public IDictionaryEnumerator GetEnumerator()
{
Hashtable htLanguage = new Hashtable();
using (StreamReader sr = new StreamReader(_fileName))
{
while (!sr.EndOfStream)
{
string[] lineParams = sr.ReadLine().Split('=');
htLanguage.Add(lineParams[0], lineParams[1]);
}
}
return htLanguage.GetEnumerator();
}
Here is the final segment of the StringResourceReader class
#region IDisposable Members
public void Dispose()
{
}
#endregion
ResourceViewModel
Here the view model class which is also responsible for initializing the resource manager class based on the above assets.
I am providing three pieces of information to the resource manager.
The first is the base name of my resource files. In my case all my resource files should have the base name of Strings. i.e. my resource files should be named Strings.Culture.resources where Culture repersents the region and language code. e.g. Strings.fr-FR.resources will be the file containing the France/French resources. You do not need to worry about how the correct region file is chosen that responsibility will be handled by .NET. You only need to ensure the correct naming scheme.
The second argument indicates the containing folder relative to the application working directory. In my case all of resource files will be in a subfolder called strings.
The third argument tells the ResourceManager to use the custom ResourceSet and ResourceReader previously discussed.
ResourceManager rm;
public ResourceViewModel()
{
rm = ResourceManager.CreateFileBasedResourceManager("Strings", "Strings", typeof(StringsResourceSet));
}
public ResourceManager Manager
{
get { return rm; }
}
}
XAML binding accessor
Here is the tip/trick for supporting binding from XAML. I have wrapped the Resource Manager GetString method in an indexer property which XAML supports binding to.
public string this[string name]
{
get { return rm.GetString(name); }
}
In the xaml you can bind to indexer properties as follows.
<TextBlock FontSize="50" Text="{Binding Path=[TESTSTRING]}"/>
The final part of the ResourceViewModel class contains a method for overiding the culture of all threads of the application. This is useful to test localisation without having to change the region information on the operating system. This code was taken from the following excellent artical which also explains why this particular piece of code is necessary.. http://blog.rastating.com/setting-default-currentculture-in-all-versions-of-net/.
public void SetDefaultCulture(CultureInfo culture)
{
Type type = typeof(CultureInfo);
try
{
type.InvokeMember("s_userDefaultCulture",
BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
null,
culture,
new object[] { culture });
type.InvokeMember("s_userDefaultUICulture",
BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
null,
culture,
new object[] { culture });
}
catch { }
try
{
type.InvokeMember("m_userDefaultCulture",
BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
null,
culture,
new object[] { culture });
type.InvokeMember("m_userDefaultUICulture",
BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
null,
culture,
new object[] { culture });
}
catch { }
}
}
}
In the XAML window class I'm intializing an instance of the view model class and setting it as the datacontext of the window. I also provide a hook for overriding the culture based on application settings if necessary.
ResourceViewModel viewModel = new ResourceViewModel();
public Window1()
{
InitializeComponent();
DataContext = viewModel;
if (!String.IsNullOrEmpty(Properties.Settings.Default.CultureInfo))
{
viewModel.SetDefaultCulture(CultureInfo.CreateSpecificCulture(Properties.Settings.Default.CultureInfo));
}
}
Points of Interest
It should be worth pointing out that this approach unfortunately does not provide new binding notication to the view when the culture is changed at runtime. Even if I implemented the INotifyPropertyChanged interface on my view model class I would not achived notifcation since at no point will the indexer property actually be changing.