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)
{
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;
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