在运行时更改PropertyGrid's Description和Category属性

本文关键字:Description Category 属性 运行时 PropertyGrid | 更新日期: 2023-09-27 18:10:36

我正在开发一个使用PropertyGrid的业务应用程序。我的项目负责人希望我在运行时本地化PropertyGrid中的文本。华友世纪! !讽刺

我尝试了很多天来本地化PropertyGrid。但是我在运行时更改属性DescriptionCategory时遇到了麻烦。更改DisplayName可以正常工作。

我做了一个简单的例子来重现这个问题:创建一个Windows窗体应用程序,并从工具箱添加一个PropertyGrid和一个按钮默认设置。

下面是我想在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。在每个方法的最后一行之后,您可以使用智能感知来查看属性对象(DescriptionAttributeCategoryAttribute)有一个已更改的成员。

运行这三个方法并重置PropertyGrid(参见button1单击方法)后;PropertyGrid 只改变了DisplayName属性DescriptionCategory属性保持不变。

我真的需要一些帮助来解决这个问题。有什么建议或解决方案吗?

注1:我不希望任何回复说这是不可能的,属性只能在设计时设置。那不是真的!这篇来自CodeProject.com的文章展示了一个如何本地化PropertyGrid并在运行时更改属性的示例。不幸的是,我有问题的范围为那些部分的例子,我需要解决这个问题。

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

最诚挚的问候,/Mc_Topaz

在运行时更改PropertyGrid's Description和Category属性

这是一篇很好的文章Globalized-property-grid

您可以为Person提供许多资源文件,然后属性网格将进行本地化。

有三个步骤:

  1. 继承自GlobalizedObject
  2. 为Person提供同名的资源文件(eg.Person.zh-cn.resx)
  3. 改变你想要显示的线程文化

你可以试试,祝你好运!

您可以做的是重用DynamicTypeDescriptor类在我的回答中描述的这个问题在这里SO: PropertyGrid未找到实体框架创建属性,如何找到它?

:

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

xudong125答案解决问题!我设法通过使用静态源来解决资源文件解决方案。这太复杂了,解释不清楚。

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

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

/Mc_Topaz

我有一个不同的原因来更改属性描述,并找到了一个相当粗糙但简单得多的解决方案来纠正网格中显示的描述。对我来说,优点是在属性网格中显示的对象的类需要更少的更改。

我的情况如下:我有两个布尔属性A和B,其中B只能在A被设置时使用。如果A为False,我想使B为只读,并将其描述设置为"此属性只能在将'A'设置为True时使用"。在对象代码中,我将B的Description属性设置为此消息,类似于Mc_Topaz的做法。

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

    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将总是被调用两次,这是低效的。

huoxuddong125的解决方案是一种可能的解决方案。我想提供另一个(但不讨论如何在运行时改变文化的东西-你可以自己谷歌;))。对于我自己,我开始使用本地化子类的DisplayName, Description和Category。

我们知道,当PropertyGrid更新时,DisplayName会更新到当前culter,但DescriptionCategory不会。我认为这是在反思层面上的原因,当PropertyGrid要求分类和描述时。如您所见,这些值在第一次读取时被缓存,但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);
    }
}

这个围绕着"just get it done"…添加一个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实例,而微软的实现似乎会在某个地方缓存创建的PropertyDescriptors

第二方案

为了避免每次都创建实例,我简单地将ForcedGlobalizationTypeDescriptor创建的所有ProperyDescriptor都存储在一个集合中。一旦本地化发生变化,set就会被调用来重置它的条目缓存值:

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方法使用反射将内部字段重置为null,因此在下次调用相应的属性时,将再次读取本地化的值。

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

由于我不知道微软的PropertyDescriptors存储在哪里和多长时间,我用WeakReference(基于WeakHashTable)创建了一个HashSet来存储相应的引用。

使用

只需将DescriptionProvider类附加到PropertyGrid:

所示的类中:
[LocalizedDescription(nameof(MyClass), typeof(MyTextResource))]
[TypeDescriptionProvider(typeof(ForcedGlobalizationTypeDescriptor<MyClass>))]
class MyClass 
{
    // ...

你的LocalizedDescription的工作方式,取决于你…