Localizzazione di DisplayNameAttribute


120

Sto cercando un modo per localizzare i nomi delle proprietà visualizzati in un PropertyGrid. Il nome della proprietà può essere "sovrascritto" utilizzando l'attributo DisplayNameAttribute. Purtroppo gli attributi non possono avere espressioni non costanti. Quindi non posso utilizzare risorse fortemente tipizzate come:

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

Ho dato un'occhiata in giro e ho trovato qualche suggerimento da ereditare da DisplayNameAttribute per poter utilizzare la risorsa. Finirei con un codice come:

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

Tuttavia perdo i vantaggi delle risorse fortemente tipizzate, il che non è sicuramente una buona cosa. Poi mi sono imbattuto in DisplayNameResourceAttribute che potrebbe essere quello che sto cercando. Ma dovrebbe essere nello spazio dei nomi Microsoft.VisualStudio.Modeling.Design e non riesco a trovare quale riferimento dovrei aggiungere per questo spazio dei nomi.

Qualcuno sa se c'è un modo più semplice per ottenere la localizzazione DisplayName in un buon modo? o se esiste un modo per utilizzare ciò che Microsoft sembra utilizzare per Visual Studio?


2
Che dire di Display (ResourceType = typeof (ResourceStrings), Name = "MyProperty") vedi msdn.microsoft.com/en-us/library/…
Peter

@Peter ha letto attentamente il post, vuole esattamente l'opposto, usando ResourceStrings e il tempo di compilazione controlla le stringhe non hard coded ...
Marko

Risposte:


113

C'è l' attributo Display da System.ComponentModel.DataAnnotations in .NET 4. Funziona su MVC 3 PropertyGrid.

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

Questo cerca una risorsa denominata UserNamenel tuo MyResourcesfile .resx.


Ho guardato dappertutto prima di trovare questa pagina ... questo è un tale salvavita. Grazie! Funziona bene su MVC5 per me.
Kris

Se il compilatore si lamenta typeof(MyResources), potrebbe essere necessario impostare il modificatore di accesso al file di risorse su Pubblico .
thatWiseGuy

80

Lo stiamo facendo per una serie di attributi al fine di supportare più lingue. Abbiamo adottato un approccio simile a Microsoft, in cui sovrascrivono i loro attributi di base e passano un nome di risorsa anziché la stringa effettiva. Il nome della risorsa viene quindi utilizzato per eseguire una ricerca nelle risorse DLL per la restituzione della stringa effettiva.

Per esempio:

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

È possibile fare un ulteriore passo avanti quando si utilizza effettivamente l'attributo e specificare i nomi delle risorse come costanti in una classe statica. In questo modo, ottieni dichiarazioni come.

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

L'aggiornamento
ResourceStrings sarebbe simile a (nota, ogni stringa farebbe riferimento al nome di una risorsa che specifica la stringa effettiva):

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

Quando provo questo approccio, ricevo un messaggio di errore che dice "Un argomento di attributo deve essere un'espressione costante, un'espressione di tipo o un'espressione di creazione di array di un tipo di parametro di attributo". Tuttavia, il passaggio del valore a LocalizedDisplayName come stringa funziona. Vorrei che fosse fortemente digitato.
Azure SME

1
@ Andy: i valori in ResourceStrings devono essere costanti, come indicato nella risposta, non proprietà o valori di sola lettura. Devono essere contrassegnati come const e fare riferimento ai nomi delle risorse, altrimenti si otterrà un errore.
Jeff Yates

1
Ho risposto alla mia domanda, riguardava dove avevi le risorse.ResourceManager, nel mio caso i file resx sono resx pubblici generati quindi era[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith

dice che ho bisogno di un'istanza di Resources.ResourceManager per chiamare get string su di essa
topwik

1
@ LTR: nessun problema. Sono contento che tu sia arrivato fino in fondo alla questione. Felice di aiutarti, se posso.
Jeff Yates

41

Ecco la soluzione che ho trovato in un assembly separato (chiamato "Common" nel mio caso):

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

con il codice per cercare la risorsa:

  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'utilizzo tipico sarebbe:

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

Ciò che è praticamente brutto perché uso stringhe letterali per la chiave della risorsa. Usare una costante significherebbe modificare Resources.Designer.cs che probabilmente non è una buona idea.

Conclusione: non sono contento di questo, ma sono ancora meno felice di Microsoft che non può fornire nulla di utile per un'attività così comune.


Molto utile. Grazie. In futuro, spero che Microsoft proponga una bella soluzione che offra un modo fortemente tipizzato di fare riferimento alle risorse.
Johnny Oshika

ya questa roba di stringa fa schifo davvero difficile :( Se potessi ottenere il nome della proprietà che usa l'attributo, potresti farlo nella convenzione rispetto al modo di configurazione, ma questo sembra non essere possibile. Anche le enumerazioni, potresti usare, non sono realmente mantenibili: /
Rookian

Questa è una buona soluzione. Semplicemente non itererei nella raccolta di ResourceManagerproprietà. Invece puoi semplicemente ottenere la proprietà direttamente dal tipo fornito nel parametro:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer

1
Combina questo con il modello T4 di @ zielu1 per generare automaticamente le chiavi delle risorse e avrai un degno vincitore!
David Keaveny

19

Usando l' attributo Display (da System.ComponentModel.DataAnnotations) e l' espressione nameof () in C # 6, otterrai una soluzione localizzata e fortemente tipizzata.

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

1
In questo esempio, cos'è "MyResources"? Un file resx fortemente tipizzato? Una lezione personalizzata?
Greg

14

È possibile utilizzare T4 per generare costanti. Ne ho scritto uno:

<#@ 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#>";
  <# } #>
  }
 }
}

Come sarebbe l'output?
irfandar

9

Questa è una vecchia domanda, ma penso che questo sia un problema molto comune, ed ecco la mia soluzione in MVC 3.

In primo luogo, è necessario un modello T4 per generare costanti per evitare stringhe fastidiose. Abbiamo un file di risorse "Labels.resx" che contiene tutte le stringhe di etichetta. Pertanto il modello T4 utilizza direttamente il file di risorse,

<#@ 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#>";                    
<#
           }    
#>
    }
}

Quindi, viene creato un metodo di estensione per localizzare il "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'attributo "DisplayName" è sostituito dall'attributo "DisplayLabel" per poter leggere automaticamente da "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;
            }
        }
    }
}

Dopo tutto questo lavoro di preparazione, è tempo di toccare gli attributi di convalida predefiniti. Utilizzo l'attributo "Obbligatorio" come esempio,

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

    }
}

Ora possiamo applicare questi attributi nel nostro modello,

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

Per impostazione predefinita, il nome della proprietà viene utilizzato come chiave per cercare "Label.resx", ma se lo imposti tramite "DisplayLabel", verrà utilizzato quello.


6

È possibile creare una sottoclasse DisplayNameAttribute per fornire i18n, sovrascrivendo uno dei metodi. Così. modifica: potrebbe essere necessario accontentarsi di utilizzare una costante per la chiave.

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

2

Uso questo modo per risolvere nel mio caso

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

Con il codice

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

}

1

Bene, l'assemblea è Microsoft.VisualStudio.Modeling.Sdk.dll. fornito con Visual Studio SDK (con pacchetto di integrazione di Visual Studio).

Ma sarebbe usato più o meno allo stesso modo del tuo attributo; non c'è modo di usare fortemente le risorse dei tipi negli attributi semplicemente perché non sono costanti.


0

Mi scuso per il codice VB.NET, il mio C # è un po 'arrugginito ... Ma avrai l'idea, giusto?

Prima di tutto, crea una nuova classe LocalizedPropertyDescriptor:, che eredita PropertyDescriptor. Sostituisci la DisplayNameproprietà in questo modo:

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 è il ResourceManager del file di risorse che contiene le tue traduzioni.

Successivamente, implementa ICustomTypeDescriptornella classe con le proprietà localizzate e sovrascrivi il GetPropertiesmetodo:

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

È ora possibile utilizzare l'attributo 'DisplayName` per memorizzare un riferimento a un valore in un file di risorse ...

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

prop_description è la chiave nel file di risorse.


La prima parte della tua soluzione è ciò che ho fatto ... finché non ho dovuto risolvere il problema "cos'è Some.ResourceManager?" domanda. Devo fornire una seconda stringa letterale come "MyAssembly.Resources.Resource"? troppo pericoloso! Per quanto riguarda la seconda parte (ICustomTypeDescriptor) non credo sia effettivamente utile
PowerKiKi

La soluzione di Marc Gravell è la strada da percorrere se non hai bisogno di nient'altro che un DisplayName tradotto: uso il descrittore personalizzato anche per altre cose, e questa era la mia soluzione. Tuttavia, non è possibile farlo senza fornire una sorta di chiave.
Vincent Van Den Berghe
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.