Another week has gone. Here we are with part 4 of our adventure. Last time, we dove into the ins and outs of using multiple XAP files. Thanks again to dsoltesz for some constructive comments on that article.
This time, we want to look into something I've been struggling with for about a day, last week. It's localizing our application. As you may remember from the first post, one of the requirements is that the application has to be available in both English and Dutch. "Well, that is easy. Just go to silverlight.net and you'll find a very nice instructional video on this", I hear you say. That was our initial thought as well and it proved to be true...
...until, that is, we tried to apply this to a datagrid
. I'll describe what happened by using the car application from some of my other articles.
The Default Implementation
As documented by many, we did the following to localize our application:
- Add a folder named "Resources" to our projects
- Add a file named "Strings.resx" and add any
string
s for the neutral culture (in our case, nl-NL
for dutch in the Netherlands)
- Add a file for each culture you want to support (in our case, only "Strings.en.resx") with all the same
string
s
- Add a local resource to each
UserControl
for the generated class:
<UserControl.Resources>
<cardemo:Engines x:Key="Engines" />
<local:Strings x:Key="LocalStrings" />
</UserControl.Resources>
- Bind each
string
to display to an entry in the resource:
<data:DataGridTextColumn Binding="{Binding Brand}"
Header="{Binding brandLabel, Source={StaticResource LocalStrings}}">
</data:DataGridTextColumn>
And Bob's your uncle. For any control, this works great, but not for the datagrid
. Run the code and you'll get the following exception:
System.Windows.Markup.XamlParseException occurred Message=
"AG_E_PARSER_BAD_TYPE [Line: 11 Position: 46]" LineNumber=11
LinePosition=46 StackTrace: at System.Windows.Application.LoadComponent(
Object component, Uri resourceLocator)
at ComboLookup.Page.InitializeComponent() at ComboLookup.Page..ctor()
First thing I did was Google around to find out if someone already solved this problem. I found out this problem was introduced in the RTW release and I found some forum threads on silverlight.net, discussing this problem. I even found some solutions, but I was not very happy with those. One solution ended up using some reflection code inside a LINQ statement to add all the string entries to the local Resources instance (which could end up being a performance issue in applications with larger resource files). Another solution involved building a custom mechanism for filling up the headers of the datagrid
, which seems like a maintenance nightmare to me.
So I figured I would have to come up with some better solution. First, I started digging to find out what was causing the problem. I used Reflector to dig into the code that is actually executed and found out that the problem is actually deep down in the core of the Silverlight .NET Framework, where the XAML is actually parsed, and you can't get to that code. This struck me as odd, as you would expect the XAML parser to be completely generic, but still there is this specific case for a specific control that goes wrong.
Based on this fact, I concluded that a fix would not be lying in the controls code, and in fact it would not be possible for me to fix the underlying problem. This meant I had to find a decent workaround for this problem, that would fit our application.
So it was back to Google again and time to study the solutions of others, to get some inspiration for a good fix. In the end, I wouldn't mind adding the strings to the local Resources instance, as this would result in nice XAML and would work the most intuitive. The problem was that I didn't like the way people were getting to the strings in the resource file. There had to be a better way, then to actually use reflection to get each property we need.
I played around with the ResourceManager
class and read some documentation, with the following code as a result:
private void LoadLocalStrings()
{
ResourceManager manager = new ResourceManager("ComboLookup.Resources.Strings",
Assembly.GetExecutingAssembly());
ResourceSet resourceSet = manager.GetResourceSet(CultureInfo.CurrentUICulture, true, true);
IDictionaryEnumerator resourceEnum = resourceSet.GetEnumerator();
while (resourceEnum.MoveNext())
{
Resources.Add(resourceEnum.Key.ToString(), resourceEnum.Value);
}
}
This method is called in the constructor BEFORE InitializeComponents()
.
What happens is that a resource manager with the specified base name ("ComboLookup.Resources.Strings
") is looked for in the assembly you specify. The base name has to be the fully qualified name for the instance of the resource class.
It should be noted that if you have resources defined in your XAML, they will override the earlier loaded resources. If you need to load other resources besides the localized strings, you should do so from code, by adding them to the Resources instance.
The XAML for using localized strings looks like this:
<data:DataGridTextColumn Binding="{Binding Brand}" Header="{StaticResource brandLabel}">
</data:DataGridTextColumn>
Note that you can now directly access a resource with the required string. To prevent us from implementing this in every single module we have to build, we defined a base class that solves this for us. All that has changed in this method is that we specify a different assembly, through a property that is overridden in the derived class. Same goes for the name of the resource manager.
I hope this saves you all a lot of troubles. If you have any questions or comments, please leave them below. I always enjoy reading and answering them.