While developing an application for my company, I had to use a PropertyGrid (default WinForms-usercontrol).
The problem was the localization of this grid.
The client wanted to localize the categories, property-names, enum selection values and the descriptions.
So I did some research and developed a couple of handy classes that can do the job.
The first step was to create some resx resource files:
- EngineProperties.resx
Used for Description and Categories - EngineProperties.nl.resx
- EngineProperties.fr.resx
- PropertyNames.resx
Used for enum-value- and propertyname localizations. - PropertyNames.nl.resx
( I created them by clicking on the project and adding a new resource file )
The first class we need is a class to localize the descriptions visible in the PropertyGrid control.
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.ComponentModel;
5:
6: namespace Whizzo3D.Engine.Localization
7: {
8: /// <summary>
9: /// Specifies a description for a property or event.
10: /// Localized version of the description class.
11: /// Used to localize messages in a propertygrid for example.
12: /// </summary>
13: public class GlobalDescriptionAttribute : DescriptionAttribute
14: {
15: public GlobalDescriptionAttribute(string descriptionKey)
16: : base()
17: {
18: // Set the value to the localized description of our key.
19: base.DescriptionValue =
20: EngineProperties.ResourceManager.GetString(descriptionKey);
21: }
22: }
23: }
The second class we need is a class to localize the categories in the PropertyGrid control.
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.ComponentModel;
5:
6: namespace Whizzo3D.Engine.Localization
7: {
8: /// <summary>
9: /// Specifies the name of the category in which to group the property or event
10: /// when displayed in a System.Windows.Forms.PropertyGrid control set to Categorized
11: /// mode.
12: /// </summary>
13: public class GlobalCategoryAttribute : CategoryAttribute
14: {
15: public GlobalCategoryAttribute(string categoryKey)
16: : base(categoryKey)
17: {
18:
19: }
20:
21: /// <summary>
22: /// Fetch a localized string for the culture defined
23: /// in the Thread.CurrentThread.CurrentUICulture
24: /// </summary>
25: /// <param name="value">Key to fetch value for</param>
26: /// <returns>Requested value.</returns>
27: protected override string GetLocalizedString(string value)
28: {
29: return EngineProperties.ResourceManager.GetString(value);
30: }
31: }
32: }
Image a test-class InfoMessage for using the attributes above:
(This class is just a class from my project, look at the attributes located above the property declarations of Message and Title)
1: /// <summary>
2: /// Respresents an information-message about the domain,
3: /// that appears to the screen when the domain is loaded.
4: /// </summary>
5: [DataContract()]
6: [Serializable]
7: public class InfoMessage : Concurrency
8: {
9: #region Fields
10:
11: // ...
12:
13: #endregion
14:
15: #region Constructor(s)
16:
17: // ...
18:
19: #endregion
20:
21: #region Properties
22:
23: /// <summary>
24: /// Gets/sets the ID of the InfoMessage
25: /// </summary>
26: [Browsable(false)] // This property does not appear in the PropertyGrid Control.
27: [DataMember]
28: public Guid IDInfoMessage
29: {
30: get { return _iDInfoMessage; }
31: set { _iDInfoMessage = value; }
32: }
33:
34: /// <summary>
35: /// Gets/sets the message.
36: /// </summary>
37: [DataMember]
38: [GlobalDescription("InfoMessage_Message"), GlobalCategory("InfoMessage")]
39: public string Message
40: {
41: get { return _message; }
42: set { _message = value; }
43: }
44:
45: /// <summary>
46: /// Gets/sets the title.
47: /// </summary>
48: [DataMember]
49: [GlobalDescription("InfoMessage_Title"), GlobalCategory("InfoMessage")]
50: public string Title
51: {
52: get { return _title; }
53: set { _title = value; }
54: }
55:
56: #endregion
57: }
The third class/part is to create some classes that can localize the Property-names displayed in the PropertyGrid. This is a little bit nifty because the control uses reflection to get the property-names that it's going to display.
The client want to replace the property-name 'Message' with the localized dutch value 'Bericht:' for example.
The things we must do to get this job done:
- Create an attribute for defining which property to change
It's not needed when the values are formatted in a certain way in the resource file.
The format is: classname_propertyname - Create a custom PropertyDescriptor to fetch the localized description values.
- Create an object that implements ICustomTypeDescriptor so that the 'GetProperties' method is overridden. This method is called by the PropertyGrid when it requests the names of the properties that it wants do display.
This knowing, we can start programming the actual classes.
The Attribute (1):
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4:
5: namespace Whizzo3D.Engine.Localization
6: {
7: /// <summary>
8: /// Defines an attribute for giving a key to localize the propertyname itself.
9: /// </summary>
10: /// <remarks>
11: /// We don't need this attribute of the keys are in the form of
12: /// classname_propertyname in the resource file.
13: /// </remarks>
14: [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
15: public class GlobalPropertyAttribute : Attribute
16: {
17: private string _resourceKey = "";
18:
19: public GlobalPropertyAttribute()
20: {
21: _resourceKey = string.Empty;
22: }
23:
24: public GlobalPropertyAttribute(string nameKey)
25: {
26: _resourceKey = nameKey;
27: }
28:
29: public String NameKey
30: {
31: get { return _resourceKey; }
32: set { _resourceKey = value; }
33: }
34: }
35: }
The PropertyDescriptor (2):
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.ComponentModel;
5:
6: namespace Whizzo3D.Engine.Localization
7: {
8: /// <summary>
9: /// GlobalizedPropertyDescriptor enhances the base class obtaining the display name for a property
10: /// from the resource.
11: /// </summary>
12: public class GlobalPropertyDescriptor : PropertyDescriptor
13: {
14: private PropertyDescriptor basePropertyDescriptor;
15: private string localizedName = "";
16:
17: #region Constructor(s)
18:
19: public GlobalPropertyDescriptor(PropertyDescriptor basePropertyDescriptor)
20: : base(basePropertyDescriptor)
21: {
22: // Set the property-descriptor where we work on.
23: this.basePropertyDescriptor = basePropertyDescriptor;
24: }
25:
26: #endregion
27:
28: #region Abstract Dummy Methods
29:
30: public override bool CanResetValue(object component)
31: {
32: return basePropertyDescriptor.CanResetValue(component);
33: }
34:
35: public override Type ComponentType
36: {
37: get { return basePropertyDescriptor.ComponentType; }
38: }
39:
40: public override object GetValue(object component)
41: {
42: return this.basePropertyDescriptor.GetValue(component);
43: }
44:
45: public override bool IsReadOnly
46: {
47: get { return this.basePropertyDescriptor.IsReadOnly; }
48: }
49:
50: public override string Name
51: {
52: get { return this.basePropertyDescriptor.Name; }
53: }
54:
55: public override Type PropertyType
56: {
57: get { return this.basePropertyDescriptor.PropertyType; }
58: }
59:
60: public override void ResetValue(object component)
61: {
62: this.basePropertyDescriptor.ResetValue(component);
63: }
64:
65: public override bool ShouldSerializeValue(object component)
66: {
67: return this.basePropertyDescriptor.ShouldSerializeValue(component);
68: }
69:
70: public override void SetValue(object component, object value)
71: {
72: this.basePropertyDescriptor.SetValue(component, value);
73: }
74:
75: public override string Description
76: {
77: get { return basePropertyDescriptor.Description; }
78: }
79:
80: #endregion
81:
82: #region Abstract Overrides
83:
84: /// <summary>
85: /// Gets the displayname for a property
86: /// </summary>
87: public override string DisplayName
88: {
89: get
90: {
91: // Get the propertyName from the resources file.
92: // For this 3D Engine it is 'PropertyNames'
93:
94: // The displaynameKey for this property (localized)
95: string displayNameKey = string.Empty;
96: // Look for the defined attribute 'GlobalizedPropertyAttribute'
97: foreach (Attribute oAttrib in this.basePropertyDescriptor.Attributes)
98: {
99: if (oAttrib.GetType().Equals(typeof(GlobalPropertyAttribute)))
100: {
101: displayNameKey = (oAttrib as GlobalPropertyAttribute).NameKey;
102: break;
103: }
104: }
105:
106: if (string.IsNullOrEmpty(displayNameKey))
107: displayNameKey = basePropertyDescriptor.DisplayName;
108:
109: this.localizedName = PropertyNames.ResourceManager.GetString(displayNameKey);
110:
111: if (string.IsNullOrEmpty(this.localizedName))
112: this.localizedName = basePropertyDescriptor.DisplayName;
113:
114: return this.localizedName;
115: }
116: }
117:
118: #endregion
119: }
120: }
The base-class where we have to inherit from with the localizable objects (3):
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.ComponentModel;
5: using System.Resources;
6:
7: using Whizzo3D.Engine.Localization;
8:
9: namespace Whizzo3D.Engine.Objects
10: {
11: /// <summary>
12: /// This base-object implements ICustomTypeDescriptor.
13: /// The main-task of this object is to instatiate our own
14: /// specialized property descriptor.
15: /// </summary>
16: public abstract class GlobalizedObject : ICustomTypeDescriptor
17: {
18: private PropertyDescriptorCollection _globalizedProps;
19:
20: /// <summary>
21: /// Instantiate a new localized object.
22: /// </summary>
23: protected GlobalizedObject()
24: {
25: // Default constructor
26: }
27:
28: #region ICustomTypeDescriptor Members
29:
30: public string GetClassName()
31: {
32: string className = TypeDescriptor.GetClassName(this, true);
33: return className;
34: }
35:
36: public AttributeCollection GetAttributes()
37: {
38: return TypeDescriptor.GetAttributes(this, true);
39: }
40:
41: public String GetComponentName()
42: {
43: return TypeDescriptor.GetComponentName(this, true);
44: }
45:
46: public TypeConverter GetConverter()
47: {
48: return TypeDescriptor.GetConverter(this, true);
49: }
50:
51: public EventDescriptor GetDefaultEvent()
52: {
53: return TypeDescriptor.GetDefaultEvent(this, true);
54: }
55:
56: public PropertyDescriptor GetDefaultProperty()
57: {
58: return TypeDescriptor.GetDefaultProperty(this, true);
59: }
60:
61: public object GetEditor(Type editorBaseType)
62: {
63: return TypeDescriptor.GetEditor(this, editorBaseType, true);
64: }
65:
66: public EventDescriptorCollection GetEvents(Attribute[] attributes)
67: {
68: return TypeDescriptor.GetEvents(this, attributes, true);
69: }
70:
71: public EventDescriptorCollection GetEvents()
72: {
73: return TypeDescriptor.GetEvents(this, true);
74: }
75:
76: /// <summary>
77: /// Called to get the properties of a type.
78: /// </summary>
79: /// <param name="attributes"></param>
80: /// <returns></returns>
81: public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
82: {
83: if (_globalizedProps == null)
84: {
85: // Get the collection of properties
86: PropertyDescriptorCollection baseProps = TypeDescriptor.GetProperties(this, attributes, true);
87:
88: _globalizedProps = new PropertyDescriptorCollection(null);
89:
90: // For each property use a property descriptor of our own that is able to be globalized
91: for (int i = 0; i < baseProps.Count; i++)
92: _globalizedProps.Add(new GlobalPropertyDescriptor(baseProps[i]));
93: }
94: return _globalizedProps;
95: }
96:
97: public PropertyDescriptorCollection GetProperties()
98: {
99: // Only do once
100: if (_globalizedProps == null)
101: {
102: // Get the collection of properties
103: PropertyDescriptorCollection baseProps = TypeDescriptor.GetProperties(this, true);
104: _globalizedProps = new PropertyDescriptorCollection(null);
105:
106: // For each property use a property descriptor of our own that is able to be globalized
107: foreach (PropertyDescriptor oProp in baseProps)
108: {
109: _globalizedProps.Add(new GlobalPropertyDescriptor(oProp));
110: }
111: }
112: return _globalizedProps;
113: }
114:
115: public object GetPropertyOwner(PropertyDescriptor pd)
116: {
117: return this;
118: }
119:
120: #endregion
121: }
122:
123: }
Now, this in mind, look back at the InfoMessage test-class. It inherits from Concurrency (it's an object of me to keep track of concurrency), and the Concurrency object inherits from the GlobalizedObject.
As far as the implementions it's done. Now we have to adjust the resource file(s).
The PropertyDescriptor uses the PropertyNames resource file(s), to get the localized values for the keys.
The descriptor makes it's own keys, all you have to is fill them in in the resource file.
Something like this:
If you supply an InfoMessage object to the PropertyGrid, then it will be localized.
The only problem now is the localization of enum-values if you have enums in the class that you want to supply to the PropertyGrid.
For localizing the enum-values, we need a TypeConverter, more specific the EnumConverter from .NET
We have to inherit from it to create our own converter.
And we have to cache the relation between the localized values and the original enum-values.
The converter looks like this:
1: /// <summary>
2: /// This class is a converter for enums.
3: /// Purpose is to localize enum values.
4: /// </summary>
5: public class GlobalEnumConverter : EnumConverter
6: {
7: Dictionary<CultureInfo, Dictionary<string, object>> _lookupTables;
8:
9: /// <summary>
10: /// Instantiate a new Enum Converter
11: /// </summary>
12: /// <param name="type">Type of the enum to convert</param>
13: public GlobalEnumConverter(Type type)
14: : base(type)
15: {
16: _lookupTables = new Dictionary<CultureInfo, Dictionary<string, object>>();
17: }
18:
19: /// <summary>
20: /// The lookuptable holds the references between the original values and the localized values.
21: /// </summary>
22: /// <param name="culture">Culture for which the localization pairs must be fetched (or created)</param>
23: /// <returns>Dictionary</returns>
24: private Dictionary<string, object> GetLookupTable(CultureInfo culture)
25: {
26: Dictionary<string, object> result = null;
27: if (culture == null)
28: culture = CultureInfo.CurrentCulture;
29:
30: if (!_lookupTables.TryGetValue(culture, out result))
31: {
32: result = new Dictionary<string, object>();
33: foreach (object value in GetStandardValues())
34: {
35: string text = ConvertToString(null, culture, value);
36: if (text != null)
37: {
38: result.Add(text, value);
39: }
40: }
41: _lookupTables.Add(culture, result);
42: }
43: return result;
44: }
45:
46: /// <summary>
47: /// Convert the localized value to enum-value
48: /// </summary>
49: /// <param name="context"></param>
50: /// <param name="culture"></param>
51: /// <param name="value"></param>
52: /// <returns></returns>
53: public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
54: {
55: if (value is string)
56: {
57: Dictionary<string, object> lookupTable = GetLookupTable(culture);
58: //LookupTable lookupTable = GetLookupTable(culture);
59: object result = null;
60: if (!lookupTable.TryGetValue(value as string, out result))
61: {
62: result = base.ConvertFrom(context, culture, value);
63: }
64: return result;
65: //return base.ConvertFrom(context, culture, value);
66: }
67: else
68: {
69: return base.ConvertFrom(context, culture, value);
70: }
71: }
72:
73: /// <summary>
74: /// Convert the enum value to a localized value
75: /// </summary>
76: /// <param name="context"></param>
77: /// <param name="culture"></param>
78: /// <param name="value"></param>
79: /// <param name="destinationType"></param>
80: /// <returns></returns>
81: public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
82: {
83: if (value != null && destinationType == typeof(string))
84: {
85: Type type = value.GetType();
86: string resourceName = string.Format("{0}_{1}", type.Name, value.ToString());
87: string result = PropertyNames.ResourceManager.GetString(resourceName, culture);
88: if (result == null)
89: result = resourceName;
90: return result;
91: }
92: else
93: {
94: return base.ConvertTo(context, culture, value, destinationType);
95: }
96: }
97: }
If you want to use it, take your enum-declaration and put the TypeConverterAttribute above it:
1: // The EnumMember and DataContract attributes are stuff from .NET 3.0...
2: [Flags]
3: [DataContract()]
4: [Serializable]
5: [TypeConverter(typeof(GlobalEnumConverter))]
6: public enum StatusFlag : int
7: {
8: [EnumMember]
9: None = 0,
10: [EnumMember]
11: Draft = 1, // Kladwerk
12: [EnumMember]
13: Deleted = 2, // Verwijderd
14: [EnumMember]
15: ToValidate = 4, // Te valideren
16: [EnumMember]
17: Invalid = 8, // Ongeldig
18: [EnumMember]
19: Valid = 16, // Geldig
20: [EnumMember]
21: ToRevise = 32 // Te herbekijken
22: }
Look at the resource image for seeing the declaration of the keys needed to localize this enum-values.
Mind the [TypeConverter(typeof(GlobalEnumConverter))] attribute on top of the declaration, that line makes sure that the converter is used when the PropertyGrid wants to display the enum-values.
Just change your Culture of you current thread and see that the PropertyGrid will change it's values ;)
Hope this helps, if there are any questions, you know where to find me.
Regards,
Whizzo
3 comments:
Waarom maak je niet een GlobalDisplayNameAttribute afgeleid van DisplayNameAttribute ipv een TypeDescriptor gebruiken?
This Custom Property Grid Control is able to display the properties of any object in a user-friendly way and allows the end users of your applications edit the properties of the object.
If you're interested in software localization tools that can help you better manage the translation of an app's strings, I recommend you check out the collaborative localization platform https://poeditor.com/
Post a Comment