I have a little validation library that must support localized error messages, it uses a simple techniques, when you set an error message for a specific validator, if you do not want localization you can simply pass a single string, but if you want message to be localized, you specified two strings, one is the name of the resource that contains the message and the other is the key of the message.
All this logic is inserted into a class called ErrorMessage that was created at the time of.NET 2.0; here is a typical test
1
2
3
4
5
6
7
| [Test]
public void TestLocalizationIt()
{
ErrorMessage sut =
new ErrorMessage("Test", "DotNetMarche.Validator.Tests.ResourcesFiles.TestRes, DotNetMarche.Validator.Tests");
Assert.That(sut.ToString(System.Globalization.CultureInfo.CreateSpecificCulture("It-It")), Is.EqualTo("Questa è una stringa di test"));
}
|
This is far from being usable, because the needs to specify the full name of the resource file that contains resources is not usable. The user can mistype the Resource File name and introduce errors that will be discovered only at runtime, and only when an object does not pass validation. At that time it was acceptable, we included all error messages into a resource file, then Store the name of the resource file into a constant and all went good.
Now with Expression tree we can obtain more. First of all notice that when you include a.resx file into a solution visual studio creates for you a code behind file to easy the usage of resources.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class TestRes {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal TestRes() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetMarche.Validator.Tests.ResourcesFiles.TestRes", typeof(TestRes).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
|
You can see that this object has an internal property called ResouceManager that is static and contains the resource manager used to access resource file. Now the goal is to make this syntax possible.
1
2
3
4
5
6
7
| [Test]
public void TestLocalizationItFullFluentExplicitCulture()
{
ErrorMessage sut = new ErrorMessage(() => TestRes.Test);
Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture("En-Us");
Assert.That(sut.ToString(System.Globalization.CultureInfo.CreateSpecificCulture("It")), Is.EqualTo("Questa è una stringa di test"));
}
|
This test create the ErrorMessage object with a lambda, now you can have full intellisense and you cannot mistype anything, in the test I set the currentUICulture to En-us and call ToString() of the ErrorMessage asking for Italian culture, to verify that I can really specify culture different from the CurrentUICulture.
The constructor that permits me to obtain this is the following.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| /// <summary>
/// Basic Constructor
/// </summary>
public ErrorMessage(Expression<Func<String>> messageLambda)
{
MemberExpression me = messageLambda.Body as MemberExpression;
Type t = me.Member.DeclaringType;
//now I know that this type is one generated by the visual studio
PropertyInfo pinfo = t.GetProperty("ResourceManager", BindingFlags.Static | BindingFlags.NonPublic);
mMessage = me.Member.Name;
mResourceTypeName = t.FullName;
lock (mResMangaers)
{
if (!mResMangaers.ContainsKey(t.FullName))
{
mResMangaers.Add(t.FullName, pinfo.GetValue(null, null));
}
}
}
|
My object was designed to store all resource managers into a static hashtable, to minimize memory usage, so I simply declare this constructor that accepts an Expression<Func<String>>, then I know that I have a MemberExpression , because the user specify one of the autogenerated properties of the.resx file, then with a little bit of reflection I grab the ResourceManager and the game is done.
With Expression tree you can quite always avoid the need for the user of you library to specify object by name if you need to use reflection.
alk.