Localization of DisplayNameAttribute

Viewed 93195

I am looking for a way to localize properties names displayed in a PropertyGrid. The property's name may be "overriden" using the DisplayNameAttribute attribute. Unfortunately attributes can not have non constant expressions. So I can not use strongly typed resources such as:

class Foo
{
   [DisplayAttribute(Resources.MyPropertyNameLocalized)]  // do not compile
   string MyProperty {get; set;}
}

I had a look around and found some suggestion to inherit from DisplayNameAttribute to be able to use resource. I would end up up with code like:

class Foo
{
   [MyLocalizedDisplayAttribute("MyPropertyNameLocalized")] // not strongly typed
   string MyProperty {get; set;}
}

However I lose strongly typed resource benefits which is definitely not a good thing. Then I came across DisplayNameResourceAttribute which may be what I'm looking for. But it's supposed to be in Microsoft.VisualStudio.Modeling.Design namespace and I can't find what reference I am supposed to add for this namespace.

Anybody know if there's a easier way to achieve DisplayName localization in a good way ? or if there is as way to use what Microsoft seems to be using for Visual Studio ?

11 Answers

We are doing this for a number of attributes in order to support multiple language. We have taken a similar approach to Microsoft, where they override their base attributes and pass a resource name rather than the actual string. The resource name is then used to perform a lookup in the DLL resources for the actual string to return.

For example:

class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    private readonly string resourceName;
    public LocalizedDisplayNameAttribute(string resourceName)
        : base()
    {
      this.resourceName = resourceName;
    }

    public override string DisplayName
    {
        get
        {
            return Resources.ResourceManager.GetString(this.resourceName);
        }
    }
}

You can take this a step further when actually using the attribute, and specify your resource names as constants in a static class. That way, you get declarations like.

[LocalizedDisplayName(ResourceStrings.MyPropertyName)]
public string MyProperty
{
  get
  {
    ...
  }
}

Update
ResourceStrings would look something like (note, each string would refer to the name of a resource that specifies the actual string):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}

Here is the solution I ended up with in a separate assembly (called "Common" in my case):

   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event)]
   public class DisplayNameLocalizedAttribute : DisplayNameAttribute
   {
      public DisplayNameLocalizedAttribute(Type resourceManagerProvider, string resourceKey)
         : base(Utils.LookupResource(resourceManagerProvider, resourceKey))
      {
      }
   }

with the code to look up the resource:

  internal static string LookupResource(Type resourceManagerProvider, string resourceKey)
  {
     foreach (PropertyInfo staticProperty in  resourceManagerProvider.GetProperties(BindingFlags.Static | BindingFlags.NonPublic))
     {
        if (staticProperty.PropertyType == typeof(System.Resources.ResourceManager))
        {
           System.Resources.ResourceManager resourceManager = (System.Resources.ResourceManager)staticProperty.GetValue(null, null);
           return resourceManager.GetString(resourceKey);
        }
     }

     return resourceKey; // Fallback with the key name
  }

Typical usage would be:

class Foo
{
      [Common.DisplayNameLocalized(typeof(Resources.Resource), "CreationDateDisplayName"),
      Common.DescriptionLocalized(typeof(Resources.Resource), "CreationDateDescription")]
      public DateTime CreationDate
      {
         get;
         set;
      }
}

What is pretty much ugly as I use literal strings for resource key. Using a constant there would mean to modify Resources.Designer.cs which is probably not a good idea.

Conclusion: I am not happy with that, but I am even less happy about Microsoft who can't provide anything useful for such a common task.

Using the Display attribute (from System.ComponentModel.DataAnnotations) and the nameof() expression in C# 6, you'll get a localized and strongly typed solution.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }

You can subclass DisplayNameAttribute to provide i18n, by overriding one of the methods. Like so. edit: You might have to settle for using a constant for the key.

using System;
using System.ComponentModel;
using System.Windows.Forms;

class Foo {
    [MyDisplayName("bar")] // perhaps use a constant: SomeType.SomeResName
    public string Bar {get; set; }
}

public class MyDisplayNameAttribute : DisplayNameAttribute {
    public MyDisplayNameAttribute(string key) : base(Lookup(key)) {}

    static string Lookup(string key) {
        try {
            // get from your resx or whatever
            return "le bar";
        } catch {
            return key; // fallback
        }
    }
}

class Program {
    [STAThread]
    static void Main() {
        Application.Run(new Form { Controls = {
            new PropertyGrid { SelectedObject =
                new Foo { Bar = "abc" } } } });
    }
}

Well, the assembly is Microsoft.VisualStudio.Modeling.Sdk.dll. which comes with the Visual Studio SDK (With Visual Studio Integration Package).

But it would be used in pretty much the same way as your attribute; there is no way to use strongly types resources in attributes simply because they are not constant.

DisplayName:

    public sealed class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDisplayNameAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string DisplayName
    {
        get
        {
            var baseName = BaseName;
            var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

            if (baseName.IsNullOrEmpty())
            {
                // ReSharper disable once PossibleNullReferenceException
                baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
            }

            // ReSharper disable once AssignNullToNotNullAttribute
            var res = new ResourceManager(baseName, assembly);

            var str = res.GetString(ResourceKey);

            return string.IsNullOrEmpty(str)
                ? $"[[{ResourceKey}]]"
                : str;
        }
    }
}

Description:

public sealed class LocalizedDescriptionAttribute : DescriptionAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDescriptionAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string Description
    {
        get
        {
                var baseName = BaseName;
                var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

                if (baseName.IsNullOrEmpty())
                {
                    // ReSharper disable once PossibleNullReferenceException
                    baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
                }

                // ReSharper disable once AssignNullToNotNullAttribute
                var res = new ResourceManager(baseName, assembly);
                var str = res.GetString(ResourceKey);
                
                return string.IsNullOrEmpty(str)
                    ? $"[[{ResourceKey}]]"
                    : str;
        }
    }
}

Categories (PropertyGrid):

    public sealed class LocalizedCategoryAttribute : CategoryAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedCategoryAttribute(string resourceKey) 
        : base(resourceKey)
    {
        ResourceKey = resourceKey;
    }

    protected override string GetLocalizedString(string resourceKey)
    {
        var baseName = BaseName;
        var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

        if (baseName.IsNullOrEmpty())
        {
            // ReSharper disable once PossibleNullReferenceException
            baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
        }

        // ReSharper disable once AssignNullToNotNullAttribute
        var res = new ResourceManager(baseName, assembly);
        var str = res.GetString(resourceKey);

        return string.IsNullOrEmpty(str)
            ? $"[[{ResourceKey}]]"
            : str;
    }
}

Example: [LocalizedDisplayName("ResourceKey", ResourceType = typeof(RE))]

Where "RE" is living in the assembly holding your resource files like "Resources.de.resx" or "Resources.en.resx".

Works on enumerations and properties.

Cheers

I apologize for the VB.NET code, my C# is a bit rusty... But you'll get the idea, right?

First of all, create a new class: LocalizedPropertyDescriptor, which inherits PropertyDescriptor. Override the DisplayName property like this:

Public Overrides ReadOnly Property DisplayName() As String
         Get
            Dim BaseValue As String = MyBase.DisplayName
            Dim Translated As String = Some.ResourceManager.GetString(BaseValue)
            If String.IsNullOrEmpty(Translated) Then
               Return MyBase.DisplayName
            Else
               Return Translated
           End If
    End Get
End Property

Some.ResourceManager is the ResourceManager of the resource file that contains your translations.

Next, implement ICustomTypeDescriptor in the class with the localized properties, and override the GetProperties method:

Public Function GetProperties() As PropertyDescriptorCollection Implements System.ComponentModel.ICustomTypeDescriptor.GetProperties
    Dim baseProps As PropertyDescriptorCollection = TypeDescriptor.GetProperties(Me, True)
    Dim LocalizedProps As PropertyDescriptorCollection = New PropertyDescriptorCollection(Nothing)

    Dim oProp As PropertyDescriptor
    For Each oProp In baseProps
        LocalizedProps.Add(New LocalizedPropertyDescriptor(oProp))
    Next
    Return LocalizedProps
End Function

You can now use the 'DisplayName` attribute to store a reference to a value in a resource file...

<DisplayName("prop_description")> _
Public Property Description() As String

prop_description is the key in the resource file.

Related