DisplayNameAttribute的本地化

133

我正在寻找一种本地化在PropertyGrid中显示的属性名称的方法。属性的名称可以使用DisplayNameAttribute属性“覆盖”。不幸的是,属性不能具有非常量表达式。因此,我无法使用强类型资源,例如:

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

我查看了一些建议,发现可以从DisplayNameAttribute继承以使用资源。最终的代码可能如下:

class Foo
{
   [MyLocalizedDisplayAttribute("MyPropertyNameLocalized")] // not strongly typed
   string MyProperty {get; set;}
}
然而,我失去了强类型资源的好处,这绝对不是一件好事。然后我发现了DisplayNameResourceAttribute,这可能是我正在寻找的东西。但它应该在Microsoft.VisualStudio.Modeling.Design命名空间中,我找不到应该添加哪个引用来使用此命名空间。
是否有更简单的方法以良好的方式实现DisplayName本地化?或者是否有一种方法可以使用Microsoft似乎正在为Visual Studio使用的方式?

2
Display(ResourceType=typeof(ResourceStrings),Name="MyProperty")是什么意思?请参考http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.displayattribute.aspx。 - Peter
@Peter 仔细阅读帖子,他想要的是完全相反的,使用 ResourceStrings 和编译时检查而不是硬编码字符串... - Marko
自C# 6开始,您可以使用nameof(Resources.MyPropertyNameLocalized)来保持强类型。 - Jiří Skála
11个回答

124

在 .NET 4 中,存在来自 System.ComponentModel.DataAnnotations 的 Display 属性。它可以用于 MVC 3 的 PropertyGrid

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

这会在你的 MyResources.resx 文件中查找名为 UserName 的资源。


我在找了很久之后才找到这个页面...真是救命稻草。谢谢!对于我来说,在MVC5上运行良好。 - Kris
2
如果编译器抱怨typeof(MyResources),你可能需要将资源文件的访问修饰符设置为Public - thatWiseGuy

83

我们这样做是为了支持多语言的多个属性。我们采用了类似于Microsoft的方法,覆盖基础属性并传递资源名称而不是实际字符串。然后使用该资源名称在DLL资源中进行查找以返回实际字符串。

例如:

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

你可以在实际使用该属性时,将资源名称指定为静态类中的常量。这样,你就可以获得像以下声明一样的内容。
[LocalizedDisplayName(ResourceStrings.MyPropertyName)]
public string MyProperty
{
  get
  {
    ...
  }
}

更新
ResourceStrings 的格式应该类似于以下内容(注意,每个字符串都会引用指定实际字符串的资源名称):

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

1
@Andy:ResourceStrings 中的值必须是常量,如答案所示,不能是属性或只读值。它们必须标记为 const 并引用资源的名称,否则会出现错误。 - Jeff Yates
1
回答了自己的问题,关键在于你放置了Resources.ResourceManager。在我的情况下,resx文件是公共的resx生成文件,所以应该是[MyNamespace].[MyResourceFile].ResourceManager.GetString("MyString"); - Tristan Warner-Smith
1
没问题。我很高兴你找到了问题的根源。如果需要帮助,我很乐意提供。 - Jeff Yates
资源查找的区域设置来自CultureInfo.CurrentUICulture。资源管理器已经设置了相应的文化。 - Jeff Yates
.NET 提供了本地化 WinForms 的内置支持。只需在表单设计器上将 Localizable 设置为 true,并告诉它当前布局所属的文化即可。在 WPF 中,您需要查找 LocBaml。 - Jeff Yates
显示剩余9条评论

42

这是我最终在一个单独的程序集中(我的程序集名为"Common")得出的解决方案:

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

使用代码查找资源:

  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
  }

典型用法如下:

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

我在使用文字字符串作为资源键时,感觉很丑陋。使用常量意味着需要修改Resources.Designer.cs,这可能不是一个好主意。

结论:虽然我对此不太满意,但对于无法提供有用功能的 Microsoft,我更不满意。


非常有用。感谢您。未来,我希望微软能够提供一种漂亮的解决方案,以强类型引用资源。 - Johnny Oshika
呀,这个字符串的东西真的很糟糕 :( 如果你能够获取使用属性的属性名称,你就可以按照约定优于配置的方式来做,但这似乎是不可能的。关心“强类型”枚举,你可以使用,但也不是真正可维护的 :/ - Rookian
那是一个好的解决方案。我只是不会遍历ResourceManager属性的集合。相反,你可以直接从参数提供的类型中获取属性:PropertyInfo property = resourceManagerProvider.GetProperty(resourceKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - Maksymilian Majer
1
将此与 @zielu1 的 T4 模板相结合,自动生成资源键,你就有了一个值得称赞的胜者! - David Keaveny

23

使用 C# 6 中的 Display 特性(来自 System.ComponentModel.DataAnnotations)和 nameof() 表达式,您可以获得本地化且强类型的解决方案。

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

3
在这个例子中,“MyResources”是什么?是一个强类型的resx文件吗?还是一个自定义类? - Greg

14

你可以使用 T4 生成常量。我写了一个:

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

输出会是什么样子? - irfandar

9

这是一个老问题,但我认为这是一个非常普遍的问题,在MVC 3中,这是我的解决方案。

首先,需要一个T4模板来生成常量,以避免恶意字符串。我们有一个资源文件'Labels.resx'保存所有标签字符串。因此,T4模板直接使用资源文件,

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

然后,创建了一个扩展方法来本地化“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;
        }
    }
}

'DisplayName' 属性被替换为 'DisplayLabel' 属性,以便自动从 '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;
            }
        }
    }
}

经过所有的准备工作,现在是时候接触那些默认验证属性了。我将以“必填”属性为例。

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

    }
}

现在,我们可以将这些属性应用于我们的模型中。
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; }
    }
}

默认情况下,属性名称用作查找“Label.resx”的键,但如果您通过“DisplayLabel”设置它,则将使用该设置代替。


6
您可以通过继承DisplayNameAttribute类来提供国际化支持,只需覆盖其中的一个方法即可。如下所示。编辑:您可能需要使用常量作为键。
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
我用这个方法解决了我的问题。
[LocalizedDisplayName("Age", NameResourceType = typeof(RegistrationResources))]
 public bool Age { get; set; }

使用这段代码

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

嗯,汇编是 Microsoft.VisualStudio.Modeling.Sdk.dll。 它是随 Visual Studio SDK(带有 Visual Studio 集成包)提供的。

但它与您的属性基本上以相同的方式使用;由于它们不是常量,因此没有办法在属性中使用强类型资源。


1

显示名称:

    public sealed class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDisplayNameAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string DisplayName
    {
        get
        {
            var baseName = BaseName;
            var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

            if (baseName.IsNullOrEmpty())
            {
                // ReSharper disable once PossibleNullReferenceException
                baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
            }

            // ReSharper disable once AssignNullToNotNullAttribute
            var res = new ResourceManager(baseName, assembly);

            var str = res.GetString(ResourceKey);

            return string.IsNullOrEmpty(str)
                ? $"[[{ResourceKey}]]"
                : str;
        }
    }
}

描述:

public sealed class LocalizedDescriptionAttribute : DescriptionAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedDescriptionAttribute(string resourceKey)
    {
        ResourceKey = resourceKey;
    }

    public override string Description
    {
        get
        {
                var baseName = BaseName;
                var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

                if (baseName.IsNullOrEmpty())
                {
                    // ReSharper disable once PossibleNullReferenceException
                    baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
                }

                // ReSharper disable once AssignNullToNotNullAttribute
                var res = new ResourceManager(baseName, assembly);
                var str = res.GetString(ResourceKey);
                
                return string.IsNullOrEmpty(str)
                    ? $"[[{ResourceKey}]]"
                    : str;
        }
    }
}

类别(PropertyGrid):

    public sealed class LocalizedCategoryAttribute : CategoryAttribute
{
    public string ResourceKey { get; }
    public string BaseName { get; set; }
    public Type ResourceType { get; set; }

    public LocalizedCategoryAttribute(string resourceKey) 
        : base(resourceKey)
    {
        ResourceKey = resourceKey;
    }

    protected override string GetLocalizedString(string resourceKey)
    {
        var baseName = BaseName;
        var assembly = ResourceType?.Assembly ?? Assembly.GetEntryAssembly();

        if (baseName.IsNullOrEmpty())
        {
            // ReSharper disable once PossibleNullReferenceException
            baseName = $"{(ResourceType != null ? ResourceType.Namespace : assembly.GetName().Name)}.Resources";
        }

        // ReSharper disable once AssignNullToNotNullAttribute
        var res = new ResourceManager(baseName, assembly);
        var str = res.GetString(resourceKey);

        return string.IsNullOrEmpty(str)
            ? $"[[{ResourceKey}]]"
            : str;
    }
}

示例:

[LocalizedDisplayName("ResourceKey", ResourceType = typeof(RE))]

这里的"RE"指的是包含您的资源文件(如"Resources.de.resx"或"Resources.en.resx")的程序集中的类。

适用于枚举和属性。

干杯!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接