This article provides a property grid with localized display names for properties by using .NET globalization and component model.


Introduction
The property grid is a nice control to display properties and values. You create an instance of your class and assign it to the property grid. By using reflection, a property grid extracts the properties of the class and displays its values.
Usually, you meet some more requirements:
- It would be nice if there is a user friendly name displayed which may differ from the property member names used for the class.
- Or the property name needs to be displayed in a different language.
- Or if international software is required at all, we need to display property names in more than one language. Maybe with switching between the languages at runtime.
So how to handle these requirements?
Fortunately, there is a excellent support for international software in .NET integrated. Even so, it is possible to customize the displaying of the property names and descriptions. Now let us see how to apply this stuff.
Globalization and Localization
First, let's have a short look at developing international software with .NET. It is a process that mainly takes two steps: Globalization and Localization.
Simply Defined
Globalization means the process of preparing your code to be able to support different languages. This is done by eliminating language or culture dependencies from your code to become culture-neutral. That is to avoid using hardcoded strings or message to be displayed to the user.
Localization means the process of separation of regional settings from the application code. Instead provide them separately as resources.
.NET has a bunch of classes integrated to support the development of international software. These classes are located in the namespaces System.Globalization
and System.Ressources
. CultureInfo
is the class that holds information about a certain language, as formatting of numbers and dates, calendar to use, decimal character... . the current language is set by assigning an instance of CultureInfo
to the property CurrentUICulture
of the Thread
instance representing the current thread:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
The example sets German as the current language. The languages identifiers are standard by ISO 639-1. The application resources are requested by using an instance of ResourceManager
. The resource manager uses the currently set CultureInfo
object to access the correct local resources.
ResourceManager rm = new ResourceManager("MyStringTable",this.GetType().Assembly);
string message = rm.GetString ("MyMessage");
The example accesses the string
named 'MyMessage
' from the stringtable
named 'MyStringTable
'.
We will use this support later on when we are localizing the property names to be displayed by the property grid. But first, let us define a sample project for demonstration purposes.
Creating a Sample Project
For demonstration purposes, select a Windows application as a new project type. Use the main form of type Form1
as a host for a property grid control. Select a property grid control from toolbox and drag it to the form.
Additionally, define a test class that provides some properties to be displayed in the property grid. Select "Add class..." and add a C# class named Person.cs to the project.
The test class models a person and should look like this:
public class Person : GlobalizedObject
{
private string firstName = "";
private string lastName = "";
private int age = 0;
public Person() {}
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
public int Age
{
get { return age; }
set { age = value; }
}
}
Now we are prepared for an initial version.
Initial Version
An instance of Person
is created and assigned to the property grid in the Form1_Load
event handler.
private void Form1_Load(object sender, System.EventArgs e)
{
person = new Person();
person.FirstName = "Max";
person.LastName = "Headroom";
person.Age = 42;
PropertyGrid1.SelectedObject = person;
}
After compiling, the initial version displays the public
properties of the person class in the property grid with property name and value. The displayed property name matches exactly the name of the property name of the class. The property 'LastName
' is displayed as 'LastName
'.
That is fine for startup. Now let us customize the displaying of property names.
Localizing Property Names
To customize how properties are displayed, Person
has to implement an interface called ICustomTypeDescriptor
. ICustomTypeDescriptor
allows an object to provide dynamic type information about itself. This interface is used to request a collection of property descriptor objects. One for each property. This matches our point of interest.
A property descriptor object is of type PropertyDescriptor
by default and provides information about a certain property, for example which name or desription text to display, what we are interested in. ICustomTypeDescriptor
and PropertyDescriptor
are located in the namespace System.ComponentModel
.
By default, there is the property name of the class returned as display name and an empty string
as description.
We override this behavior by providing our own property descriptor. Let us start with the implementation of ICustomTypeDescriptor
. Because it may be common code for all customized business classes, so it is placed into a base class from which Person
can derive.
The base class is called GlobalizedObject
and can be found in Descriptors.cs.
public class GlobalizedObject : ICustomTypeDescriptor
{
...
Our implementation overrides GetProperties()
only and creates a collection of custom property descriptors of type GlobalizedPropertyDescriptor
and returns them to the caller instead of the default ones.
public PropertyDescriptorCollection GetProperties()
{
if ( globalizedProps == null)
{
PropertyDescriptorCollection baseProps =
TypeDescriptor.GetProperties(this, true);
globalizedProps = new PropertyDescriptorCollection(null);
foreach( PropertyDescriptor oProp in baseProps )
{
globalizedProps.Add(new GlobalizedPropertyDescriptor(oProp));
}
}
return globalizedProps;
}
The rest of the methods are delegating the call to the .NET class TypeDescriptor
providing static
methods for default type information.
The custom property descriptor class GlobalizedPropertyDescriptor
derives from PropertyDescriptor
. The default property descriptor is passed as an argument in the constructor. We use this instance to provide default behaviour for all methods we don't override. The class can be found in Descriptor.cs, too.
public class GlobalizedPropertyDescriptor : PropertyDescriptor
{
private PropertyDescriptor basePropertyDescriptor;
private String localizedName = "";
private String localizedDescription = "";
public GlobalizedPropertyDescriptor(PropertyDescriptor basePropertyDescriptor) :
base(basePropertyDescriptor)
{
this.basePropertyDescriptor = basePropertyDescriptor;
}
...
The focus of interest are the properties DisplayName
and Description
. DisplayName
will be overridden to return a string
obtained from resources.
public override string DisplayName
{
get
{
string tableName = basePropertyDescriptor.ComponentType.Namespace + "." +
basePropertyDescriptor.ComponentType.Name;
string displayName = this.basePropertyDescriptor.DisplayName;
ResourceManager rm = new ResourceManager(
tableName,basePropertyDescriptor.ComponentType.Assembly);
string s = rm.GetString(displayName);
this.localizedName = (s!=null)? s : this.basePropertyDescriptor.DisplayName;
return this.localizedName;
}
}
The implementation of DisplayName
uses the class name as a resource string
table name and the property name as the string
identifier by default.
The implementation of property Description
is nearly the same except that the resource string
id is built by property name appended by 'Description
'.
The next step is to derive Person
from GlobalizedObject
.
public class Person : GlobalizedObject
Okay, having done this, our code is prepared to be globalized. Now let us do localization. We define resources for supported languages and make them selectable.
Defining Resources
Our sample will support two languages, English and German. For each language and class we add an assembly resource file to our sample project. Due to the fact that we use the class name as resource table name, we have to name the resource file same as the class: Person.de.resx and Person.en.resx. Additional fact for this naming is Visual Studio will recognize these resource files and generate the appropriate resource DLLs (.NET calls them satellite assemblies. They contain no application code, only resource definitions).
Note: It is good to have a default language integrated into your application. In our case, I have used German, so I name the German resource file Person.resx instead of Person.de.resx. In this case, German language will be integrated into the application and no satellite assembly will be generated then. The default language resource is also used if a resource in the current language cannot be found, this is part of a process to find resources that MS calls 'fallback process'. The resource tables may look like this:

There are two entries for each property, name and description.
Now we have different sets of resources and globalized code that is able to extract strings to be displayed depending on the current language. Our code should be updated to allow switching between supported languages.
Switching of Current Language
The only thing left is to make the two languages selectable. First, we construct an array of supported languages in the constructor of the main form. We use the ISO 639-1 standard format for identifying languages as used by .NET: en
for English, and de
for German should be enough for this sample. Also, an instance of Person
is created here.
public Form1()
{
InitializeComponent();
supportedLanguages = new string[2];
supportedLanguages[0] = "en";
supportedLanguages[1] = "de";
person = new Person();
person.FirstName = "Max";
person.LastName = "Headroom";
person.Age = 42;
}
Second, we add a combobox
named cbLang
to the main form. This combobox
is filled with the displayable language names. The displayable language names are obtained by using a CultureInfo
object. Moreover, the Person
object is assigned to the property grid. We use the form load event handler for this:
private void Form1_Load(object sender, System.EventArgs e)
{
cbLang.Items.Insert(0,
(new CultureInfo(supportedLanguages[0])).DisplayName);
cbLang.Items.Insert(1,
(new CultureInfo(supportedLanguages[1])).DisplayName);
cbLang.SelectedIndex = 0;
PropertyGrid1.SelectedObject = person;
}
Last but not least, we define an event handler to be notified when the selected index changes in the combo box because we want to change the current language. We inform the current thread about the new current language by setting its static
property CurrentUIThread
to a new instance of CultureInfo
initialized with the ISO 639-1 name. After setting the new language, a refresh of the property grid is necessary.
private void cbLang_SelectedIndexChanged(object sender, System.EventArgs e)
{
int lang = cbLang.SelectedIndex;
if( lang == -1 )
return;
Thread.CurrentThread.CurrentUICulture = new CultureInfo(supportedLanguages[lang]);
PropertyGrid1.Refresh();
}
That is a basic version that demonstrates how to display custom property names and descriptions. In the sample code, there is one enhancement provided.
Enhancement
The default selection of the resource string is by the class name as string
table name and the property name as the string
definition. This can be superposed by using a .NET attribute.
The attribute GlobalizedPropertyAttribute
is defined in Attributes.cs and can be applied as follows:
[GlobalizedProperty("Surname",Description="ADescription",
Table="GlobalizedPropertyGrid.MyStringTable")]
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
The example defines the display name for the property name LastName
can be found in stringtable
GlobalizedPropertyGrid.MyStringTable
and the string
is identified by Surname
, the optional description text is identified by ADescription
.
To get this enhancement working, an addition has to be made to the DisplayName
and Description
properties of the descriptor class GlobalizedPropertyDescriptor
:
public override string DisplayName
{
get
{
string tableName = "";
string displayName = "";
foreach( Attribute oAttrib in this.basePropertyDescriptor.Attributes )
{
if( oAttrib.GetType().Equals(typeof(GlobalizedPropertyAttribute)) )
{
displayName = ((GlobalizedPropertyAttribute)oAttrib).Name;
tableName = ((GlobalizedPropertyAttribute)oAttrib).Table;
}
}
if( tableName.Length == 0 )
tableName = basePropertyDescriptor.ComponentType.Namespace + "." +
basePropertyDescriptor.ComponentType.Name;
if( displayName.Length == 0 )
displayName = this.basePropertyDescriptor.DisplayName;
ResourceManager rm = new ResourceManager(
tableName,basePropertyDescriptor.ComponentType.Assembly);
string s = rm.GetString(displayName);
this.localizedName = (s!=null)? s : this.basePropertyDescriptor.DisplayName;
return this.localizedName;
}
}
This enhancement is commented out in the sample project. Remove the comment and recompile to see it working.
Summary
These are the steps to localize the names and descriptions displayed in the property grid:
- Implement the
ICustomTypeDescriptor
interface for your class to customize the display names and descriptions for properties. - Override the
GetProperties()
method to return a collection of customized property descriptor classes. - Override
DisplayName
and Description
properties in the customized property descriptor class. - The overridden versions should use a resource manager object to retrieve the display
string
s for the current language from the resource modules.
References
Contribution
This code is inspired by a fellow who showed how to provide user friendly names using VB code. I have adopted his work, using C# instead and prepared the code to be international and meet some recurrent real world requirements.
History
- 17th April, 2002: Initial version
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.