Lokalizacja DisplayNameAttribute

120

Szukam sposobu na zlokalizowanie nazw właściwości wyświetlanych w PropertyGrid. Nazwę właściwości można „nadpisać” przy użyciu atrybutu DisplayNameAttribute. Niestety atrybuty nie mogą mieć wyrażeń innych niż stałe. Nie mogę więc używać silnie wpisanych zasobów, takich jak:

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

Rozejrzałem się i znalazłem sugestię, aby dziedziczyć po DisplayNameAttribute, aby móc korzystać z zasobów. Skończyłbym z kodem takim jak:

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

Jednak tracę mocno wpisane korzyści związane z zasobami, co zdecydowanie nie jest dobrą rzeczą. Potem natknąłem się na DisplayNameResourceAttribute, który może być tym, czego szukam. Ale ma znajdować się w przestrzeni nazw Microsoft.VisualStudio.Modeling.Design i nie mogę znaleźć odniesienia, które mam dodać do tej przestrzeni nazw.

Czy ktoś wie, czy istnieje łatwiejszy sposób na osiągnięcie lokalizacji DisplayName w dobry sposób? lub jeśli istnieje sposób wykorzystania tego, co wydaje się być używane przez firmę Microsoft w programie Visual Studio?

PowerKiKi
źródło
2
A co z Display (ResourceType = typeof (ResourceStrings), Name = "MyProperty") patrz msdn.microsoft.com/en-us/library/…
Peter
@Peter uważnie przeczytaj post, chce dokładnie odwrotnie, używając ResourceStrings i czasu kompilacji, sprawdź nie zakodowane ciągi ...
Marko

Odpowiedzi:

113

Istnieje atrybut Display z System.ComponentModel.DataAnnotations w .NET 4. Działa na MVC 3 PropertyGrid.

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

To wyszukuje zasób nazwany UserNamew twoim MyResourcespliku resx.

RandomEngy
źródło
Rozejrzałem się, zanim znalazłem tę stronę ... to takie uratowanie życia. Dzięki! Dla mnie działa dobrze na MVC5.
Kris
Jeśli kompilator narzeka typeof(MyResources), może być konieczne ustawienie modyfikatora dostępu do pliku zasobów na Publiczny .
thatWiseGuy
80

Robimy to dla wielu atrybutów, aby obsługiwać wiele języków. Podjęliśmy podobne podejście do firmy Microsoft, w której zastępują one swoje podstawowe atrybuty i przekazują nazwę zasobu zamiast rzeczywistego ciągu. Nazwa zasobu jest następnie używana do wyszukiwania w zasobach DLL w celu zwrócenia rzeczywistego ciągu.

Na przykład:

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

Możesz pójść o krok dalej, gdy faktycznie używasz atrybutu i określić nazwy zasobów jako stałe w klasie statycznej. W ten sposób otrzymasz deklaracje typu.

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

Aktualizacja
ResourceStrings wyglądałaby mniej więcej tak (uwaga, każdy ciąg odnosiłby się do nazwy zasobu, który określa rzeczywisty ciąg):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Jeff Yates
źródło
Kiedy próbuję tego podejścia, otrzymuję komunikat o błędzie mówiący: „Argument atrybutu musi być wyrażeniem stałym, wyrażeniem typu lub wyrażeniem tworzenia tablicy typu parametru atrybutu”. Jednak przekazanie wartości do LocalizedDisplayName jako ciągu działa. Chciałbym, żeby było mocno wpisane na maszynie.
Azure SME,
1
@Andy: wartości w ResourceStrings muszą być stałymi, jak wskazano w odpowiedzi, a nie właściwościami lub wartościami tylko do odczytu. Muszą być oznaczone jako stałe i odnosić się do nazw zasobów, w przeciwnym razie wystąpi błąd.
Jeff Yates
1
Odpowiedziałem na moje własne pytanie, dotyczyło tego, gdzie masz zasoby.ResourceManager, w moim przypadku pliki resx są publicznie wygenerowanymi plikami resx, więc tak było[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith
Mówi, że potrzebuję wystąpienia Resources.ResourceManager, aby wywołać na nim ciąg get
topwik
1
@LTR: Nie ma problemu. Cieszę się, że dotarłeś do sedna problemu. Chętnie pomogę, jeśli mogę.
Jeff Yates,
41

Oto rozwiązanie, które znalazłem w osobnym zestawie (zwanym w moim przypadku „wspólnym”):

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

z kodem do wyszukania zasobu:

  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
  }

Typowe użycie to:

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

Co jest dość brzydkie, ponieważ używam literalnych ciągów jako klucza zasobów. Używanie stałej oznaczałoby modyfikację Resources.Designer.cs, co prawdopodobnie nie jest dobrym pomysłem.

Wniosek: nie jestem z tego zadowolony, ale jeszcze mniej cieszy mnie Microsoft, który nie jest w stanie zapewnić niczego przydatnego do tak powszechnego zadania.

PowerKiKi
źródło
Bardzo przydatne. Dzięki. Mam nadzieję, że w przyszłości Microsoft przedstawi fajne rozwiązanie, które będzie oferować silnie typizowany sposób odwoływania się do zasobów.
Johnny Oshika,
ya, te ciągi są naprawdę trudne :( Jeśli możesz uzyskać nazwę właściwości właściwości używającej atrybutu, możesz to zrobić w konwencji zamiast konfiguracji, ale wydaje się, że nie jest to możliwe. Dbanie o "silnie tpyed" Wyliczenia, których możesz użyć, również nie są tak naprawdę możliwe do utrzymania: /
Rookian
To dobre rozwiązanie. Po prostu nie przeglądałbym kolekcji ResourceManagerwłaściwości. Zamiast tego możesz po prostu pobrać nieruchomość bezpośrednio z typu podanego w parametrze:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer
1
Połącz to z szablonem T4 @ zielu1, aby automatycznie generować klucze zasobów i masz godnego zwycięzcę!
David Keaveny
19

Używając atrybutu Display (z System.ComponentModel.DataAnnotations) i wyrażenia nameof () w C # 6, otrzymasz zlokalizowane i silnie wpisane rozwiązanie.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
dionoid
źródło
1
W tym przykładzie, co to jest „Moje zasoby”? Plik resx o silnym typie? Klasa niestandardowa?
Greg
14

Możesz użyć T4 do wygenerowania stałych. Napisałem jeden:

<#@ 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
źródło
Jaki byłby wynik?
irfandar
9

To stare pytanie, ale myślę, że jest to bardzo powszechny problem, a oto moje rozwiązanie w MVC 3.

Po pierwsze, szablon T4 jest potrzebny do generowania stałych, aby uniknąć nieprzyjemnych ciągów znaków. Mamy plik zasobów „Labels.resx” zawierający wszystkie ciągi etykiet. Dlatego szablon T4 używa bezpośrednio pliku zasobów,

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

Następnie zostaje utworzona metoda rozszerzenia, aby zlokalizować „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;
        }
    }
}

Atrybut „DisplayName” zostaje zastąpiony atrybutem „DisplayLabel” w celu automatycznego odczytu z „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;
            }
        }
    }
}

Po tych wszystkich przygotowaniach czas na dotknięcie domyślnych atrybutów walidacji. Jako przykładu używam atrybutu „Wymagane”,

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

    }
}

Teraz możemy zastosować te atrybuty w naszym modelu,

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

Domyślnie nazwa właściwości jest używana jako klucz do wyszukiwania „Label.resx”, ale jeśli ustawisz ją za pomocą „DisplayLabel”, użyje go zamiast tego.

YYFish
źródło
6

Możesz podklasę DisplayNameAttribute, aby zapewnić i18n, zastępując jedną z metod. Tak, tak. edycja: Być może będziesz musiał zadowolić się użyciem stałej dla klucza.

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
źródło
2

W moim przypadku używam tego rozwiązania

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

Z kodem

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
źródło
1

Cóż, montaż jest Microsoft.VisualStudio.Modeling.Sdk.dll. który jest dostarczany z Visual Studio SDK (z pakietem integracyjnym Visual Studio).

Ale byłby używany w mniej więcej taki sam sposób, jak twój atrybut; nie ma możliwości użycia silnie typów zasobów w atrybutach tylko dlatego, że nie są one stałe.

konfigurator
źródło
0

Przepraszam za kod VB.NET, mój C # jest trochę zardzewiały ... Ale zrozumiesz, o co chodzi, prawda?

Przede wszystkim utwórz nową klasę LocalizedPropertyDescriptor:, która dziedziczy PropertyDescriptor. Zastąp DisplayNamewłaściwość w następujący sposób:

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 to ResourceManager pliku zasobów, który zawiera tłumaczenia.

Następnie zaimplementuj ICustomTypeDescriptorw klasie ze zlokalizowanymi właściwościami i zastąp GetPropertiesmetodę:

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

Możesz teraz użyć atrybutu „DisplayName” do przechowywania odniesienia do wartości w pliku zasobów ...

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

prop_description jest kluczem w pliku zasobów.

Vincent Van Den Berghe
źródło
Pierwszą częścią twojego rozwiązania jest to, co zrobiłem ... dopóki nie musiałem rozwiązać problemu „co to jest Some.ResourceManager?” pytanie. Czy mam podać drugi ciąg literału, taki jak „MyAssembly.Resources.Resource”? zbyt niebezpieczne! Jeśli chodzi o drugą część (ICustomTypeDescriptor), nie sądzę, że jest to faktycznie przydatne
PowerKiKi,
Rozwiązanie Marc Gravell jest najlepszym rozwiązaniem, jeśli nie potrzebujesz niczego innego niż przetłumaczona nazwa DisplayName - używam niestandardowego deskryptora również do innych rzeczy i to było moje rozwiązanie. Nie da się tego jednak zrobić bez dostarczenia jakiegoś klucza.
Vincent Van Den Berghe,