Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Globalization in WPF or Silverlight

0.00/5 (No votes)
14 Mar 2010 1  
Dynamic globalization for WPF and Silverlight apps without breaking design time

Introduction

In this C# demo, I'll show how to add globalization with some sense of error detection to a WPF or Silverlight application. This will enable the developer to create an application where the user can choose in which language to display information as well as other cultural settings rather than having them be based on their Windows settings. For example, on a public or group use computer, the application can allow the user to choose their language and render the application in that language. You might see this in a program providing for employment applications and testing.

The other goal is to make the strings visible at design time in Visual Studio or from Expression Blend. (Note the Common.dll or CommonSL.dll needs to be available for Expression Blend to be able to see the strings.) This allows your artist to come in after the developers are done and review the application as the user would see it.

The Code

We'll create a class called PublicResource which implements both IValueConverter and INotifyPropertyChanged. This class is used to make public the bits of a resx file that aren't normally available. It will also hold a Converter for use in XAML.

To start with, create a child private class that inherits from ResourceManager. Some of this will look very familiar from the regular designer.cs for your resx file. Note how we're adding a way to display a default message if the string isn't found in the two GetString() methods.

public class PublicResource : IValueConverter, INotifyPropertyChanged
{
  private class ResourceManagerWithErrors 
        : global::System.Resources.ResourceManager
  {
    public ResourceManagerWithErrors(Type resourceSource) 
        : base(resourceSource) { }
    public ResourceManagerWithErrors(string baseName, Assembly assembly) 
        : base(baseName, assembly) { }
    public ResourceManagerWithErrors(string baseName, Assembly assembly,
        Type usingResourceSet)
        : base(baseName, assembly, usingResourceSet) { }

    public string NotFoundMessage = "#Missing#";

    public override string GetString(string name)
    {
      return base.GetString(name) ?? NotFoundMessage + name;
    }

    public override string GetString(string name, CultureInfo culture)
    {
      return base.GetString(name, culture) ?? NotFoundMessage + name;
    }
  }

  private static ResourceManagerWithErrors _stringResourceManager;
  private static ResourceManagerWithErrors StringResourceManager
  {
    get
    {
      if (object.ReferenceEquals(_stringResourceManager, null))
      {
        _stringResourceManager = 
            new ResourceManagerWithErrors("WPFApplication1.Strings",
            typeof(PublicResource).Assembly);
        _stringResourceManager.NotFoundMessage = "#StringResourceMissing#";
      }
      return _stringResourceManager;
    }
  }

Be sure to replace WPFApplication1.Strings with the namespace and name of your resx file (without the .resx).

Next we implement IValueConverter so we can use the class in XAML. By moving the logic of finding the string to the Converter, we can dynamically build the globalized text.

There’s a bit of error detection making sure that value is set. We'll get to that in a bit. Note that we lookup the string using the CurrentUICulture and not the culture that is passed in. The culture passed in is the global culture that doesn't change.

public object Convert(object value, Type targetType,
    object parameter, CultureInfo culture)
{
  if ((value == null) || !(value is string))
    return "set Binding Path/Source!";

  return StringResourceManager.GetString(
          (string)parameter,
          System.Threading.Thread.CurrentThread.CurrentUICulture);
}

public object ConvertBack(object value, Type targetType,
    object parameter, CultureInfo culture)
{
  throw new NotImplementedException("No reason to do this.");
}

Next we need a default string. We'll bind to this and override it with the Converter. If the path fails to be resolved, the converter never runs. We make it a constant so the converter always runs.

public static string AString { get { return "AString"; } }

Next we need some way to tell the application to change the culture. We do this by notifying that the one single property that everyone is going to point to has changed. If you are using generated code for your resx file, set the culture for it as well.

public event PropertyChangedEventHandler PropertyChanged;

public void ChangeCulture(string culture)
{
  // set App culture and the resource file culture
  System.Threading.Thread.CurrentThread.CurrentUICulture =
      new System.Globalization.CultureInfo(culture);
  System.Threading.Thread.CurrentThread.CurrentCulture =
          System.Threading.Thread.CurrentThread.CurrentUICulture;
  Strings.Culture = 
          System.Threading.Thread.CurrentThread.CurrentUICulture;

  // notify that the culture has changed
  PropertyChangedEventHandler handler = PropertyChanged;

  if (handler != null)
    handler(this, new PropertyChangedEventArgs("AString"));
}

Finally, since PublicResource is acting like a static class, but we can't call NotifyCultureChanged without an instance, we'll save a reference to itself in its own constructor.

public static PublicResource Myself;

public PublicResource() 
{ 
  Myself = this; 
}

That rounds out the PublicResource class. Let’s see how it is used. In the XAML, we add a TextBlock and call the converter.

xmlns:res="clr-namespace:WPFApplication1;assembly=Common"

<Window.Resources>
  <res:PublicResource x:Key="Resource"/>
</Window.Resources>

<TextBlock Text="{Binding Path=AString, 
    Source={StaticResource Resource}, Mode=OneWay,
    Converter={StaticResource Resource}, ConverterParameter=HelloWorld}"/>

Be sure to change out WPFApplication1 and Common with the name and assembly name for where to find the PublicResource class.

So, what happens here? The Binding goes and looks for a property in PublicResource named AString. This gets returned as "AString". Once this is done, the converter gets executed. This goes and looks up the parameter HelloWorld in the appropriate resource file and returns whatever it is mapped to based on the current culture. If the string isn't found in the resx file, it will return #StringResourceMissing#HelloWorld. This is pretty easy to spot on the interface rather than a completely missing string.

To dynamically change the culture, we call:

PublicResource.Myself.ChangeCulture("es-ES");

We need to set the full culture name, not just "es".

For Silverlight applications, you also need to edit the .csproj file for the client application. Modify the SupportedCultures tag with a comma delimited list of the resx files. There’s nothing to include for the default one. These codes have to match the codes on the resource files. If you want es-MX and es-ES to be different, then you'll need to support both in both places.

<SupportedCultures>en,de,es</SupportedCultures>

I hope this has helped you out with your Internationalization, Globalization and Localization efforts.

History

  • 14th March, 2010: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here