【问题标题】:Changing the PropertyGrid's Description and Category attributes at runtiome在运行时更改 PropertyGrid 描述和类别属性
【发布时间】:2014-08-19 13:36:30
【问题描述】:

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

我已经尝试了很多天来本地化 PropertyGrid。但我无法在运行时更改属性 DescriptionCategory。更改 DisplayName 效果很好。

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

这是我想在 PropertyGrid 中显示的类:

class Person
{
    int age;

    public Person()
    {
        age = 10;
    }

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

在表单的构造函数中;我创建了 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 查看 Attribute 对象(DescriptionAttributeCategoryAttribute) 有一个成员发生了变化。

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

我真的很想得到一些帮助来解决这个问题。请问有什么建议或解决办法吗?

注 1: 我不希望任何回应说这是不可能的,并且只能在设计时设置属性。那不是真的!这个来自 CodeProject.com 的article 展示了如何本地化 PropertyGrid 并在运行时更改属性的示例。不幸的是,我在为解决此问题所需的那些部分确定示例的范围时遇到问题。

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

最好的问候, /Mc_Topaz

【问题讨论】:

    标签: c# .net propertygrid


    【解决方案1】:

    这是 Globalized-property-grid

    的好文章

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

    这里是三个步骤:

    1. 继承自GlobalizedObject
    2. 给Person同名资源文件(eg.Person.zh-cn.resx)
    3. 更改要显示的线程的头像。

    你可以试试,祝你好运!

    【讨论】:

    • 谢谢,但我不认为我可以使用那个解决方案... :( 让我解释一下:本地化位于语言文件中。这些文件是在应用程序启动期间读取的。每种语言被加载到 Dictionary 中。通过对代码中的索引进行硬编码,从字典中返回当前语言的正确字符串。不幸的是,由于其他好处,我必须使用该解决方案。
    • 继续评论:这就是我在我的三个示例方法(SetDisplayName、SetDescription 和 SetCategory)中使用字符串来更改属性的方式。如果可能的话,我想继续使用该解决方案。或者有没有办法在运行时为每种语言创建一个资源文件并用我从字典中获得的本地化填充它? /Mc_Topaz
    【解决方案2】:

    您可以做的是重用我在此处对这个问题的回答中描述的 DynamicTypeDescriptor 类: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();
      }
    

    【讨论】:

    • 谢谢!我用 xudong125 解决方案解决了这个问题。你的解决方案看起来很相似,所以我猜它基本上是一样的。我已将 xudong125 答案标记为解决方案,因此任何其他人都可以从它以及您的解决方案中受益。
    • 我已经尝试过了,但是当我执行你的代码时没有任何反应。就好像它根本不存在一样。你知道为什么会这样吗?更改属性后我没有忘记刷新网格。
    【解决方案3】:

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

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

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

    /Mc_Topaz

    【讨论】:

      【解决方案4】:

      我有不同的理由来更改属性描述,并找到了一个相当粗略但更简单的解决方案,用于更正网格中显示的描述。对我来说好处是属性网格中显示的对象的类需要的更改要少得多。

      我的情况如下:我有两个布尔属性 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 将始终被调用两次,这是低效的。

      【讨论】:

        【解决方案5】:

        huoxudong125的解决方案是one possible solution。我想提供另一个(但不讨论如何在运行时改变文化的东西——你可以自己谷歌搜索;))。对于我自己,我开始使用localized subclasses for DisplayName, Description and Category

        正如我们所知,PropertyGrid 更新时,DisplayName 确实会更新为当前 culter,但 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 的实现似乎在某个地方缓存了创建的PropertyDescriptors

        第二种解决方案

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

        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 存储在哪里以及存储多长时间,所以我创建了一个HashSetWeakReference(基于WeakHashTable)来存储相应的引用。

        用法

        只需将DescriptionProvider 类附加到PropertyGrid 中显示的类:

        [LocalizedDescription(nameof(MyClass), typeof(MyTextResource))]
        [TypeDescriptionProvider(typeof(ForcedGlobalizationTypeDescriptor<MyClass>))]
        class MyClass 
        {
            // ...
        

        LocalizedDescription 的工作方式取决于您...

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-12-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-23
          • 1970-01-01
          相关资源
          最近更新 更多