在运行时更改Property Grid的Description和Category属性

时间:2014-06-28 16:30:54

标签: c# .net propertygrid

我正在使用PropertyGrid的业务应用程序。我的项目负责人希望我在运行时本地化PropertyGrid中的文本。欢呼!!! 讽刺

我已经尝试了很多天来本地化PropertyGrid。但是我在运行时更改属性描述类别时遇到问题。更改 DisplayName 可以正常工作。

我做了一个简单的例子来重现这个问题:创建一个 Windows窗体应用程序,然后从 ToolBox 添加一个 PropertyGrid 和一个< strong>按钮,默认设置。

以下是我想在PropertyGrid中显示的类:

class Person
{
    int age;

    public Person()
    {
        age = 10;
    }

    [Description("Person's age"), DisplayName("Age"), Category("Fact")]
    public int Age
    {
        get { return age; }
    }
}

在Form的构造函数中;我创建了Person对象并将其显示在PropertyGrid中。

    public Form1()
    {
        InitializeComponent();
        propertyGrid1.SelectedObject = new Person();
    }

该按钮用于在运行时更改DisplayName,Description和Category属性。

    private void button1_Click(object sender, EventArgs e)
    {
        SetDisplayName();
        SetDescription();
        SetCategory();

        propertyGrid1.SelectedObject = propertyGrid1.SelectedObject;  // Reset the PropertyGrid
    }

SetDisplayName ()方法工作正常,实际上会在运行时更改属性的DisplayName!

    private void SetDisplayName()
    {
        Person person = propertyGrid1.SelectedObject as Person;
        PropertyDescriptor descriptor = TypeDescriptor.GetProperties(person)["Age"];
        DisplayNameAttribute attribute = descriptor.Attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
        FieldInfo field = attribute.GetType().GetField("_displayName", BindingFlags.NonPublic | BindingFlags.Instance);
        field.SetValue(attribute, "The age");
    }

SetDescription ()和 SetCategory ()方法几乎与 SetDisplayName ()方法完全相同,但某些类型更改和字符串除外访问每个属性的私有成员。

    private void SetDescription()
    {
        Person person = propertyGrid1.SelectedObject as Person;
        PropertyDescriptor descriptor = TypeDescriptor.GetProperties(person)["Age"];
        DescriptionAttribute attribute = descriptor.Attributes[typeof(DescriptionAttribute)] as DescriptionAttribute;
        FieldInfo field = attribute.GetType().GetField("description", BindingFlags.NonPublic |BindingFlags.Instance);
        field.SetValue(attribute, "Age of the person");
    }

    private void SetCategory()
    {
        Person person = propertyGrid1.SelectedObject as Person;
        PropertyDescriptor descriptor = TypeDescriptor.GetProperties(person)["Age"];
        CategoryAttribute attribute = descriptor.Attributes[typeof(CategoryAttribute)] as CategoryAttribute;
        FieldInfo[] fields = attribute.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
        FieldInfo field = attribute.GetType().GetField("categoryValue", BindingFlags.NonPublic | BindingFlags.Instance);
        field.SetValue(attribute, "Info");
    }

SetDescription ()和 SetCategory ()方法都可以编译并运行,但不会影响ProperytGrid。在每个方法的最后一行之后,您可以使用 IntelliSense 来查看属性对象( DescriptionAttribute CategoryAttribute >)有一个已经改变的成员。

运行这三个方法并重置PropertyGrid后(参见button1 click方法); PropertyGrid 仅更改了DisplayName属性描述类别属性保持不变。

我真的希望得到一些帮助来解决这个问题。请提出任何建议或解决方案?

注1: 我不希望任何回复说这是不可能的,属性只能在设计时设置。那不是真的! CodeProject.com的这个article显示了如何本地化PropertyGrid以及在运行时更改属性的示例。不幸的是,我遇到了解决这个问题所需的部分示例问题。

注2: 我想避免使用资源文件。这是由于本地化位于不同的语言文件中。每个文件都包含一堆索引,每个索引都有一个字符串值。所有索引和字符串值都加载到Dictionary对象中。要访问字符串,索引用于访问它。不幸的是,我最常使用这个解决方案。

祝你好运, / Mc_Topaz

5 个答案:

答案 0 :(得分:2)

你可以做的是重新使用我在这个问题的答案中描述的DynamicTypeDescriptor类,在SO:PropertyGrid Browsable not found for entity framework created property, how to find it?

像这样:

  public Form1()
  {
      InitializeComponent();

      Person p = new Person();

      DynamicTypeDescriptor dt = new DynamicTypeDescriptor(typeof(Person));
      propertyGrid1.SelectedObject = dt.FromComponent(p);
  }

  private void button1_Click(object sender, EventArgs e)
  {
      DynamicTypeDescriptor dt = (DynamicTypeDescriptor)propertyGrid1.SelectedObject;
      DynamicTypeDescriptor.DynamicProperty dtp = (DynamicTypeDescriptor.DynamicProperty)dt.Properties["Age"];

      dtp.SetDisplayName("The age");
      dtp.SetDescription("Age of the person");
      dtp.SetCategory("Info");

      propertyGrid1.Refresh();
  }

答案 1 :(得分:1)

以下是 Globalized-property-grid

的好文章

您可以为Person提供许多资源文件,而不是自动网格本地化。

这是三步:

  1. 继承自GlobalizedObject
  2. 为具有相同名称的Person提供资源文件(例如.Person.zh-cn.resx)
  3. 更改要显示的线索的剪裁。
  4. 你可以尝试,祝你好运!

答案 2 :(得分:0)

xudong125答案解决了这个问题!我设法通过使用静态源来解决资源文件解决方案。解释起来很复杂......

但是创建实现ICustomTypeDescriptor和PropertyDescriptor的类是可行的方法。

关键是覆盖PropertyDescriptor类的子类中的DisplayName,Description和Category方法。在这些重写方法中,我指向一个公共静态源并设法获取我想要的字符串。

/ Mc_Topaz

答案 3 :(得分:0)

我有一个不同的理由来更改属性描述,并找到一个相当粗略但更简单的解决方案,用于纠正网格中显示的描述。对我而言,优势在于属性网格中显示的对象类需要更少的更改。

我的情况如下:我有两个布尔属性A和B,其中只有在设置A时才能使用B.如果A为False,我想将B设为只读并将其描述设置为&#34; 此属性只能在设置&#39; A&#39;真的&#34;。在目标代码中,我将B的Description属性设置为此消息,类似于Mc_Topaz的做法。

将所选属性显示的描述设置为正确的当前值,我使用名为SelectedGridItemChanged的PropertyGrid的以下pgConfig事件处理程序:< / p>

    private void pgConfig_SelectedGridItemChanged(object sender, SelectedGridItemChangedEventArgs e)
    {
      GridItem giSelected = e.NewSelection;
      if ((giSelected != null) && (giSelected.PropertyDescriptor != null))
      {
        string sDescription = GetCurrentPropertyDescription(giSelected.PropertyDescriptor.Name);
        if ((sDescription != null) && (sDescription != giSelected.PropertyDescriptor.Description))
        {
          MethodInfo miSetStatusBox = pgConfig.GetType().GetMethod("SetStatusBox", BindingFlags.NonPublic | BindingFlags.Instance);
          if (miSetStatusBox != null)
            miSetStatusBox.Invoke(pgConfig, new object[] { giSelected.PropertyDescriptor.DisplayName, sDescription });
        }
      }
    }

在代码示例中,GetCurrentPropertyDescription是一个私有函数,它检索属性网格中显示的对象的当前属性描述(在我的例子中为m_da.Config):

    private string GetCurrentPropertyDescription(string sPropertyName)
    {
      PropertyDescriptor oPropDescriptor = TypeDescriptor.GetProperties(m_da.Config.GetType())[sPropertyName];
      if (oPropDescriptor != null)
      {
        DescriptionAttribute oDescriptionAttr = (DescriptionAttribute)oPropDescriptor.Attributes[typeof(DescriptionAttribute)];
        if (oDescriptionAttr != null)
          return oDescriptionAttr.Description;
      }
      return null;
    }

如果你想要完全全球化,我的解决方案不如huoxudong125那么合适,但如果你只想要一些属性的动态描述而不改变所显示对象的继承,那么这是一个选项。

我的方法的缺点是网格的底层缓存PropertyDescriptor对象永远不会更新,因此如果选择了具有更改描述的属性,则SetStatusBox将始终被调用两次,这是低效的。

答案 4 :(得分:0)

huoxudong125的解决方案是one possible solution。我想提供另一个(但不谈论如何在运行时更改文化内容-您可以自己搜索;))。对于我自己,我从使用localized subclasses for DisplayName, Description and Category开始。

我们知道,DisplayName确实会在更新PropertyGrid时更新到当前的培养者,但是DescriptionCategory不会更新。我认为这样做的原因是当PropertyGrid请求categorydescription时的反射级别。如您所见,这些值在第一次读取时被缓存,而displayName则没有。为了解决这个问题,我开发了两个解决方案(第一个解决方案很奇怪,我不知道为什么这对我自己有效。)。两者都围绕着额外的TypeDescriptionProvider

基地

首先使用自定义TypeDescriptionProvider,该自定义变量可以通过属性绑定到任何类:

internal class UpdateableGlobalizationDescriptionProvider<TTargetType> : TypeDescriptionProvider
{
    private static TypeDescriptionProvider defaultTypeProvider = TypeDescriptor.GetProvider(typeof(TTargetType));

    public UpdateableGlobalizationDescriptionProvider() : base(defaultTypeProvider) { }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        var result = base.GetTypeDescriptor(objectType, instance);
        return new ForcedGlobalizationTypeDescriptor(result);
    }
}

第一个解决方案

这是围绕“只要完成就行”的内容...添加一个CustomTypeDescriptor实现,该实现将原始PropertyDescriptor实例与自定义实例包装在一起:

internal class ForcedGlobalizationTypeDescriptor : CustomTypeDescriptor
{
    readonly ICustomTypeDescriptor inner;

    public ForcedGlobalizationTypeDescriptor(ICustomTypeDescriptor typeDescriptor) : base(typeDescriptor)
    {
        inner = typeDescriptor;
    }

    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        // First solution
        var result = base.GetProperties(attributes);
        var transformed = result.OfType<PropertyDescriptor>().Select(d => new ForcedPropertyDescriptor(d)).ToArray();
        return new PropertyDescriptorCollection(transformed);
    }
}

,在PropertyDesciptor外部,仅返回包装的PropertyDescriptor中的值。我发现的PropertyDescriptor的最简单实现-请告诉我,如果有一个更短的实现。

internal class ForcedPropertyDescriptor : PropertyDescriptor
{
    private PropertyDescriptor innerDescriptor;
    public ForcedPropertyDescriptor(PropertyDescriptor descriptor) : base(descriptor)
    {
        innerDescriptor = descriptor;
    }
    // important:
    public override string Category => base.Category;
    public override string Description => base.Description;

    public override Type ComponentType => innerDescriptor.ComponentType;
    public override bool IsReadOnly => innerDescriptor.IsReadOnly;
    public override Type PropertyType => innerDescriptor.PropertyType;
    public override bool CanResetValue(object component) => innerDescriptor.CanResetValue(component);
    public override object GetValue(object component) => innerDescriptor.GetValue(component);
    public override void ResetValue(object component) => innerDescriptor.ResetValue(component);
    public override void SetValue(object component, object value) => innerDescriptor.SetValue(component, value);
    public override bool ShouldSerializeValue(object component) => innerDescriptor.ShouldSerializeValue(component);
}

我认为它是可行的,因为对于每次阅读类别或描述,都会有一个新的ForcedPropertyDescriptor,但尚未缓存该值。同时,这也是一个缺点:对于CategoryDescription属性的每个请求,都会创建一个ForcedPropertyDescriptor的新实例,而Microsoft的实现似乎会缓存残酷的{{1} } somewehere。

第二个解决方案

为避免每次都创建该实例,我只是将PropertyDescriptors创建的所有看到的ProperyDescriptor存储在一个集中。并且,一旦出现本地化更改,就会调用该集合以重置其项目的缓存值:

ForcedGlobalizationTypeDescriptor

internal class DescriptorReset { public static DescriptorReset Default { get; } = new DescriptorReset(); private HashSet<MemberDescriptor> descriptors = new HashSet<MemberDescriptor>(); public void Add(MemberDescriptor descriptor) { descriptors.Add(descriptor); } private void RunUpdate() { if (descriptors.Count == 0) return; FieldInfo category, description; category = typeof(MemberDescriptor).GetField(nameof(category), BindingFlags.NonPublic | BindingFlags.Instance); description = typeof(MemberDescriptor).GetField(nameof(description), BindingFlags.NonPublic | BindingFlags.Instance); foreach (var descriptor in descriptors) { category.SetValue(descriptor, null); description.SetValue(descriptor, null); } } } 方法使用反射将内部字段重置为空,因此在下一次调用相应属性时,将再次读取本地化的值。

您现在所需要的就是在适当的时候调用RunUpdate的妙处。对于我自己,我的核心解决方案中有一个类,该类提供了一种设置新的CultureInfo的方法。调用时,它将默认的ui区域性和默认的区域性设置为新的RunUpdate并引发两个事件:第一个事件是更新所有内部逻辑,第二个事件是针对基于内部逻辑的所有内容的,例如GUI

并且由于我不知道Microsoft CultureInfo的存储位置和存储时间,因此我使用PropertyDescriptors(基于WeakHashTable)创建了一个HashSet,以存储相应的引用。

用法

只需将WeakReference类附加到DescriptionProvider中显示的类中即可:

PropertyGrid

[LocalizedDescription(nameof(MyClass), typeof(MyTextResource))] [TypeDescriptionProvider(typeof(ForcedGlobalizationTypeDescriptor<MyClass>))] class MyClass { // ... 的工作方式取决于您...