Localização de DisplayNameAttribute

120

Estou procurando uma maneira de localizar nomes de propriedades exibidos em um PropertyGrid. O nome da propriedade pode ser "substituído" usando o atributo DisplayNameAttribute. Infelizmente, os atributos não podem ter expressões não constantes. Portanto, não posso usar recursos fortemente tipados, como:

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

Eu dei uma olhada e encontrei algumas sugestões para herdar de DisplayNameAttribute para poder usar o recurso. Eu acabaria com código como:

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

No entanto, perco os benefícios de recursos fortemente tipados, o que definitivamente não é uma coisa boa. Então me deparei com DisplayNameResourceAttribute, que pode ser o que estou procurando. Mas ele deve estar no namespace Microsoft.VisualStudio.Modeling.Design e não consigo encontrar qual referência devo adicionar para esse namespace.

Alguém sabe se existe uma maneira mais fácil de obter a localização DisplayName de uma maneira boa? ou se existe como usar o que a Microsoft parece estar usando para o Visual Studio?

PowerKiKi
fonte
2
E quanto a Display (ResourceType = typeof (ResourceStrings), Name = "MyProperty"), consulte msdn.microsoft.com/en-us/library/…
Peter
@ Peter leu o post com atenção, ele quer exatamente o oposto, usando ResourceStrings e verificar o tempo de compilação, não as strings codificadas ...
Marko

Respostas:

113

Existe o atributo Display de System.ComponentModel.DataAnnotations no .NET 4. Ele funciona no MVC 3 PropertyGrid.

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

Isso procura um recurso nomeado UserNameno seu MyResourcesarquivo .resx.

RandomEngy
fonte
Eu olhei por toda parte antes de encontrar esta página ... isso é um salva-vidas. Obrigado! Funciona bem no MVC5 para mim.
Kris
Se o compilador estiver reclamando typeof(MyResources), pode ser necessário definir seu modificador de acesso ao arquivo de recursos como Público .
thatWiseGuy 02/02/19
80

Estamos fazendo isso para vários atributos, a fim de suportar vários idiomas. Adotamos uma abordagem semelhante à Microsoft, onde eles substituem seus atributos básicos e passam um nome de recurso em vez da string real. O nome do recurso é usado para executar uma pesquisa nos recursos DLL para a seqüência real retornar.

Por exemplo:

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

Você pode dar um passo além ao usar o atributo e especificar os nomes dos recursos como constantes em uma classe estática. Dessa forma, você recebe declarações como.

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

A atualização
ResourceStrings se pareceria com (observe que cada string se referiria ao nome de um recurso que especifica a string real):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Jeff Yates
fonte
Quando tento essa abordagem, recebo uma mensagem de erro dizendo "Um argumento de atributo deve ser uma expressão constante, tipo de expressão ou expressão de criação de matriz de um tipo de parâmetro de atributo". No entanto, passar o valor para LocalizedDisplayName como uma sequência de caracteres funciona. Gostaria que fosse fortemente digitado.
Azure SME
1
@ Andy: Os valores em ResourceStrings devem ser constantes, conforme indicado na resposta, não propriedades ou valores somente leitura. Eles devem ser marcados como const e referir-se aos nomes dos recursos, caso contrário, você receberá um erro.
Jeff Yates
1
Respondi minha própria pergunta, era sobre onde você teve a Resources.ResourceManager, no meu caso as resx são resx pública gerada por isso foi[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith
diz que eu preciso uma instância de Resources.ResourceManager, a fim de chamada cadeia de chegar nele
topwik
1
@ LTR: Não há problema. Estou feliz que você tenha chegado ao final da questão. Feliz em ajudar, se eu puder.
21912 Jeff Yates
41

Aqui está a solução que eu acabei em um assembly separado (chamado "Common" no meu 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))
      {
      }
   }

com o código para procurar o 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
  }

O uso típico seria:

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

O que é muito feio quando eu uso cadeias literais para a chave do recurso. O uso de uma constante significaria modificar o Resources.Designer.cs, o que provavelmente não é uma boa ideia.

Conclusão: Não estou satisfeito com isso, mas ainda menos com a Microsoft, que não pode fornecer nada de útil para uma tarefa tão comum.

PowerKiKi
fonte
Muito útil. Obrigado. No futuro, espero que a Microsoft tenha uma boa solução que ofereça uma maneira fortemente tipada de referenciar os recursos.
Johnny Oshika
ya essa coisa de cadeia é uma porcaria muito difícil :( Se você pudesse obter o nome da propriedade que usa o atributo, poderia fazê-lo na convenção sobre a configuração, mas isso parece não ser possível. Cuidar do "fortemente tipado" As enumerações, você poderia usar, também não são realmente mantidas: /
Rookian
Essa é uma boa solução. Eu simplesmente não iria percorrer a coleção de ResourceManagerpropriedades. Em vez disso, você pode simplesmente obter a propriedade diretamente do tipo fornecido no parâmetro:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer
1
Combine isso com o modelo T4 do @ zielu1 para gerar automaticamente as chaves de recursos e você terá um vencedor digno!
David Keaveny
19

Usando o atributo Display (de System.ComponentModel.DataAnnotations) e a expressão nameof () no C # 6, você obterá uma solução localizada e de tipo forte.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
dionóide
fonte
1
Neste exemplo, o que é "MyResources"? Um arquivo resx fortemente tipado? Uma classe personalizada?
Greg
14

Você pode usar o T4 para gerar constantes. Eu escrevi um:

<#@ 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
fonte
Como seria a saída?
irfandar
9

Essa é uma pergunta antiga, mas acho que esse é um problema muito comum, e aqui está minha solução no MVC 3.

Primeiramente, é necessário um modelo T4 para gerar constantes para evitar seqüências desagradáveis. Temos um arquivo de recurso 'Labels.resx' contém todas as seqüências de caracteres de rótulo. Portanto, o modelo T4 usa o arquivo de recurso diretamente,

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

Em seguida, um método de extensão é criado para localizar o '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;
        }
    }
}

O atributo 'DisplayName' é substituído pelo atributo 'DisplayLabel' para ler automaticamente a 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;
            }
        }
    }
}

Depois de todo esse trabalho de preparação, é hora de tocar nesses atributos de validação padrão. Estou usando o atributo "Necessário" como exemplo,

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

    }
}

Agora, podemos aplicar esses atributos em nosso 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; }
    }
}

Por padrão, o nome da propriedade é usado como a chave para procurar 'Label.resx', mas se você o definir por meio de 'DisplayLabel', ele será usado.

YYFish
fonte
6

Você pode subclassificar DisplayNameAttribute para fornecer i18n, substituindo um dos métodos. Igual a. editar: você pode ter que se contentar em usar uma constante para a chave.

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

Eu uso dessa maneira resolver no meu caso

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

Com o código

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

Bem, a montagem é Microsoft.VisualStudio.Modeling.Sdk.dll. que acompanha o Visual Studio SDK (com pacote de integração do Visual Studio).

Mas seria usado da mesma maneira que o seu atributo; não há como usar recursos de tipos fortes em atributos simplesmente porque eles não são constantes.

configurador
fonte
0

Peço desculpas pelo código VB.NET, meu C # está um pouco enferrujado ... Mas você entendeu a ideia, certo?

Primeiro de tudo, crie uma nova classe LocalizedPropertyDescriptor:, que herda PropertyDescriptor. Substitua a DisplayNamepropriedade assim:

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 é o ResourceManager do arquivo de recurso que contém suas traduções.

Em seguida, implemente ICustomTypeDescriptorna classe com as propriedades localizadas e substitua o 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

Agora você pode usar o atributo 'DisplayName` para armazenar uma referência a um valor em um arquivo de recurso ...

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

prop_description é a chave no arquivo de recurso.

Vincent Van Den Berghe
fonte
A primeira parte da sua solução foi o que fiz ... até que tive que resolver o "o que é o Some.ResourceManager?" questão. Devo fornecer uma segunda seqüência literal como "MyAssembly.Resources.Resource"? muito perigoso! Quanto à segunda parte (ICustomTypeDescriptor), eu não acho que seja realmente útil
PowerKiKi
A solução de Marc Gravell é o caminho a percorrer, se você não precisar de mais nada além de um DisplayName traduzido - eu também uso o descritor personalizado para outras coisas, e essa foi a minha solução. Não há como fazer isso sem fornecer algum tipo de chave.
Vincent Van Den Berghe