Lokalisasi DisplayNameAttribute

120

Saya mencari cara untuk melokalkan nama properti yang ditampilkan di PropertyGrid. Nama properti mungkin "diganti" menggunakan atribut DisplayNameAttribute. Sayangnya atribut tidak boleh memiliki ekspresi non konstan. Jadi saya tidak dapat menggunakan sumber daya yang diketik dengan kuat seperti:

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

Saya telah melihat-lihat dan menemukan beberapa saran untuk mewarisi dari DisplayNameAttribute agar dapat menggunakan sumber daya. Saya akan berakhir dengan kode seperti:

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

Namun saya kehilangan manfaat sumber daya yang diketik dengan kuat yang jelas bukan hal yang baik. Kemudian saya menemukan DisplayNameResourceAttribute yang mungkin saya cari. Tapi itu seharusnya di namespace Microsoft.VisualStudio.Modeling.Design dan saya tidak dapat menemukan referensi apa yang harus saya tambahkan untuk namespace ini.

Ada yang tahu jika ada cara yang lebih mudah untuk mencapai pelokalan DisplayName dengan cara yang baik? atau jika ada cara untuk menggunakan apa yang tampaknya digunakan Microsoft untuk Visual Studio?

PowerKiKi
sumber
2
Bagaimana dengan Tampilan (ResourceType = typeof (ResourceStrings), Name = "MyProperty") lihat msdn.microsoft.com/en-us/library/…
Peter
@Peter membaca posting dengan hati-hati, dia ingin kebalikannya, menggunakan ResourceStrings dan waktu kompilasi memeriksa string yang tidak dikodekan keras ...
Marko

Jawaban:

113

Ada atribut Tampilan dari System.ComponentModel.DataAnnotations di .NET 4. Ia bekerja pada MVC 3 PropertyGrid.

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

Ini mencari sumber daya yang bernama UserNamedalam MyResourcesfile .resx Anda .

RandomEngy
sumber
Saya melihat-lihat sebelum menemukan halaman ini ... ini benar-benar penyelamat. Terima kasih! Berfungsi baik di MVC5 untuk saya.
Kris
Jika kompilator mengeluh tentangnya typeof(MyResources), Anda mungkin perlu menyetel pengubah akses file sumber daya Anda ke Publik .
thatWiseGuy
80

Kami melakukan ini untuk sejumlah atribut untuk mendukung banyak bahasa. Kami telah mengambil pendekatan yang mirip dengan Microsoft, di mana mereka mengganti atribut dasarnya dan meneruskan nama sumber daya daripada string sebenarnya. Nama sumber daya kemudian digunakan untuk melakukan pencarian di sumber daya DLL untuk mengembalikan string sebenarnya.

Sebagai contoh:

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

Anda dapat mengambil langkah ini lebih jauh ketika benar-benar menggunakan atribut tersebut, dan menetapkan nama sumber daya Anda sebagai konstanta di kelas statis. Dengan begitu, Anda mendapatkan deklarasi seperti.

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

Pembaruan
ResourceStrings akan terlihat seperti (catatan, setiap string akan merujuk ke nama sumber daya yang menentukan string sebenarnya):

public static class ResourceStrings
{
    public const string ForegroundColorDisplayName="ForegroundColorDisplayName";
    public const string FontSizeDisplayName="FontSizeDisplayName";
}
Jeff Yates
sumber
Ketika saya mencoba pendekatan ini, saya mendapatkan pesan kesalahan yang mengatakan "Argumen atribut harus berupa ekspresi konstan, ekspresi tipe atau ekspresi pembuatan larik dari tipe parameter atribut". Namun, meneruskan nilai ke LocalizedDisplayName sebagai string berfungsi. Berharap itu akan diketik dengan kuat.
Azure SME
1
@Andy: Nilai dalam ResourceStrings harus berupa konstanta, seperti yang ditunjukkan dalam jawaban, bukan properti atau nilai hanya-baca. Mereka harus ditandai sebagai const dan merujuk ke nama sumber daya, jika tidak, Anda akan mendapatkan kesalahan.
Jeff Yates
1
Menjawab pertanyaan saya sendiri, adalah tentang di mana Anda memiliki Resources.ResourceManager, dalam kasus saya, file resx adalah file resx publik yang dibuat jadi[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString");
Tristan Warner-Smith
mengatakan saya memerlukan instance Resources.ResourceManager untuk memanggil get string di atasnya
topwik
1
@ LTR: Tidak masalah. Saya senang Anda telah menyelesaikan masalah ini. Senang bisa membantu, jika saya bisa.
Jeff Yates
41

Berikut adalah solusi yang saya dapatkan dalam perakitan terpisah (disebut "Umum" dalam kasus saya):

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

dengan kode untuk mencari sumber daya:

  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
  }

Penggunaan tipikal adalah:

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

Apa yang cukup jelek karena saya menggunakan string literal untuk kunci sumber daya. Menggunakan konstanta berarti memodifikasi Resources.Designer.cs yang mungkin bukan ide yang bagus.

Kesimpulan: Saya tidak senang dengan itu, tetapi saya bahkan kurang senang dengan Microsoft yang tidak dapat memberikan sesuatu yang berguna untuk tugas umum seperti itu.

PowerKiKi
sumber
Sangat berguna. Terima kasih. Di masa depan, saya berharap Microsoft menghadirkan solusi bagus yang menawarkan cara yang sangat diketik untuk mereferensikan sumber daya.
Johnny Oshika
ya hal string ini sangat menyebalkan :( Jika Anda bisa mendapatkan nama properti dari properti yang menggunakan atribut, Anda dapat melakukannya di konvensi melalui cara konfigurasi, tetapi ini tampaknya tidak mungkin. Merawat "sangat tpyed" Enumerasi, yang bisa Anda gunakan, juga tidak bisa dipelihara: /
Rookian
Itu solusi yang bagus. Saya tidak akan mengulang melalui koleksi ResourceManagerproperti. Alih-alih, Anda bisa langsung mendapatkan properti dari jenis yang disediakan di parameter:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
Maksymilian Majer
1
Gabungkan ini dengan template T4 @ zielu1 untuk menghasilkan kunci sumber daya secara otomatis, dan Anda akan mendapatkan pemenang yang layak!
David Keaveny
19

Menggunakan atribut Display (dari System.ComponentModel.DataAnnotations) dan ekspresi nameof () di C # 6, Anda akan mendapatkan solusi yang dilokalkan dan diketik dengan kuat.

[Display(ResourceType = typeof(MyResources), Name = nameof(MyResources.UserName))]
public string UserName { get; set; }
dionoid
sumber
1
Dalam contoh ini, apa itu "Sumber Daya Saya"? File resx yang diketik dengan kuat? Kelas khusus?
Greg
14

Anda bisa menggunakan T4 untuk menghasilkan konstanta. Saya menulis satu:

<#@ 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
sumber
Akan seperti apa hasilnya?
irfandar
9

Ini adalah pertanyaan lama, tapi menurut saya ini adalah masalah yang sangat umum, dan inilah solusi saya di MVC 3.

Pertama, template T4 diperlukan untuk menghasilkan konstanta untuk menghindari string yang tidak menyenangkan. Kami memiliki file sumber daya 'Labels.resx' menampung semua string label. Oleh karena itu, templat T4 menggunakan file sumber daya secara langsung,

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

Kemudian, metode ekstensi dibuat untuk melokalkan '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;
        }
    }
}

Atribut 'DisplayName' diganti dengan atribut 'DisplayLabel' untuk membaca dari 'Labels.resx' secara otomatis,

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

Setelah semua pekerjaan persiapan itu, saatnya menyentuh atribut validasi default tersebut. Saya menggunakan atribut 'Wajib' sebagai contoh,

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

    }
}

Sekarang, Kita dapat menerapkan atribut tersebut dalam model kita,

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

Secara default, nama properti digunakan sebagai kunci untuk mencari 'Label.resx', tetapi jika Anda menyetelnya melalui 'DisplayLabel', ia akan menggunakannya.

YYFish
sumber
6

Anda dapat membuat subkelas DisplayNameAttribute untuk menyediakan i18n, dengan mengganti salah satu metode. Seperti itu. edit: Anda mungkin harus puas menggunakan konstanta untuk kunci.

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

Saya menggunakan cara ini untuk memecahkan kasus saya

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

Dengan kode

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

Nah, perakitannya Microsoft.VisualStudio.Modeling.Sdk.dll. yang disertakan dengan Visual Studio SDK (With Visual Studio Integration Package).

Tapi itu akan digunakan dengan cara yang hampir sama seperti atribut Anda; tidak ada cara untuk menggunakan resource tipe kuat dalam atribut hanya karena atribut tidak konstan.

konfigurator
sumber
0

Saya minta maaf untuk kode VB.NET, C # saya agak berkarat ... Tapi Anda akan mengerti, bukan?

Pertama-tama, buat kelas baru LocalizedPropertyDescriptor:, yang mewarisi PropertyDescriptor. Ganti DisplayNameproperti seperti ini:

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 adalah ResourceManager dari file resource yang berisi terjemahan Anda.

Selanjutnya, implementasikan ICustomTypeDescriptordi kelas dengan properti yang dilokalkan, dan ganti GetPropertiesmetode:

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

Anda sekarang dapat menggunakan atribut 'DisplayName` untuk menyimpan referensi ke nilai dalam file sumber daya ...

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

prop_description adalah kunci dalam file sumber daya.

Vincent Van Den Berghe
sumber
Bagian pertama dari solusi Anda adalah apa yang saya lakukan ... sampai saya harus menyelesaikan pertanyaan "apa itu Some.ResourceManager?" pertanyaan. Apakah saya harus memberikan string literal kedua seperti "MyAssembly.Resources.Resource"? terlalu berbahaya! Adapun bagian kedua (ICustomTypeDescriptor) menurut saya ini tidak benar-benar berguna
PowerKiKi
Solusi Marc Gravell adalah cara yang harus ditempuh jika Anda tidak memerlukan apa pun selain DisplayName yang diterjemahkan - Saya juga menggunakan deskriptor khusus untuk hal-hal lain, dan ini adalah solusi saya. Tidak ada cara untuk melakukan ini tanpa menyediakan semacam kunci.
Vincent Van Den Berghe