Click here to Skip to main content
16,020,741 members
Articles / Programming Languages / C#

Dynamic Type Description Framework for PropertyGrid

Rate me:
Please Sign up or sign in to vote.
4.96/5 (36 votes)
25 Feb 2013CPOL19 min read 118.4K   4.5K   91   57
A framework that allows you to customize your class for PropertyGrid at design-time and/or run-time. Using this framework your can control what properties PropertyGrid shows, how the properties are shown, and when it shows them.

Contents

  1. Introduction
  2. Using the Code
    1. Altering properties at run-time
    2. Hiding/removing properties at run-time
    3. Creating new properties at run-time
    4. Displaying images for property values at run-time
    5. Displaying state image of properties
    6. Providing standard values for properties
    7. Customizing enumeration (enum) type properties
    8. Customizing boolean type properties
    9. Creating child properties from IEnumeration properties
    10. Sorting properties
    11. Localizing categories, properties, boolean, and enumeration
    12. Working with ValueType objects
  3. Point of interest
  4. References

1 Introduction

This article describes a framework that allows you to customize your class for PropertyGrid at design-time and/or run-time. Using this framework your can control what properties PropertyGrid shows, how the properties are shown, and when it shows them. This framework adheres completely with the .NET Component Model architecture while providing you with many rich features. The design makes no distinction between run-time and design-time, meaning what you can do at design-time, you can also do at run-time and vice-versa.

This article assumes you have some knowledge of the following topics:

It is okay if you aren't familiar with these classes, in that case I would highly recommend you read the first two articles in the References section. This will bring you up to speed.

2 Using the code

The sample source code provided in this article contains two C#, VS2010 projects:

Let's consider this simple source code.

C#
  1  using System;
  2  using System.Windows.Forms;
  3  using System.Drawing.Design;
  4  using Dyn = DynamicTypeDescriptor;
  5  using Scm = System.ComponentModel;
  6  using DynamicTypeDescriptor;
  7  namespace DynamicTypeDescriptorApp
  8  {
  9    public partial class Form2 : Form
 10    {
 11      public Form2()
 12      {
 13        InitializeComponent( );
 14      }
 15      private void Form2_Load( object sender, EventArgs e )
 16      {
 17        propertyGrid1.PropertySort = PropertySort.CategorizedAlphabetical;    
 18        MyClassA mcA = new MyClassA( );
 19        Dyn.TypeDescriptor.IntallTypeDescriptor( mcA );
 20        this.propertyGrid1.SelectedObject = mcA ;
 21      }
 22      private void button1_Click( object sender, EventArgs e )
 23      {
 24        // write code here to modify the class at run-time
 25      }
 26    }
 27    public class MyClassA
 28    {
 29      public MyClassA() {    }
 30  
 31      private int m_PropA = 3;
 32      public int PropA
 33      {
 34        get{return m_PropA;}
 35        set{m_PropA = value;}
 36      }
 37      private bool m_PropB = false;
 38      public bool PropB
 39      {
 40        get{return m_PropB;}
 41        set{m_PropB = value;}
 42      }
 43    }
 44  }
Listing 1

Figure 1

Listing 1 shows a very simple Form with a very simple class MyClassA. An instance of this class is selected into a PropertyGrid. The output is shown in Figure 1.

In line 4, I have declared an alias Dyn for namespace DynamicTypeDescriptor. All classes that make up this solution are in this namespace. In line 5, I have declared another alias Scm for namespace System.ComponentModel. I will be using these aliases throughout this article.

Line 19 is the most important line of code. You must do this before setting the object to the PropertyGrid.SelectedObject property for this framework to work. Note the button1_Click event handler is empty at this point. We will write code in this handler that will demonstrate the different features of this framework later in this article. We will be using Listing 1 as a template.

2.1 Altering properties at run-time

Let's fill in the button1_Click event handler with some code:

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.DisplayNameAttribute("New PropA"), true);
  6    pd.Attributes.Add(new Scm.CategoryAttribute("New CategoryA"), true);
  7    pd.Attributes.Add(new Scm.DefaultValueAttribute(3), true);
  8    pd.Attributes.Add(new Scm.DescriptionAttribute("Description of PropA"), true);
  9    propertyGrid1.Refresh( );
 10  }
Listing 2

Figure 2

Now if you invoke the button1_Click event handler, it will rename PropA to "New PropA", put it under a category "New CategoryA", set the default value to 3, and set its description to "Description of PropA". Figure 2 shows the output after Listing 2 has executed.

Line 3 executes successfully because of line 19 in Listing 1. Line 4 retrieves Dyn.PropertyDescriptor using the property name. Lines 5 through 8 add some attributes to the property. Dyn.PropertyDescriptor.Attributes.Add takes an Attribute as first argument and a bool as the second argument. Passing true to the second argument will remove any previously added attributes of similar type before adding the new one. Finally, line 9 refreshes the PropertyGrid to show the changes.

If you compare Figure 2 with Figure 1, you will notice several differences. One difference I would like to point is that the value 3 is shown in bold font in Figure 1 while it is shown in normal font in Figure 2. That is because I have assigned a default value for PropA (see line 7 of Listing 2). Since the value of the property is the same as the default value, PropertyGrid shows the value in normal font. Now if you change the value to something other than 3 in the PropertyGrid, it will show the value in bold font again. If there is no default value for a property, PropertyGrid always shows the value in bold font, which is the case for PropB.

From Listing 2, you can see this solution allows you to alter the attributes of a property at run-time. This opens up a door that will allow us to do all sorts customization of classes and properties at run-time.

2.2 Hiding/removing properties at run-time

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.BrowsableAttribute(false), true);
  6    propertyGrid1.Refresh( );
  7  }
Listing 3

Figure 3
C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.CustomTypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    td.GetProperties( ).Remove(pd);
  6    propertyGrid1.Refresh( );
  7  }
Listing 4

Both Listing 3 and Listing 4 produce Figure 3. In Listing 3, it simply hides property PropA, which can later be shown again if you wish by adding a new Scm.BrowsableAttribute(true). In Listing 4, it removes the property PropA permanently.

2.3 Creating new properties at run-time

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = new Dyn.PropertyDescriptor(propertyGrid1.SelectedObject.GetType( ), 
  5                    "PropC",
  6                    typeof(string), "Hello world",
  7                    new Scm.BrowsableAttribute(true), 
  8                    new Scm.DisplayNameAttribute("Property C"), 
  9                    new Scm.DescriptionAttribute("This property was created on-the-fly."),
 10                    new Scm.DefaultValueAttribute("Hello world"));
 11    pd.AddValueChanged(propertyGrid1.SelectedObject, new EventHandler(this.OnPropCChanged));
 12    td.GetProperties( ).Add(pd);
 13    propertyGrid1.Refresh( );
 14  }
 15  private void OnPropCChanged( object sender, EventArgs e )
 16  {
 17    Dyn.PropertyDescriptor pd = 
 18       Dyn.TypeDescriptor.GetTypeDescriptor(sender).GetProperties( ).Find("PropC", true)
 19             as Dyn.PropertyDescriptor;
 20    string sNewValue = pd.GetValue(sender).ToString();
 21    MessageBox.Show("New value is: " + sNewValue);
 22  }
Listing 5

Figure 4

Listing 5 creates a new property named PropC. Figure 4 shows the new property.

When you create a property on-the-fly, you would also like to get notification when the value of the property changes. This is done here in line 11. If you change the value of PropC, the OnPropCChanged event handler will be invoked.

2.4 Displaying images for property values at run-time

Here we will again consider the code in Listing 1 and add code in the button1_click event handler, also add a new event handler OnPropAChanged.

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.EditorAttribute(typeof(Dyn.PropertyValuePaintEditor), typeof(UITypeEditor)), true);
  6    pd.AddValueChanged(propertyGrid1.SelectedObject, new EventHandler(this.OnPropAChanged));
  7    pd.SetValue(propertyGrid1.SelectedObject, 1); // set initial value
  8    propertyGrid1.Refresh( );
  9  }
 10  private void OnPropAChanged( object sender, EventArgs e )
 11  {
 12    Dyn.PropertyDescriptor pd =
 13       Dyn.TypeDescriptor.GetTypeDescriptor(sender).GetProperties( ).Find("PropA", true)
 14             as Dyn.PropertyDescriptor;
 15    int nNewValue = (int)cpd.GetValue(sender);
 16    if (nNewValue == 0)
 17    {
 18      pd.ValueImage = CustomTypeDescriptorApp.Properties.Resources.UnhappyFace;
 19    }
 20    else
 21    {
 22      pd.ValueImage = CustomTypeDescriptorApp.Properties.Resources.HappyFace;
 23    }
 24  }
Listing 6

Figure 5

Figure 6

Listing 6 shows how you can represent a property value with a corresponding image. Here, when the value of PropA is 0, it shows an "unhappy face" (Figure 6), otherwise it shows a "happy face" (Figure 5). The images should be 18x11 pixels in size.

The Dyn.PropertyDescriptor.ValueImage property is of type System.Drawing.Image. You must set this property and provide an appropriate editor (see lines 5, 18, and 22) for the image to be drawn.

2.5 Displaying state image of properties

Here we will again consider the code in Listing 1 and add code in the button1_click event handler, also add two new event handlers: OnPropAChanged and StateImageItemClicked.

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    propertyGrid1.Site = td.GetSite( );
  5    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  6    pd.AddValueChanged(propertyGrid1.SelectedObject, new EventHandler(this.OnPropAChanged));
  7    pd.SetValue(propertyGrid1.SelectedObject, 1); // set initial value
  8    propertyGrid1.Refresh( );
  9  }
 10  private void OnPropAChanged( object sender, EventArgs e )
 11  {
 12    Dyn.PropertyDescriptor pd =
 13       Dyn.TypeDescriptor.GetTypeDescriptor(sender).GetProperties( ).Find("PropA", true)
 14             as Dyn.PropertyDescriptor;
 15    int nNewValue = (int)pd.GetValue(sender);
 16    int nCount = Math.Min(nNewValue, 5); // just restrict to max 5 state images
 17    pd.StateItems.Clear( );
 18    for (int i = 0; i < nCount; i++)
 19    {
 20        PropertyValueUIItem pvui =
 21           new PropertyValueUIItem(CustomTypeDescriptorApp.Properties.Resources.ErrorState1,
 22             this.StateImageItemClicked, "Index " + (i + 1).ToString( ) + ". Double-click the icon.");        
 23        pd.StateItems.Add(pvui);
 24  
 25    }
 26  }
 27  private void StateImageItemClicked( Scm.ITypeDescriptorContext context,
 28                             Scm.PropertyDescriptor propDesc, PropertyValueUIItem item )
 29  {
 30    string sMsg = "State icon clicked for property '" + propDesc.DisplayName + "'.";
 31    MessageBox.Show(sMsg.ToString( ));
 32  }
Listing 7

Figure 7

Figure 8

Listing 7 shows how you can add one or more state images to a property. The images should be 8x8 pixels in size. Each state image can also have its own tool-tip. When you move mouse over to one of this images, the tool-tip will show up for that image. If you double-click on one of these images, it invokes the associated event handler (see lines 22 and 27). In this example, I restricted the maximum number images it should display, which is 5. The most important line of code here is line 4. It requires that you set a custom Scm.ISite interface to the PropertyGrid.Site property. This framework provides you a with custom Scm.ISite implementation. I used two different images for the state images in this example. But you certainly can use different images for each state image.

2.6 Providing standard values for properties

Here we will again consider the code in Listing 1 and add code in the button1_click event handler.

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.TypeConverterAttribute(typeof(Dyn.StandardValuesConverter)), true);
  6  
  7    Dyn.StandardValue sv = null;
  8    sv = new Dyn.StandardValue( 0 );
  9    sv.DisplayName = "Nothing";
 10    sv.Description = "Zero value.";
 11    pd.StatandardValues.Add(sv);
 12  
 13    sv = new Dyn.StandardValue(1);
 14    sv.DisplayName = "One";
 15    sv.Description = "One value.";
 16    pd.StatandardValues.Add(sv);
 17  
 18    sv = new Dyn.StandardValue(2);
 19    sv.DisplayName = "Two";
 20    sv.Description = "Two value.";
 21    sv.Enabled = false;
 22    pd.StatandardValues.Add(sv);
 23  
 24    sv = new Dyn.StandardValue(3);
 25    sv.DisplayName = "Three";
 26    sv.Description = "Three value.";
 27    sv.Visible = false;
 28    pd.StatandardValues.Add(sv);
 29    
 30    propertyGrid1.Refresh( );
 31  }
Listing 8

Figure 9

Figure 10

Listing 8 shows a very important and desirable feature of this framework. Here we added four standard values for PropA representing the values 0..3. Line 4 is the most important line of code here. It adds a special type-converter to the property. After clicking the button "button1", PropA shows Three instead of 3 (see Figure 9). Because 3 corresponds with one of our standard values that we have defined. Now there is a drop-down list for you to choose from. For example, if you choose "Nothing" from the list, it will assign the value 0 to PropA. But on display, it will read "Nothing". If you double-click the label of PropA, the value will be changed to the next value in the list. If the current value is the last value in the list, it will be changed to the first value in the list.

The list is not exclusive. Meaning you can type in a value using the keyboard. Below is a table that shows what happens when you type in some values using the keyboard:

Entered value (using keyboard) 1 0 Two Abc THREE -7
Display value One Nothing Two PropertyGrid will show an error message dialog and then it restore the original value. Because the entered value does not map to any of standard values we have defined, nor does it represent any integer value. Three -7
Assigned value 1 0 2 Value is unchanged. 3 -7

Notice, standard values "Two" and "Three" are not in the list even though "Three" is currently in display (Figure 10). "Two" is not in the list because we have set its Enabled property to false (line 21). Technically, "Three" should have been shown as "inactive". But the drop-down editor we see in Figure 10 is not able to show an item as "inactive", thus it simply does not show the item. And "Three" is not in the list because we have set its Visible to false (line 27). Another disadvantage of this editor is that it cannot display the value of the Dyn.StandardValue.Description property. This editor is provided by the .NET Framework as default. But surely we need a smarter dropdown editor. This framework comes with one. To use the smart editor, insert the following line of code in between lines 5 and 30 of Listing 8:

C#
  1  pd.Attributes.Add(new Scm.EditorAttribute(typeof(Dyn.StandardValuesEditor), typeof(UITypeEditor)), true);
Listing 9

Figure 11

We clearly can see differences between Figure 10 and Figure 11. In Figure 11, we can see that "Three" is not in the list as expected (see Listing 8 line 27), "Two" is disabled as expected (see Listing 8 line 21). Also, there is a "description" area in the drop-down editor that shows the value of the Dyn.StandardValue.Description property.

As I mentioned earlier, the list is not exclusive, but one might prefer an exclusive list of standard values where users must pick a value from the list (not allowing users to enter values using the keyboard). To achieve that you just insert this following line of code in between lines 5 and 30 of Listing 8:

C#
  1  pd.Attributes.Add(new Dyn.ExclusiveStandardValuesAttribute(true), true);
Listing 10

As a side note, a practical scenario for standard values might be a case where you show "customer name" in the list but store the "customer id" in the property.

2.7 Customizing enumeration (enum) type properties

Here we will again consider code in Listing 1, except we will modify MyClassA slightly and add a new type EnumA enumeration.

C#
  1  public class MyClassA
  2  {
  3      public MyClassA(){}
  4  
  5      private EnumA m_PropA = EnumA.Mon;
  6      public EnumA PropA
  7      {
  8        get{return m_PropA;}
  9        set{m_PropA = value;}
 10      }
 11  }
 12  public enum EnumA
 13  {
 14      Mon,
 15      Tue,
 16      Wed,
 17      Thr,
 18  }
Listing 11

Figure 12

Figure 12 shows the output of Listing 11. There is nothing unusual about Listing 11. Now we can customize PropA in a similar way we did it in Listing 8:

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.TypeConverterAttribute(typeof(Dyn.StandardValuesConverter)), true);
  6    pd.Attributes.Add(new Scm.EditorAttribute(typeof(Dyn.StandardValuesEditor), typeof(UITypeEditor)), true);
  7    System.Diagnostics.Debug.Assert(pd.StatandardValues.Count == 4);
  8    System.Diagnostics.Debug.Assert(pd.StatandardValues.IsReadOnly == true);      
  9    foreach (Dyn.StandardValue sv in pd.StatandardValues)
 10    {
 11      EnumA enumVal = (EnumA)Enum.ToObject(typeof(EnumA), sv.Value);
 12      switch(enumVal)
 13      {
 14        case EnumA.Mon:
 15          sv.DisplayName = "Monday";
 16          sv.Description= "Day of the Moon.";
 17          break;
 18        case EnumA.Tue:
 19          sv.DisplayName = "Tuesday";
 20          sv.Description = "Day of the Mars.";
 21          break;
 22        case EnumA.Wed:
 23          sv.DisplayName = "Wednesday";
 24          sv.Description = "Day of the Mercury.";
 25          break;
 26        case EnumA.Thr:
 27          sv.DisplayName = "Thirsday";
 28          sv.Description = "Day of the Jupiter.";
 29          break;
 30      }
 31    }
 32    propertyGrid1.Refresh( );
 33  }
Listing 12

Figure 13

There are several things to note in Listing 8. First we used the same type-converter and same editor as we did in section "2.6 Providing standard values for properties". Line 7 shows that the collection is already populated by this framework and line 8 shows that the collection is now read-only since the elements of the collection are well defined.

While we can customize enum using the Dyn.StandardValuesConverter and Dyn.StandardValuesEditor pair, it is not adequate enough for enumeration with FlagAttribute. This framework comes with another type-converter and an editor pair: Dyn.EnumConverter and Dyn.EnumEditor. These are specialized just for handling enum type properties. It is recommended that you use this pair for all enum type properties. To demonstrate the use of these components, we are going to re-write EnumA and remove all code from the button1_Click event handler.

C#
  1  [Scm.Editor(typeof(Dyn.EnumEditor), typeof(UITypeEditor))]
  2  [Scm.TypeConverter(typeof(Dyn.EnumConverter))]
  3  [Flags]
  4  [Dyn.ExpandEnum(true)]
  5  public enum EnumA
  6  {
  7      [Scm.Description("Event will not reoccure.")]
  8      [Dyn.DisplayName("Not Selected")]
  9      None = 0,
 10  
 11      [Scm.Description("Day of the Moon.")]
 12      [Dyn.DisplayName("Monday")]
 13      Mon = 1,
 14  
 15      [Dyn.DisplayName("Tuesday")]
 16      [Scm.Description("Day of the Mars.")]
 17      Tue = 2,
 18  
 19      // disable this one, just for the sake of it
 20      [Dyn.DisplayName("Wednesday")]
 21      [Scm.Description("Day of the Mercury.")]
 22      [Scm.ReadOnly(true)]
 23      Wed = 4,
 24  
 25      [Dyn.DisplayName("Thursday")]
 26      [Scm.Description("Day of the Jupiter.")]
 27      Thr = 8,
 28  
 29      [Dyn.DisplayName("Friday")]
 30      [Scm.Description("Venus's day.")]
 31      Fri = 16,
 32  
 33      // hide this one, just for the sake of it
 34      [Dyn.DisplayName("Saturday")]
 35      [Scm.Description("Day of the Saturn.")]
 36      [Scm.Browsable(false)]
 37      Sat = 32,
 38      
 39      // hide this one, just for the sake of it
 40      [Dyn.DisplayName("Sunday")]
 41      [Scm.Description("Day of the sun.")]
 42      [Scm.Browsable(false)]
 43      Sun = 64,
 44  
 45      [Dyn.DisplayName("Weekdays")]
 46      [Scm.Description("All days except Saturday and Sunday.")]
 47      Work = Days.Mon | Days.Tue | Days.Wed | Days.Thr | Days.Fri,
 48  
 49      [Dyn.DisplayName("Weekend")]
 50      [Scm.Description("Only Saturday and Sunday.")]
 51      NoWork = Days.Sat | Days.Sun,
 52  }
Listing 13

Figure 14

Now we can directly use the type-converter and the editor on the enum itself (lines 1 and 2). EnumA now has FlagAttribute (line 3). We are using different attributes on each field member as well, similar to what we did for properties. Notice that we are using Dyn.DisplayName instead of Scm.DisplayName. It turned out that Scm.DisplayName is not allowed to be placed on field members while Scm.Description, Scm.Browsable, and Scm.ReadOnly are allowed. It is actually a flaw on Microsoft's part. There is a bug report on this on the MS site.

Figure 14 shows the new editor for editing enum type properties. We can also see in Figure 14 that PropA is displaying each enum field member as a child property. That is because of the attribute Dyn.ExpandEnum(true) (line 4). At this point I would like to thank Sergey Gorbenko for his article where I got this idea from. EnumA.Wed is disabled because of line 22. You can edit the property either using the editor and/or the child properties.

In Listing 13, we added attributes to the enum members at design-time (during coding) and you cannot add/remove attributes at run-time and you cannot alter these attributes at run-time. In situations where you do not have access to the source code of the enum definition, you can use the technique in Listing 12, but always use the Dyn.EnumConverter and Dyn.EnumEditor classes for enum type properties.

2.8 Customizing boolean type properties

Here we will again consider the code in Listing 1, except we will modify MyClassA and add code in button1_Click.

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4    Dyn.PropertyDescriptor pd = td.GetProperties( ).Find("PropA", true) as Dyn.PropertyDescriptor;
  5    pd.Attributes.Add(new Scm.TypeConverterAttribute(typeof(Dyn.BooleanConverter)), true);
  6    pd.Attributes.Add(new Scm.EditorAttribute(typeof(Dyn.StandardValuesEditor), typeof(UITypeEditor)), true);
  7    System.Diagnostics.Debug.Assert(pd.StatandardValues.Count == 2);
  8    System.Diagnostics.Debug.Assert(pd.StatandardValues.IsReadOnly == true);
  9    foreach (Dyn.StandardValue sv in pd.StatandardValues)
 10    {
 11      if ((bool)sv.Value == true)
 12      {
 13        sv.DisplayName = "Yes, that's right.";
 14        sv.Description = "It is positive.";
 15      }
 16      else
 17      {
 18        sv.DisplayName = "No, no way.";
 19        sv.Description = "It is negative.";
 20      }
 21    }
 22    propertyGrid1.Refresh( );
 23  }
 24  public class MyClassA
 25  {
 26      public MyClassA(){}
 27  
 28      private bool m_PropA = true;
 29      public bool PropA
 30      {
 31        get{return m_PropA;}
 32        set{m_PropA = value;}
 33      }
 34  }
Listing 14

Figure 15

Here we are using Dyn.BooleanConverter with Dyn.StandardValuesEditor. Dyn.BooleanConverter is more powerful than Dyn.StandardValueConverter when it comes to Boolean properties. It can support localizing of the display string as we will see later in this article.

2.9 Creating child properties from IEnumeration properties

If the property type is IEnumeration, directly or indirectly, you can show the elements of IEnumeration as child properties.

Here we will again consider the code in Listing 1, except we will modify MyClassA, introduce a new class MyClassB, and remove all code from the button1_Click event handler.

C#
  1  public class MyClassA
  2  {
  3      public MyClassA()
  4      {
  5        ArrayList propC = new ArrayList( );
  6        MyClassB mcB = null;
  7        for (int i = 0; i < 2; i++)
  8        {
  9          mcB = new MyClassB("First " + i.ToString(), "Last " + i.ToString() );
 10          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DisplayNameAttribute("Person " + (i + 1).ToString( )));
 11          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DescriptionAttribute("Desciption of Person " + (i + 1).ToString( )));
 12          m_PropA.Add(mcB);
 13  
 14          mcB = new MyClassB("First " + i.ToString( ), "Last " + i.ToString( ));
 15          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DisplayNameAttribute("Person " + (i + 1).ToString( )));
 16          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DescriptionAttribute("Desciption of Person " + (i + 1).ToString( )));
 17          m_PropB.Add(mcB);
 18          
 19          mcB = new MyClassB("First " + i.ToString( ), "Last " + i.ToString( ));
 20          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DisplayNameAttribute("Person " + (i + 1).ToString( )));
 21          Scm.TypeDescriptor.AddAttributes(mcB, new Scm.DescriptionAttribute("Desciption of Person " + (i + 1).ToString( )));
 22          propC.Add(mcB);
 23        }
 24        m_PropC = (MyClassB[])propC.ToArray(typeof(MyClassB));
 25      }
 26  
 27      private List<MyClassB> m_PropA = new List();
 28      [Scm.TypeConverter(typeof(Dyn.ExpandableIEnumerationConverter))]
 29      public List<MyClassB> PropA
 30      {
 31        get{return m_PropA;}
 32        set{m_PropA = value;}
 33      }
 34      private Collection<MyClassB> m_PropB = new Collection<MyClassB>( );
 35      [Scm.TypeConverter(typeof(Dyn.ExpandableIEnumerationConverter))]
 36      public Collection<MyClassB> PropB
 37      {
 38        get{return m_PropB;}
 39        set{m_PropB = value;}
 40      }
 41      private MyClassB[] m_PropC = new MyClassB[]{};
 42      [Scm.TypeConverter(typeof(Dyn.ExpandableIEnumerationConverter))]
 43      public MyClassB[] PropC
 44      {
 45        get{return m_PropC;}
 46        set{m_PropC = value;}
 47      }
 48    }
 49  
 50    [Scm.TypeConverter(typeof(Scm.ExpandableObjectConverter))]
 51    public class MyClassB
 52    {
 53      public MyClassB(){}
 54      public MyClassB( string firstName, string lastName )
 55      {
 56        FirstName = firstName;
 57        LastName = lastName;
 58      }
 59      private string m_FirstName = String.Empty;
 60      [Scm.DisplayName("First Name")]
 61      public string FirstName
 62      {
 63        get{return m_FirstName;}
 64        set{m_FirstName = value;}
 65      }
 66      private string m_LastName = String.Empty;
 67      [Scm.DisplayName("Last Name")]
 68      public string LastName
 69      {
 70        get{return m_LastName;}
 71        set{m_LastName = value;}
 72      }
 73      public override string ToString()
 74      {
 75        return LastName + ", " + FirstName;
 76      }
 77  }
Listing 15

Figure 16

Listing 15 shows the use of Dyn.ExpandableIEnumerationConverter which makes each element in the IEnumeration to be a child property. Figure 16 shows the output. The collections of all three properties (PropA, PropB, and PropC) can be altered through the default collection-editor at run-time. Even if you add or remove elements in the collection using the collection-editor, it will not be reflected in the child properties immediately. Because the setXXXX method of the property is not called when you use the collection-editor. To reflect the changes, you will have to call PropertyGrid.Refresh() at some point. But the problem is that you never know when the collection has been modified. You will have to design your collection so that you receive a notification when the collection is changed. You can Google for "C# ObservableCollection". This article does not cover this area.

2.10 Sorting properties

When it comes to sorting, PropertyGrid can sort your properties, but in a limited way. You can set the PropertyGrid.PropertySort property to four different values, but to get most sorting capabilities out of this framework, you should set this property to PropertySort.Categorized.

Here we will again consider the code in Listing 1, except we will modify MyClassA, and remove all code from the button1_Click event handler.

C#
  1  public class MyClassA
  2  {
  3      public MyClassA(){}
  4  
  5      [Scm.Category("CatA")]
  6      [Dyn.SortID(2, 0)]
  7      public int PropA{get; set;}
  8  
  9      [Scm.Category("CatB")]
 10      [Dyn.SortID(3, 1)]
 11      public int PropB{get; set;}
 12  
 13      [Scm.Category("CatB")]
 14      [Dyn.SortID(4, 1)]
 15      public int PropC{get; set;}
 16  
 17      [Scm.Category("CatB")]
 18      [Dyn.SortID(1, 1)]
 19      public int PropD{get; set;}
 20  }
Listing 16

Figure 17

Figure 17 shows the output of Listing 16 with PropertyGrid.PropertySort set to PropertySort.CategorizedAlphabetical. Nothing unusual here except the Dyn.SortIDAttributes. There are times when you would want to sort your categories and properties some specific way other than the property-names or category-names. This attribute allows you to do so. By adding the attribute, you assign a number to the property and to the category. The first integer is for property and the second integer is for the category. At this point, there is no use of these Dyn.SortIDAttributes. These are sitting there. Now we will code in the button1_Click to change the sorting:

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3        Dyn.TypeDescriptor td = Dyn.TypeDescriptor.GetTypeDescriptor(propertyGrid1.SelectedObject);
  4        propertyGrid1.PropertySort = PropertySort.Categorized;
  5        td.CategorySortOrder = Dyn.SortOrder.ByIdDescending;
  6        td.PropertySortOrder = Dyn.SortOrder.ByIdAscending;
  7        propertyGrid1.Refresh( );
  8  }
Listing 17

Figure 18

Now we sort the categories by category IDs in descending order and the properties by property IDs in ascending order. Therefore, the Dyn.SortIDAttributes will be used. Since category "CatA" has ID 0 and category "CatB" has ID 1, category "CatB" is on the top. And for properties, PropA has ID 2, PropB has ID 3, PropC has ID 4, and PropD has ID 1. Thus, sorting by IDs: PropD, PropB, and then PropC in category "CatB", and in category "CatA", there is just one property. Code below shows the SortOrder enum.

C#
  1  public enum SortOrder
  2  {
  3      // no custom sorting
  4      None,
  5      // sort asscending using the property name or category name
  6      ByNameAscending,
  7      // sort descending using the property name or category name
  8      ByNameDescending,
  9      // sort asscending using property id or categor id
 10      ByIdAscending,
 11      // sort descending using property id or categor id
 12      ByIdDescending,
 13  }
Listing 18

So you have five options for sorting categories and also the same five options for sorting properties, and PropertGrid has its own four options of sorting. You can create many sorting effects by combining these options. The demo application with this article gives options to combine these settings at run-time and experiment with different combinations.

2.11 Localizing categories, properties, boolean, and enumeration

Let me just try to explain the idea how this localizing works with this framework first. This framework allows you to localize property name, category name, and property description. But it also can localize field members of an enum which we will talk about in detail later in this section. To access a localized string, we need a "key" for that string. The keys are inferred from the property itself in most part. Let's consider the following class in Listing 19:

C#
  1  [Dyn.Resource(BaseName="", AssemblyFullName="", KeyPrefix="MyPrefix_")]
  2  public class MyClassA
  3  {
  4    public MyClassA(){}
  5    [Dyn.CategoryResourceKey("MyCatKey")] 
  6    public int PropA{get; set;}
  7  }
Listing 19

Now for PropA, name-key would be "PropA_Name" (property name + "_Name"), and description-key would be "PropA_Desc" (property name + "_Desc"). We are now missing the key for the category. We cannot use a similar pattern for a category as categories can be repeated multiple times. Thus this framework provides an attribute Dyn.CategoryResourceKey (line 5). So the category-key would be the "MyCatKey" property PropA. Now we have solved the "key" problem.

But there is still another problem. That is, how do we know where the resources are. Or even in a more general context, how should we construct the System.Resources.ResourceManager which will be used to get the resources. There are several constructors for this class; which one should we use? Another thing is that resources can be in a different assembly than where the class (MyClassA) is defined. To resolve these issues, this framework provides an attribute called Dyn.ResourceAttribute. You must specify this attribute to your class (or enum) for localizing to work in this framework (line 1). This attribute can be applied to classes and enums.

You can see the Dyn.TypeDescriptor.UpdateStringFromResource method in the source code of this article to see how a System.Resources.ResourceManager is constructed from the Dyn.ResourceAttribute attribute. But I think the Dyn.ResourceAttribute.KeyPrefix property needs some discussion. It is used to resolve resource key conflicts. For example, if you have two classes with similar property names and the localized strings for these classes are stored in the same .resx file, then you will have "key" conflicts. By specifying a unique KeyPrefix for your classes, you can resolve the conflict. Since we have specified KeyPrefix as "MyPrefix_" (line 1), the name-key for our PropA would be "MyPrefix_PropA_Name", description-key would be "MyPrefix_PropA_Desc", and the category-key would be "MyPrefix_MyCatKey".

When localizing is enabled (by specifying the Dyn.ResourceAttribute attribute to your class), this framework goes through this following process:

For property name:

  • If a string resource is found with the key "XXXX_Name", it displays the string from the resource.

  • Otherwise, if the Scm.DisplayNameAttribute attribute is specified, it displays value from this attribute.

  • Otherwise, it displays the property name itself. In our case, it is "PropA".

For description:

  • If a string resource is found with the key "XXXX_Desc", it displays the string from the resource.

  • Otherwise, if the Scm.Description attribute is specified, it displays the value from this attribute.

  • Otherwise, it displays an empty-string.

And for category name:

  • If the Dyn.CategoryResourceKey attribute is specified and a string resource is found with the key Dyn.CategoryResourceKey.ResourceKey, it displays the string from the resource.

  • Otherwise, if the Scm.CategoryAttribute attribute is specified, it displays the value from this attribute.

  • Otherwise, it displays the default category which is "Misc".

You can turn on or off localizing for any specific property by specifying the Scm.LocalizableAttribute attribute to the property. Localizing is turned on if this attribute is not specified.

As I mentioned earlier, the enum type can also be localized. The semantics of localizing is same as for localizing a class, just think of the enum as a class and each field member of the enum as a property. Let's consider the listing below.

C#
  1  [Dyn.Resource(BaseName="",AssemblyFullName="", KeyPrefix="A_")]
  2  public class MyClassA
  3  {
  4      public MyClassA(){}
  5      [Dyn.Resource(BaseName="",AssemblyFullName="", KeyPrefix="B_")]
  6      [Scm.TypeConverter(typeof(Dyn.EnumConverter))]
  7      public System.Windows.Forms.FormStartPosition PropA{get; set;}
  8  
  9      [Dyn.Resource(BaseName = "", AssemblyFullName = "", KeyPrefix = "C_")]
 10      public EnumC PropB{get; set;}
 11  
 12      public EnumC PropC{get; set;}
 13  }
 14  
 15  [Dyn.Resource(BaseName = "", AssemblyFullName = "", KeyPrefix = "D_")]
 16  [Scm.TypeConverter(typeof(Dyn.EnumConverter))]
 17  public enum EnumC
 18  {
 19      One,
 20      Two,
 21  }
Listing 20
Line 1 Must be specified to enable localizing.
Line 5

Enables localizing of the System.Windows.Forms.FormStartPosition enum, even though we do not have access to its source code. It has the following members: Manual, CenterScreen, WindowsDefaultLocation WindowsDefaultBounds, and CenterParent.

Line 6

You must need this converter to localize the enum type. Let's see the keys that this converter will be using: B_Manul_Name, B_Manual_Desc, B_CenterScreen_Name, B_CenterScreen_Desc, etc.

Line 9

Enables localizing of the EnumC. This attribute is also specified on the

enum
itself (line 15). We are overriding the resource information at property level. We are not specifying Scm.TypeConverter with type Dyn.EnumCnverter on PropB, because it is already specified on EnumC itself (line 16). Let's see the keys that the converter will be using: C_One_Name, C_One_Desc, C_Two_Name, C_Two_Desc.

Notice it is prefixed with "C_" instead of "D_". Because we are overriding the resource information.

Line 12

PropB is not overriding anything. It will using whatever is specified on EnumC. Let's see the keys that the converter will be using: D_One_Name, D_One_Desc, D_Two_Name, D_Two_Desc.

The demo application in this article is localized in three different languages: English, Danish, and Chinese. Google translation was used to do the actual translation. So the it is not 100% perfect.

It might be useful for your application to localize some .NET types centrally. For example, you may want to localize the Boolean type such that when users see the texts "True" or "False" in the PropertyGrid, they see it in localized form. You can achieve this by adding the appropriate attributes like Listing 21.

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    Dyn.ResourceAttribute ra = new Dyn.ResourceAttribute( );
  4    ra.AssemblyFullName = this.GetType( ).Assembly.FullName;
  5    ra.KeyPrefix = "BuiltinBool_";
  6    ra.BaseName = "CustomTypeDescriptorApp.Properties.Resources";
  7    Scm.TypeConverterAttribute tca = new Scm.TypeConverterAttribute(typeof(Dyn.BooleanConverter));
  8    Scm.TypeDescriptor.AddAttributes(typeof(Boolean), ra, tca);
  9  }
Listing 21

You can also achieve a similar behavior for any enum type, in this case the Dyn.EnumConverter class. For any Struct type, such as System.Drawing.Size or System.Drawing.Rectangle, you will have to create a converter yourself to achieve this behavior.

2.12 Working with ValueType objects

ValueyType types such as Struct have different semantics than classes. .NET does not allow to add TypeDescriptionProvider on a ValueType. To overcome this issue, this framework provides a wrapper class called Dyn.StructWrapper.

To use a Struct type in this framework, you would do this:

C#
  1  private void button1_Click( object sender, EventArgs e )
  2  {
  3    System.Drawing.Rectangle rc = new System.Drawing.Rectangle();
  4    Dyn.StructWrapper sw = new Dyn.StructWrapper(rc);
  5    Dyn.TypeDescriptor.IntallTypeDescriptor(sw);
  6    this.propertyGrid1.SelectedObject = sw;
  7  }
Listing 22

After this, everything works as if it were a class.

3 Points of interest

The demo application in this article demonstrates many features of this framework, but no all features.

4 References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Denmark Denmark
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionSeems to get a little upset with Child Classes Pin
Member 149220361-Dec-20 20:28
Member 149220361-Dec-20 20:28 
GeneralMy vote of 5 Pin
LightTempler6-Sep-20 0:37
LightTempler6-Sep-20 0:37 
Very cool stuff!
LiTe
QuestionInterdependent properties Pin
RudolfHenning7-Jan-19 1:04
RudolfHenning7-Jan-19 1:04 
QuestionIssue with Providing standard values for properties Pin
Stigzler741-Dec-18 10:35
Stigzler741-Dec-18 10:35 
QuestionAn Error occured when i use this great work on my self-definition Enum type Pin
Jacberg1-Jun-16 17:28
Jacberg1-Jun-16 17:28 
QuestionOn The Fly Property Drop Item Shows Value and Not DisplayValue After Selecting Item Pin
Sling Blade 222-Apr-16 21:26
professionalSling Blade 222-Apr-16 21:26 
QuestionOutstanding Pin
phil.o7-Oct-15 8:47
professionalphil.o7-Oct-15 8:47 
QuestionOnTheFly create child properties from IEnumeration properties Pin
Member 97573517-Sep-15 22:13
Member 97573517-Sep-15 22:13 
QuestionDoes it work with the PropertyGrid in Xceed.Wpf.Toolkit? Pin
mythzxp14-Jan-15 14:22
mythzxp14-Jan-15 14:22 
QuestionIssue with dynamic property collections in PropertyGrid Pin
sivanry1-Dec-14 4:31
sivanry1-Dec-14 4:31 
AnswerRe: Issue with dynamic property collections in PropertyGrid Pin
Mizan Rahman15-May-15 20:33
Mizan Rahman15-May-15 20:33 
QuestionProblems with expandable collections - Dynamic Properties for PropertyGrid Pin
charliecolester24-Oct-14 13:37
charliecolester24-Oct-14 13:37 
AnswerRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
Mizan Rahman25-Oct-14 20:16
Mizan Rahman25-Oct-14 20:16 
GeneralRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
charliecolester26-Oct-14 14:28
charliecolester26-Oct-14 14:28 
GeneralRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
Mizan Rahman26-Oct-14 14:48
Mizan Rahman26-Oct-14 14:48 
GeneralRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
charliecolester26-Oct-14 15:21
charliecolester26-Oct-14 15:21 
GeneralRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
Mizan Rahman29-Oct-14 16:46
Mizan Rahman29-Oct-14 16:46 
GeneralRe: Problems with expandable collections - Dynamic Properties for PropertyGrid Pin
charliecolester31-Oct-14 8:41
charliecolester31-Oct-14 8:41 
QuestionChanges to Arrays/Collections not persistent? Pin
Ruben Fuchs12-Oct-14 22:49
Ruben Fuchs12-Oct-14 22:49 
AnswerRe: Changes to Arrays/Collections not persistent? Pin
Mizan Rahman25-Oct-14 20:17
Mizan Rahman25-Oct-14 20:17 
SuggestionAssert at derived class Pin
Nicola Ahrens17-Jun-14 5:43
Nicola Ahrens17-Jun-14 5:43 
GeneralMy vote of 5 Pin
BillWoodruff28-Jul-13 3:56
professionalBillWoodruff28-Jul-13 3:56 
QuestionBreaking Changes Pin
MrGadget3-Jul-13 1:06
MrGadget3-Jul-13 1:06 
AnswerRe: Breaking Changes Pin
Mizan Rahman3-Jul-13 23:48
Mizan Rahman3-Jul-13 23:48 
GeneralRe: Breaking Changes Pin
MrGadget4-Jul-13 7:02
MrGadget4-Jul-13 7:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.