Monday, January 12, 2009

Adventures while building a Silverlight Enterprise application part #4

Another week has gone. Here we are with part 4 of our adventure. Last time we dough 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 strings 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 strings
  • 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 treads 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 to add 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, trough a property that is overridden in the derived class. Same goes for the name of the resource manager.

The demo project can be downloaded below:


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.

10 comments:

  1. Thanks for sharing, I might try that. The thing is that I worked around my issues and now I can do the cool stuff, so I don't feel like going back to localization for the moment... But if I have to, I'll keep in mind your advice.

    ReplyDelete
  2. Jonathan, can you publish client code of solution (Page.xaml.cs, resx-files, ..) ?

    Thanks.

    ReplyDelete
  3. Hi, you can download the ComboLookup.zip. Inside is an example solution that contains the files you're looking for.

    Greets,
    Jonathan

    ReplyDelete
  4. Jonathan, i'm sorry, but "ComboLookup.zip/ComboLookup\ComboLookup\" contains only \Bin, App.xaml.cs and App.xaml :(

    I would like to see complete client project with Page.xaml.cs, resx-files..

    Thanks.

    ReplyDelete
  5. Ah, sorry about that. I've repackaged the project and uploaded it again. It should be complete now.
    Thanks for telling me.

    Greets,
    Jonathan

    ReplyDelete
  6. Thanks, Jonathan. I was stuck with the DataGrid issue the whole day until I found your valuable post. You save the day ;)

    Emil Stoychev
    SilverlightShow.net

    ReplyDelete
  7. when you got resources defined in your xaml, theyll wipe off your localizations. to prevent this, you have to assign them in the code behind after the LoadLocalStrings method call.

    ReplyDelete
  8. Hi Andre,
    You would be absolutely right. The call to InitializeComponents and, in effect, a call to Application.LoadComponent, would result in the resources to be cleared out, even before the rest of the XAML is parsed.

    I've been digging into the framework source, but couldn't find where they do this. But I did end up with native API calls, so maybe it's in mscorelib.

    Thanks for the heads up.

    Greets,
    Jonathan

    ReplyDelete
  9. Hi Jonathan,

    You're post really helped me out as I was having the same problem. Thanks so much! Now you mentioned in your post that:

    "If you need to load other resources besides the localized strings, you should do so from code, by adding them to the Resources instance."

    I actually have Value Converters that need to be used in the DataGrid and they are currently being loaded as a resource in the XAML file. How would I go about loading this resource in the code?

    Thanks.

    Dave.

    ReplyDelete
  10. Hi Dave,

    You can use the Resources.Add method for this. You simply pass in a key and and instance of your value converter. It would look something like this:

    Resources.Add("myValueConverter", new SomeValueConverter());

    HTH.
    Jonathan

    ReplyDelete