Localización de DisplayNameAttribute

120

Estoy buscando una forma de localizar los nombres de las propiedades que se muestran en un PropertyGrid. El nombre de la propiedad puede ser "anulado" mediante el atributo DisplayNameAttribute. Desafortunadamente, los atributos no pueden tener expresiones no constantes. Entonces no puedo usar recursos fuertemente tipados como:

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

Eché un vistazo y encontré una sugerencia para heredar de DisplayNameAttribute para poder usar el recurso. Terminaría con un código como:

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

Sin embargo, pierdo beneficios de recursos fuertemente tipificados, lo que definitivamente no es algo bueno. Luego me encontré con DisplayNameResourceAttribute, que puede ser lo que estoy buscando. Pero se supone que debe estar en el espacio de nombres Microsoft.VisualStudio.Modeling.Design y no puedo encontrar qué referencia se supone que debo agregar para este espacio de nombres.

¿Alguien sabe si hay una manera más fácil de lograr la localización de DisplayName de una buena manera? ¿O si hay alguna forma de usar lo que Microsoft parece estar usando para Visual Studio?

PowerKiKi
fuente
2
¿Qué pasa con Display (ResourceType = typeof (ResourceStrings), Name = "MyProperty") consulte msdn.microsoft.com/en-us/library/…
Peter
@Peter leyó la publicación con cuidado, quiere exactamente lo contrario, usando ResourceStrings y compilar el tiempo de verificación, no cadenas codificadas de forma rígida ...
Marko

Respuestas:

113

Existe el atributo Display de System.ComponentModel.DataAnnotations en .NET 4. Funciona en MVC 3 PropertyGrid.

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

Esto busca un recurso nombrado UserNameen su MyResourcesarchivo .resx.

RandomEngy
fuente
Miré por todas partes antes de encontrar esta página ... esto es un gran salvavidas. ¡Gracias! Funciona bien en MVC5 para mí.
Kris
Si el compilador se queja typeof(MyResources), es posible que deba establecer el modificador de acceso al archivo de recursos en Público .
thatWiseGuy
80

Estamos haciendo esto para varios atributos con el fin de admitir varios idiomas. Hemos adoptado un enfoque similar a Microsoft, donde anulan sus atributos básicos y pasan un nombre de recurso en lugar de la cadena real. A continuación, el nombre del recurso se utiliza para realizar una búsqueda en los recursos DLL para que vuelva la cadena real.

Por ejemplo:

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

Puede llevar esto un paso más allá cuando realmente use el atributo y especificar los nombres de sus recursos como constantes en una clase estática. De esa manera, obtienes declaraciones como.

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

La actualización
ResourceStrings se vería así (tenga en cuenta que cada cadena se referirá al nombre de un recurso que especifica la cadena real):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Jeff Yates
fuente
Cuando intento este enfoque, aparece un mensaje de error que dice "Un argumento de atributo debe ser una expresión constante, un tipo de expresión o una expresión de creación de matriz de un tipo de parámetro de atributo". Sin embargo, pasar el valor a LocalizedDisplayName como una cadena funciona. Ojalá estuviera fuertemente tipado.
Azure SME
1
@Andy: Los valores en ResourceStrings deben ser constantes, como se indica en la respuesta, no propiedades o valores de solo lectura. Deben estar marcados como const y hacer referencia a los nombres de los recursos; de lo contrario, obtendrá un error.
Jeff Yates
1
Respondí a mi propia pregunta, era sobre dónde tenía los Resources.ResourceManager, en mi caso, los archivos resx son públicos generados por resx, por lo que fue[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith
dice que necesito una instancia de Resources.ResourceManager para llamar a get string en él
topwik
1
@LTR: No hay problema. Me alegra que hayas llegado al fondo del problema. Feliz de ayudar, si puedo.
Jeff Yates
41

Aquí está la solución con la que terminé en un ensamblado separado (llamado "Común" en mi 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 el código para buscar el recurso:

  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
  }

El uso típico sería:

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

Lo que es bastante feo ya que uso cadenas literales para la clave de recursos. Usar una constante allí significaría modificar Resources.Designer.cs, lo que probablemente no sea una buena idea.

Conclusión: no estoy contento con eso, pero estoy aún menos contento con Microsoft, que no puede proporcionar nada útil para una tarea tan común.

PowerKiKi
fuente
Muy útil. Gracias. En el futuro, espero que Microsoft presente una buena solución que ofrezca una forma muy tipada de hacer referencia a los recursos.
Johnny Oshika
ya, esto de la cadena apesta realmente :( Si pudiera obtener el nombre de propiedad de la propiedad que usa el atributo, podría hacerlo en la convención sobre la forma de configuración, pero esto parece no ser posible. Las enumeraciones, podría usar, tampoco son realmente mantenibles: /
Rookian
Esa es una buena solucion. Simplemente no iteraría a través de la colección de ResourceManagerpropiedades. En su lugar, puede simplemente obtener la propiedad directamente del tipo proporcionado en el parámetro:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer
1
Combine esto con la plantilla T4 de @ zielu1 para generar automáticamente las claves de recursos, ¡y tendrá un ganador digno!
David Keaveny
19

Usando el atributo Display (de System.ComponentModel.DataAnnotations) y la expresión nameof () en C # 6, obtendrá una solución localizada y fuertemente tipada.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
dionoide
fuente
1
En este ejemplo, ¿qué es "MyResources"? ¿Un archivo resx fuertemente tipado? ¿Una clase personalizada?
Greg
14

Puede usar T4 para generar constantes. Escribí 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#>";
  <# } #>
  }
 }
}
zielu1
fuente
¿Cómo sería la salida?
irfandar
9

Esta es una pregunta antigua, pero creo que es un problema muy común y aquí está mi solución en MVC 3.

En primer lugar, se necesita una plantilla T4 para generar constantes y evitar cadenas desagradables. Tenemos un archivo de recursos 'Labels.resx' que contiene todas las cadenas de etiquetas. Por lo tanto, la plantilla T4 usa el archivo de recursos directamente,

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

Luego, se crea un método de extensión para localizar el '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;
        }
    }
}

El atributo 'DisplayName' se reemplaza por el atributo 'DisplayLabel' para leer de 'Labels.resx' automáticamente,

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

Después de todo ese trabajo de preparación, es hora de tocar esos atributos de validación predeterminados. Estoy usando el atributo 'Requerido' como ejemplo,

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

    }
}

Ahora, podemos aplicar esos atributos en nuestro modelo,

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

De forma predeterminada, el nombre de la propiedad se usa como clave para buscar 'Label.resx', pero si lo configura a través de 'DisplayLabel', lo usará en su lugar.

YYFish
fuente
6

Puede crear una subclase de DisplayNameAttribute para proporcionar i18n, anulando uno de los métodos. Al igual que. editar: Puede que tenga que conformarse con usar una constante para la clave.

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
fuente
2

Yo uso de esta manera resolver en mi caso

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

Con el codigo

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
fuente
1

Bueno, la asamblea es Microsoft.VisualStudio.Modeling.Sdk.dll. que viene con el SDK de Visual Studio (con el paquete de integración de Visual Studio).

Pero se usaría prácticamente de la misma manera que su atributo; no hay forma de utilizar recursos de tipos fuertes en atributos simplemente porque no son constantes.

configurador
fuente
0

Pido disculpas por el código VB.NET, mi C # está un poco oxidado ... Pero entenderás la idea, ¿verdad?

En primer lugar, cree una nueva clase:, LocalizedPropertyDescriptorque hereda PropertyDescriptor. Anule la DisplayNamepropiedad de esta manera:

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 es el ResourceManager del archivo de recursos que contiene sus traducciones.

A continuación, implemente ICustomTypeDescriptoren la clase con las propiedades localizadas y anule el GetPropertiesmétodo:

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

Ahora puede usar el atributo 'DisplayName` para almacenar una referencia a un valor en un archivo de recursos ...

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

prop_description es la clave en el archivo de recursos.

Vincent Van Den Berghe
fuente
La primera parte de su solución es lo que hice ... hasta que tuve que resolver el "¿qué es Some.ResourceManager?" pregunta. ¿Se supone que debo dar una segunda cadena literal como "MyAssembly.Resources.Resource"? ¡Demasiado peligroso! En cuanto a la segunda parte (ICustomTypeDescriptor), no creo que sea realmente útil
PowerKiKi
La solución de Marc Gravell es el camino a seguir si no necesita nada más que un DisplayName traducido; también uso el descriptor personalizado para otras cosas, y esta fue mi solución. Sin embargo, no hay forma de hacer esto sin proporcionar algún tipo de clave.
Vincent Van Den Berghe