Localisation de DisplayNameAttribute

120

Je recherche un moyen de localiser les noms de propriétés affichés dans un PropertyGrid. Le nom de la propriété peut être «remplacé» à l'aide de l'attribut DisplayNameAttribute. Malheureusement, les attributs ne peuvent pas avoir d'expressions non constantes. Je ne peux donc pas utiliser de ressources fortement typées telles que:

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

J'ai jeté un coup d'œil et j'ai trouvé une suggestion pour hériter de DisplayNameAttribute pour pouvoir utiliser la ressource. Je finirais avec un code comme:

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

Cependant, je perds des avantages de ressources fortement typés, ce qui n'est certainement pas une bonne chose. Ensuite, je suis tombé sur DisplayNameResourceAttribute qui est peut-être ce que je recherche. Mais il est censé être dans l'espace de noms Microsoft.VisualStudio.Modeling.Design et je ne trouve pas la référence que je suis censé ajouter pour cet espace de noms.

Quelqu'un sait-il s'il existe un moyen plus simple de parvenir à la localisation DisplayName d'une bonne manière? ou s'il existe un moyen d'utiliser ce que Microsoft semble utiliser pour Visual Studio?

PowerKiKi
la source
2
Qu'en est-il de l'affichage (ResourceType = typeof (ResourceStrings), Name = "MyProperty") voir msdn.microsoft.com/en-us/library
Peter
@Peter a lu attentivement le message, il veut exactement le contraire, en utilisant ResourceStrings et en vérifiant l'heure de compilation, pas de chaînes codées en dur ...
Marko

Réponses:

113

Il existe l' attribut Display de System.ComponentModel.DataAnnotations dans .NET 4. Il fonctionne sur le MVC 3 PropertyGrid.

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

Cela recherche une ressource nommée UserNamedans votre MyResourcesfichier .resx.

RandomEngy
la source
J'ai regardé partout avant de trouver cette page ... c'est une telle bouée de sauvetage. Merci! Fonctionne bien sur MVC5 pour moi.
Kris
Si le compilateur se plaint typeof(MyResources), vous devrez peut-être définir votre modificateur d'accès au fichier de ressources sur Public .
thatWiseGuy
80

Nous faisons cela pour un certain nombre d'attributs afin de prendre en charge plusieurs langues. Nous avons adopté une approche similaire à Microsoft, où ils remplacent leurs attributs de base et transmettent un nom de ressource plutôt que la chaîne réelle. Le nom de la ressource est ensuite utilisé pour effectuer une recherche dans les ressources DLL pour la chaîne réelle à renvoyer.

Par exemple:

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);
        }
    }
}

Vous pouvez aller plus loin lorsque vous utilisez réellement l'attribut et spécifier vos noms de ressources en tant que constantes dans une classe statique. De cette façon, vous obtenez des déclarations comme.

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

La mise à jour
ResourceStrings ressemblerait à quelque chose comme (note, chaque chaîne ferait référence au nom d'une ressource qui spécifie la chaîne réelle):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Jeff Yates
la source
Lorsque j'essaye cette approche, j'obtiens un message d'erreur disant "Un argument d'attribut doit être une expression constante, une expression de type ou une expression de création de tableau d'un type de paramètre d'attribut". Toutefois, la transmission de la valeur à LocalizedDisplayName en tant que chaîne fonctionne. Je souhaite qu'il soit fortement typé.
Azure SME
1
@Andy: les valeurs de ResourceStrings doivent être des constantes, comme indiqué dans la réponse, et non des propriétés ou des valeurs en lecture seule. Ils doivent être marqués comme const et faire référence aux noms des ressources, sinon vous obtiendrez une erreur.
Jeff Yates
1
Répondu à ma propre question, était de savoir où vous aviez le Resources.ResourceManager, dans mon cas, les fichiers resx sont des fichiers resx publics générés donc c'était[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith
dit que j'ai besoin d'une instance de Resources.ResourceManager afin d'appeler get string dessus
topwik
1
@LTR: Pas de problème. Je suis content que vous ayez exploré le fond du problème. Heureux d'aider, si je peux.
Jeff Yates
41

Voici la solution avec laquelle je me suis retrouvé dans un assemblage séparé (appelé "Common" dans mon cas):

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

avec le code pour rechercher la ressource:

  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
  }

L'utilisation typique serait:

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

Ce qui est assez laid car j'utilise des chaînes littérales pour la clé de ressource. Utiliser une constante signifierait modifier Resources.Designer.cs, ce qui n'est probablement pas une bonne idée.

Conclusion: je ne suis pas satisfait de cela, mais je suis encore moins satisfait de Microsoft qui ne peut rien fournir d'utile pour une tâche aussi courante.

PowerKiKi
la source
Très utile. Merci. À l'avenir, j'espère que Microsoft proposera une solution intéressante qui offre une manière fortement typée de référencer les ressources.
Johnny Oshika
ya ce truc de chaîne est vraiment nul: (Si vous pouviez obtenir le nom de propriété de la propriété qui utilise l'attribut, vous pourriez le faire dans la convention sur la manière de configuration, mais cela ne semble pas être possible. Les énumérations, que vous pourriez utiliser, ne sont pas non plus vraiment maintenables: /
Rookian
C'est une bonne solution. Je ne voudrais tout simplement pas parcourir la collection de ResourceManagerpropriétés. Au lieu de cela, vous pouvez simplement obtenir la propriété directement à partir du type fourni dans le paramètre:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer
1
Combinez cela avec le modèle T4 de @ zielu1 pour générer automatiquement les clés de ressources, et vous avez un digne gagnant!
David Keaveny
19

En utilisant l' attribut Display (de System.ComponentModel.DataAnnotations) et l' expression nameof () en C # 6, vous obtiendrez une solution localisée et fortement typée.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
dionoïde
la source
1
Dans cet exemple, qu'est-ce que "MyResources"? Un fichier resx fortement typé? Une classe personnalisée?
Greg
14

Vous pouvez utiliser T4 pour générer des constantes. J'en ai écrit un:

<#@ 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
la source
À quoi ressemblerait la sortie?
irfandar
9

C'est une vieille question, mais je pense que c'est un problème très courant, et voici ma solution dans MVC 3.

Tout d'abord, un modèle T4 est nécessaire pour générer des constantes afin d'éviter les chaînes désagréables. Nous avons un fichier de ressources 'Labels.resx' contenant toutes les chaînes d'étiquettes. Par conséquent, le modèle T4 utilise directement le fichier de ressources,

<#@ template debug="True" hostspecific="True" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="C:\Project\trunk\Resources\bin\Development\Resources.dll" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Resources" #>
<#
  var resourceStrings = new List<string>();
  var manager = Resources.Labels.ResourceManager;

  IDictionaryEnumerator enumerator = manager.GetResourceSet(CultureInfo.CurrentCulture,  true, true)
                                             .GetEnumerator();
  while (enumerator.MoveNext())
  {
        resourceStrings.Add(enumerator.Key.ToString());
  }
#>     

// This file is generated automatically. Do NOT modify any content inside.

namespace Lib.Const{
        public static class LabelNames{
<#
            foreach (String label in resourceStrings){
#>                    
              public const string <#=label#> =     "<#=label#>";                    
<#
           }    
#>
    }
}

Ensuite, une méthode d'extension est créée pour localiser le 'DisplayName',

using System.ComponentModel.DataAnnotations;
using Resources;

namespace Web.Extensions.ValidationAttributes
{
    public static class ValidationAttributeHelper
    {
        public static ValidationContext LocalizeDisplayName(this ValidationContext    context)
        {
            context.DisplayName = Labels.ResourceManager.GetString(context.DisplayName) ?? context.DisplayName;
            return context;
        }
    }
}

L'attribut 'DisplayName' est remplacé par l'attribut 'DisplayLabel' afin de lire automatiquement à partir de 'Labels.resx',

namespace Web.Extensions.ValidationAttributes
{

    public class DisplayLabelAttribute :System.ComponentModel.DisplayNameAttribute
    {
        private readonly string _propertyLabel;

        public DisplayLabelAttribute(string propertyLabel)
        {
            _propertyLabel = propertyLabel;
        }

        public override string DisplayName
        {
            get
            {
                return _propertyLabel;
            }
        }
    }
}

Après tout ce travail de préparation, il est temps de toucher ces attributs de validation par défaut. J'utilise l'attribut "Obligatoire" comme exemple,

using System.ComponentModel.DataAnnotations;
using Resources;

namespace Web.Extensions.ValidationAttributes
{
    public class RequiredAttribute : System.ComponentModel.DataAnnotations.RequiredAttribute
    {
        public RequiredAttribute()
        {
          ErrorMessageResourceType = typeof (Errors);
          ErrorMessageResourceName = "Required";
        }

        protected override ValidationResult IsValid(object value, ValidationContext  validationContext)
        {
            return base.IsValid(value, validationContext.LocalizeDisplayName());
        }

    }
}

Maintenant, nous pouvons appliquer ces attributs dans notre modèle,

using Web.Extensions.ValidationAttributes;

namespace Web.Areas.Foo.Models
{
    public class Person
    {
        [DisplayLabel(Lib.Const.LabelNames.HowOldAreYou)]
        public int Age { get; set; }

        [Required]
        public string Name { get; set; }
    }
}

Par défaut, le nom de la propriété est utilisé comme clé pour rechercher «Label.resx», mais si vous le définissez via «DisplayLabel», il l'utilisera à la place.

YYFish
la source
6

Vous pouvez sous-classer DisplayNameAttribute pour fournir i18n, en remplaçant l'une des méthodes. Ainsi. edit: Vous devrez peut-être vous contenter d'utiliser une constante pour la clé.

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
la source
2

J'utilise cette façon résoudre dans mon cas

[LocalizedDisplayName("Age", NameResourceType = typeof(RegistrationResources))]
 public bool Age { get; set; }

Avec le code

public sealed class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    private PropertyInfo _nameProperty;
    private Type _resourceType;


    public LocalizedDisplayNameAttribute(string displayNameKey)
        : base(displayNameKey)
    {

    }

    public Type NameResourceType
    {
        get
        {
            return _resourceType;
        }
        set
        {
            _resourceType = value;
            _nameProperty = _resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
        }
    }

    public override string DisplayName
    {
        get
        {
            if (_nameProperty == null)
            {
                return base.DisplayName;
            }

            return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
        }
    }

}
HaikMnatsakanyan
la source
1

Eh bien, l'assemblée l'est Microsoft.VisualStudio.Modeling.Sdk.dll. qui est fourni avec le SDK Visual Studio (avec le package d'intégration Visual Studio).

Mais il serait utilisé à peu près de la même manière que votre attribut; il n'y a aucun moyen d'utiliser fortement des ressources de types dans les attributs simplement parce qu'elles ne sont pas constantes.

configurateur
la source
0

Je m'excuse pour le code VB.NET, mon C # est un peu rouillé ... Mais vous aurez l'idée, non?

Tout d'abord, créez une nouvelle classe LocalizedPropertyDescriptor:, qui hérite PropertyDescriptor. Remplacez la DisplayNamepropriété comme ceci:

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 est le ResourceManager du fichier de ressources qui contient vos traductions.

Ensuite, implémentez ICustomTypeDescriptordans la classe avec les propriétés localisées et remplacez la GetPropertiesméthode:

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

Vous pouvez maintenant utiliser l'attribut 'DisplayName` pour stocker une référence à une valeur dans un fichier de ressources ...

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

prop_description est la clé du fichier de ressources.

Vincent Van Den Berghe
la source
La première partie de votre solution est ce que j'ai fait ... jusqu'à ce que je doive résoudre le "qu'est-ce que Some.ResourceManager?" question. Suis-je censé donner une deuxième chaîne littérale telle que "MyAssembly.Resources.Resource"? bien trop dangereux! Quant à la deuxième partie (ICustomTypeDescriptor), je ne pense pas que ce soit vraiment utile
PowerKiKi
La solution de Marc Gravell est la voie à suivre si vous n'avez besoin de rien d'autre qu'un DisplayName traduit - j'utilise également le descripteur personnalisé pour d'autres choses, et c'était ma solution. Il n'y a cependant aucun moyen de le faire sans fournir une sorte de clé.
Vincent Van Den Berghe