When starting the current version I decided to move the 5 fields we had localized in our web.config to a Resource file (I mostly used Tim Heuer blog post “Silverlight and localizing string data”). I thought I was doing the right thing but I didn’t consider our application and the limitation of Resx resources in Silverlight (I thought it might be used like ResourceDictionary by zipping/unzipping the XAP file). When the problem was discover it was already to late from 5 fields we have grown to use ~40 fields and had plans to expand on that.
So what is the problem? In our application we have a Silverlight web server per country, our deployment process is also a bit different, we always deploy to Israel first (give the application a month or two for the Israeli crowd to find all the bugs) and then deploy it in other countries. Even more difficult is the fact that countries are added on the fly and without any compilation to our application (and they always have little changes in the strings localization department). Whereas changes to Resx files in Silverlight cannot be done without compilation. Because of all of that using the String Resources of Silverlight is not a good solution for our string localization deal.
Problems with Silverlight current Resource Strings:
- Add the cultures supported to the project file (changes here need compilation)
- Add resx per culture (changes here need compilation)
- Change the internal constructor to public (that is just an annoying problem)
- Have to be compiled
- A different version for each deployed application (even if it is just the strings)
What do I need:
- Only one culture
- Silverlight Static typed – I want to know if I have a mistake and have auto complete (in Resharper 6 EAP resources that do not exist are marked red and auto complete)
- Optional: Not writing the properties of the strings by hand
- Changed without compiling
- Better: can be changed without rebuilding the XAP
- Better: resources can be saved in source control (if we have them in development time)
Options I thought of for Server Side:
- Use the DB and get the resources at startup – I don’t really like this option, something about saving the resources in the DB just feels wrong
- Use the web.config – this is how we done it until now. But at this version we started adding a lot of resources using resource files and adding all that “garbage” to the web.config feels wrong.
- I entertained a thought about using a service to return a ResourceManager back from the server, but anything with “ComVisible(true)” attribute gets my red flag (if you get a chance open ResourceManager using dotPeek or any other decompiler the first line was just too funny:
- [ComVisible(true)]
- public class ResourceManager
- {
- public static readonly int MagicNumber = -1091581234;
(not sure where it is used though…))
- Service that returns a Dictionary of string,string – optional
- File (xml or csv) in the Web project passed to the Silverlight Application on startup – optional
- Chosen Solution: File (Resx) in the Web project with Custom Tool to generate the code – since resx is actually an XML it’s actually the last option disguised. The server sends the text in the resx file to the Silverlight client.
Client Side:
The client receives the text and reads it by parsing the resources xml and then puts it in the generated designer.cs file that we linked from the web project.
That was all good on paper but in actuality the setting of ResourceManager in the resources.designer.cs file is written without any way of changing this property (it’s not virtual, the property only has a get and it’s stored in a private member).
I had a thought about creating a tool of my own but in the end common sense won, and I found this post about using t4 templates to generate the resources code so I tweaked the original T4 a bit (too much)… You can find my T4 file here.
So lets look at some code!
Server side I have added the resources XML to the parameters for the config (see post), like this:
- private void SaveSilverlightResourcesSettings(StringBuilder parameters)
- {
- var culture = ConfigurationManager.AppSettings["Culture"] ?? Thread.CurrentThread.CurrentCulture.Name;
- var filename = Server.MapPath(String.Format("Resources\\StringsResource.{0}.resx", culture));
- if (!File.Exists(filename))
- filename = Server.MapPath("Resources\\StringsResource.resx");
- TextReader tr = new StreamReader(filename);
- parameters.Append(StringsResourceName.Name);
- parameters.Append("=");
- parameters.Append(HttpUtility.UrlEncode(tr.ReadToEnd()));
- parameters.Append(",");
- tr.Close();
- }
line 3: the current culture is taken from either the web.config or the server value (allowing multiple culture web sites in the same server)
Line 4: takes the file by the culture set in the server (that allows us to save the resources string in the Source control if we have them at development time). If it doesn’t exist we take the default file.
Server side resources look like this:
The T4resx.Designer.cs is linked to the client Silverlight application:
In the Silverlight App.Xaml we load the text of the string resources:
- private void Application_Startup(object sender, StartupEventArgs e)
- {
- MapApplicationConfigWrapper.Instance.Initialize(e);
- if (e.InitParams.ContainsKey(StringsResourceName.Name))
- {
- var resourceString = HttpUtility.UrlDecode(e.InitParams[StringsResourceName.Name]);
- if (resourceString != null)
- StringsResource.SetResourceDictionary(XDocument.Parse(resourceString).Element("root"));
- }
- RootVisual = new MainPage();
- }
StringsResource is the class in StringsReource.Designer.cs, it is based on a regular Resource designer but has some changes:
- public class StringsResourceName { public const string Name = "StringsResource"; }
- private static bool _useResourceManager = true;
- private static Dictionary<string, string> _resourcesDictionary;
- public static void SetResourceDictionary(XElement xml)
- {
- _useResourceManager = false;
- _resourcesDictionary = new Dictionary<string, string>();
- foreach (var data in xml.Descendants("data"))
- {
- _resourcesDictionary.Add(data.Attribute("name").Value, data.Element("value").Value);
- }
- }
- private static string GetResourceString(string key, params string[] tokens)
- {
- if(!_useResourceManager)
- {
- if (_resourcesDictionary.ContainsKey(key))
- return _resourcesDictionary[key];
- return string.Empty;
- }
Using this was as easy as changing the namesapce for the resource in the xaml file:
- xmlns:res="clr-namespace:DllShepherd.SilverlightGoogleMaps.Web.Resources"
- <UserControl.Resources>
- <res:StringsResource x:Key="StringsRes" />
- </UserControl.Resources>
- <Controls:MenuItem Header="{Binding Path=StreetView, Source={StaticResource StringsRes}}"
You can see a sample project using this at my CodePlex GoogleStreets project that changes the header of the context menu.
The only downside I found for this method is in changing the Resources file. The T4 template then needs to be included in the build process for this (you need to install Visual Studio Visualization and Modeling SDK (which uses VS2010 SDK)) and edit the web project file (see here). Though with this change the template causes problems with MsBuild because of how it is written (the baseT4MVC uses the VS IDE). I guess a better solution will be to rewrite the T4resx so that it will track changes in StringsResource.resx (possible using Directive Processor) but I don’t have time to implement it just yet.
Note: another solution is running the tool manually by right clicking on the T4resx.Designer.tt and:
I think both solutions are still more elegant than changing the constructor to public.
Keywords: Resource, Silverlight