views:

3520

answers:

7

Hi,

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 ?

+1  A: 

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.

configurator
+2  A: 

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" } } } });
    }
}
Marc Gravell
A: 

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.

Vincent Van Den Berghe
First part of your solution is what I did... until i had to solve the "what is Some.ResourceManager ?" question. Am I supposed to give a *second* literal string such as "MyAssembly.Resources.Resource" ? way too dangerous!As for second part (ICustomTypeDescriptor) I don't think it's actually useful
PowerKiKi
Marc Gravell's solution is the way to go if you don't need anything else than a translated DisplayName -- I use the custom descriptor for other stuff too, and this was my solution. There is no way to do this without supplying some sort of key, though.
Vincent Van Den Berghe
+17  A: 

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";
}
Jeff Yates
When I try this approach, I get an error message saying "An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type". However, passing the value to LocalizedDisplayName as a string works. Wish it would be strongly typed.
Andy
@Andy: The values in ResourceStrings must be constants, as indicated in the answer, not properties or readonly values. They must be marked as const and refer to the names of the resources, otherwise you will get an error.
Jeff Yates
Answered my own question, was about where you had the Resources.ResourceManager, in my case the resx files are public resx generated so it was `[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");`
TreeUK
+10  A: 

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.

PowerKiKi
Very useful. Thanks. In the future, I hope Microsoft comes up with a nice solution that offers strongly typed way of referencing the resources.
JohnnyO
ya this string stuff sucks really hard :( If you could get the property name of the property that uses the attribute, you could do it in the convention over configuration way, but this seems not to be possible. Caring for the "strongly tpyed" Enumerations, you could use, is also not really maintainable :/
Rookian
That's a good solution. I just wouldn't iterate through the collection of `ResourceManager` properties. Instead you can simply get the property directly from the type provided in the parameter: `PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);`
brainnovative
+3  A: 

You could use T4 to generate constants. I wrote one:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.XPath" #>
using System;
using System.ComponentModel;


namespace Bear.Client
{
 /// <summary>
 /// Localized display name attribute
 /// </summary>
 public class LocalizedDisplayNameAttribute : DisplayNameAttribute
 {
  readonly string _resourceName;

  /// <summary>
  /// Initializes a new instance of the <see cref="LocalizedDisplayNameAttribute"/> class.
  /// </summary>
  /// <param name="resourceName">Name of the resource.</param>
  public LocalizedDisplayNameAttribute(string resourceName)
   : base()
  {
   _resourceName = resourceName;
  }

  /// <summary>
  /// Gets the display name for a property, event, or public void method that takes no arguments stored in this attribute.
  /// </summary>
  /// <value></value>
  /// <returns>
  /// The display name.
  /// </returns>
  public override String DisplayName
  {
   get
   {
    return Resources.ResourceManager.GetString(this._resourceName);
   }
  }
 }

 partial class Constants
 {
  public partial class Resources
  {
  <# 
   var reader = XmlReader.Create(Host.ResolvePath("resources.resx"));
   var document = new XPathDocument(reader);
   var navigator = document.CreateNavigator();
   var dataNav = navigator.Select("/root/data");
   foreach (XPathNavigator item in dataNav)
   {
    var name = item.GetAttribute("name", String.Empty);
  #>
   public const String <#= name#> = "<#= name#>";
  <# } #>
  }
 }
}
zielu1
A: 

I don't know if the PropertyGrid supports it, but there is the Display attribute from System.ComponentModel.DataAnnotations in .NET 4.

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

This would look up a resource named UserName in your MyResources .resx file.

RandomEngy