Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Dynamic string resource \ localisation and MVVM.

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
16 Jun 2015CPOL4 min read 18.2K   214   4   1
Dynamic string resource\localisation and XAML binding.

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.

/// <summary>
/// Custom resource set implementation.
/// </summary>
class StringsResourceSet : ResourceSet
{
    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="fileName">The fil</param>
    public StringsResourceSet(string fileName)
        : base(new StringResourceReader(fileName))
    { }

    /// <summary>
    /// //Return custom reader as default one for reading language resources 
    /// </summary>
    /// <returns></returns>
    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.

/// <summary>
/// Custom resource reader for strings.
/// </summary>
class StringResourceReader : IResourceReader
{
    #region Fields
    /// <summary>
    /// The resource file name to load.
    /// </summary>
    string _fileName;
    #endregion

    #region Constructors
    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="baseName">The resource filename to load resources from.</param>
    public StringResourceReader(string resourceFileName)
    {
        //Set fields.
        _fileName = resourceFileName;
    }
    #endregion

    #region IResourceReader Members
    /// <summary>
    /// //Implement IEnumerable interface 
    /// </summary>
    /// <returns></returns>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    /// <summary>
    /// Required for interface implementation.
    /// </summary>
    public void Close()
    {
        //throw new NotImplementedException();
    }
}

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. 

/// <summary>
/// To get enumerator to iterate through the resources hashtable
/// </summary>
/// <returns></returns>
public IDictionaryEnumerator GetEnumerator()
{
    //Hashtable to store Key-values pairs for resources
    Hashtable htLanguage = new Hashtable();

    //Read the file.
    using (StreamReader sr = new StreamReader(_fileName))
    {
        //While not end of file.
        while (!sr.EndOfStream)
        {
            //Split the resource key and value.
            string[] lineParams = sr.ReadLine().Split('=');
            //Add resource to hash table.
            htLanguage.Add(lineParams[0], lineParams[1]);
        }
    }

    //Return enumerator.
    return htLanguage.GetEnumerator();
}

Here is the final segment of the StringResourceReader class

#region IDisposable Members
/// <summary>
/// Required for interface implementation.
/// </summary>
public void Dispose()
{
    //throw new NotImplementedException();
}
#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.

/// <summary>
/// Resources view model.
/// </summary
public class ResourceViewModel
{
    /// <summary>
    /// Resource manager for string literals. 
    /// </summary>
    ResourceManager rm;
    
    /// <summary>
    /// Default constructor.
    /// </summary>
    public ResourceViewModel()
    {
        //Initialize resource manager using custom resource set and resource reader.
        rm = ResourceManager.CreateFileBasedResourceManager("Strings", "Strings", typeof(StringsResourceSet));
    }
 
    /// <summary>
    /// Gets the resource manager.
    /// </summary>
    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.

/// <summary>
/// Gets the resource matching the specified name.
/// </summary>
/// <param name="name">The name of the resource of </param>
/// <returns></returns>
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/.

/// <summary>
/// Special implementation to set application wide culture.
/// </summary>
/// <param name="culture">The culture to set for the application.</param>
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.

/// <summary>
/// Initialize view model class.
/// </summary>
ResourceViewModel viewModel = new ResourceViewModel();

/// <summary>
/// Default constructor.
/// </summary>
public Window1()
{
    //Initialize.
    InitializeComponent();

    //Set the view model.
    DataContext = viewModel;

    //If there is another culture defined.
    if (!String.IsNullOrEmpty(Properties.Settings.Default.CultureInfo))
    {
        //Override the default culture of all threads in the application.
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Ireland Ireland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralGood article Pin
rammanusani26-Mar-19 8:08
rammanusani26-Mar-19 8:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.