Localizing the Property Grid

The Property Grid is quite simply my all time favourite control - there's just so much there it would take a book to do justice to all the capabilities. One that isn't really a property grid facility, but is manifest within the grid is the use of localized data, such as the category and description that is shown for a given property. This article will describe how to roll your own localized UI.

How it's Done

The property grid reads the Description and Category attributes in order to determine what's shown on screen, so all we need to do is hijack these and provide our own classes which return localized text. The method is different for each class, so I'll describe each in turn. The basics are :-

  • Derive your own class from the xxxAttribute class
  • Override something to get your localization code called
  • Attribute code with your new attribute.

Localizing Descriptions

Here you need to create a subclass of System.ComponentModel.DescriptionAttribute, and then override the Description property...

using System;
using System.ComponentModel;

namespace LocalizedPropertyGrid
{
    /// <summary>
    /// Localized description attribute
    /// </summary>
    [AttributeUsage(AttributeTargets.All,AllowMultiple=false,Inherited=true)]
    public class SRDescriptionAttribute : DescriptionAttribute
    {
        /// <summary>
        /// Construct the description attribute
        /// </summary>
        /// <param name="text"></param>
        public SRDescriptionAttribute ( string text ) : base ( text )
        {
            _localized = false ;
        }

        /// <summary>
        /// Override the return of the description text to localize the text
        /// </summary>
        public override string Description
        {
            get
            {
                if ( !_localized )
                {
                    _localized = true ;
                    this.DescriptionValue = SR.GetString ( this.DescriptionValue ) ;
                }

                return base.Description ;
            }
        }

        /// <summary>
        /// Store a flag indicating whether this has been localized
        /// </summary>
        private bool    _localized ;
    }
}

This class can then be used in place of the regular Description attribute.

Note: This class caches the localized version of the attribute, so if you happen to permit the UI culture to change without re-running the application then you should remove this caching and always call SR.GetString() from within the Description property getter.

Localizing Categories

This one's a little simpler as there's a method on System.ComponentModel.CategoryAttribute called GetLocalizedString which is all you need to override...

using System;
using System.ComponentModel;

namespace LocalizedPropertyGrid
{
    /// <summary>
    /// Localized version of the Category attribute
    /// </summary>

    [AttributeUsage(AttributeTargets.All,AllowMultiple=false,Inherited=true)]
    public class SRCategoryAttribute : CategoryAttribute
    {
        /// <summary>
        /// Construct the attribute
        /// </summary>
        /// <param name="name"></param>

        public SRCategoryAttribute ( string name ) : base ( name ) { }

        /// <summary>
        /// Return the localized version of the passed string
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>

        protected override string GetLocalizedString ( string value )
        {
            return SR.GetString ( value ) ;
        }
    }
}

Note: This class too caches the localized string, and this is done by the base class which is a whole load more difficult to change on the fly. If you ask me this is a bug, but a pretty small one at that.

The SR class

You may have noticed in the above classes that I've used a class called 'SR' - this is a class which wraps access to resources for the current assembly, and provides static methods for retrieving resources. This wraps all the complexity (well, simplicity if you ask me!) of accessing resources bound within your assembly (and also in satellite assemblies), so all I need to do is request the appropriate attribute. This class is closely modelled on the way that .NET does it's resource handling...

using System;
using System.Globalization;
using System.Resources;

namespace LocalizedPropertyGrid
{
    /// <summary>
    /// Class which exposes string resources
    /// </summary>

    public sealed class SR
    {
        #region Instance methods and data

        /// <summary>
        /// Private constructor
        /// </summary>

        private SR ( )
        {
            _rm = new System.Resources.ResourceManager ( "LocalizedPropertyGrid.LPG" , this.GetType().Assembly ) ;
        }

        /// <summary>
        /// Return the resource manager for the assembly
        /// </summary>

        private ResourceManager Resources
        {
            get { return _rm ; }
        }

        /// <summary>
        /// Store the resource manager
        /// </summary>

        private ResourceManager    _rm ;

        #endregion

        #region Static methods and data

        /// <summary>
        /// Return the static loader instance
        /// </summary>
        /// <returns></returns>

        private static SR GetLoader ( )
        {
            if ( null == _loader )
            {
                lock ( _lock )
                {
                    if ( null == _loader )
                        _loader = new SR ( ) ;
                }
            }

            return _loader ;
        }

        /// <summary>
        /// Get a string resource
        /// </summary>
        /// <param name="name">The resource name</param>
        /// <returns>The localized resource</returns>

        public static string GetString ( string name )
        {
            SR    loader = GetLoader ( ) ;
            string localized = null ;
            
            if ( null != loader )
                localized = loader.Resources.GetString ( name , null ) ;

            return localized ;
        }

        /// <summary>
        /// Get the localized string for a particular culture
        /// </summary>
        /// <param name="culture">The culture for which the string is desired</param>
        /// <param name="name">The resource name</param>
        /// <returns>The localized resource</returns>

        public static string GetString ( CultureInfo culture , string name )
        {
            SR    loader = GetLoader ( ) ;
            string localized = null ;
            
            if ( null != loader )
                localized = loader.Resources.GetString ( name , culture ) ;

            return localized ;
        }

        /// <summary>
        /// Cache the one and only instance of the loader
        /// </summary>

        private static SR    _loader = null ;

        /// <summary>
        /// Object used to lock
        /// </summary>

        private static object _lock = new object ( ) ;

        #endregion
    }
}

This class exposes string resources from the main assembly. The constructor uses a resource name (here LocalizedPropertyGrid.LPG) which is the name I've given to the localized resources for my test application. I created a Windows Forms project called LocalizedPropertyGrid, and then added a .resx file called LPG, so this is where the name comes from. I then created a German version (LPG.de.resx) and added some 'German' resources into it - well, it's sort of German, apologies to all those real German speakers out there!. VS.NET will build appropriate localized assemblies based on the resource files you add, so that's pretty much all there is to it. Here's a couple of images which show how this looks on screen.

This first image shows the properties of an object in English - the Category and Description are in English

This second image shows the properties of an object in German - well, sort of German

Summary

In this article I've described how to localize the property grid control, with some examples of how best to accomplish this. As usual, .NET supports this facility, you just need to know how to do it!.

You can download the code (as a Visual Studio.NET v1.1 solution) here.

LocalizedPropertyGrid.zip