Contents
- Introduction
- Using the Code
- Altering properties at run-time
- Hiding/removing properties at run-time
- Creating new properties at run-time
- Displaying images for property values at run-time
- Displaying state image of properties
- Providing standard values for properties
- Customizing enumeration (enum) type properties
- Customizing boolean type properties
- Creating child properties from IEnumeration properties
- Sorting properties
- Localizing categories, properties, boolean, and enumeration
- Working with ValueType objects
- Point of interest
- 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.
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
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:
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
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
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
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
.
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);
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
.
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);
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);
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.
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:
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:
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.
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:
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.
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
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
34 [Dyn.DisplayName("Saturday")]
35 [Scm.Description("Day of the Saturn.")]
36 [Scm.Browsable(false)]
37 Sat = 32,
38
39
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
.
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.
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.
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.SortIDAttribute
s. 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.SortIDAttribute
s. These are sitting there.
Now we will code in the button1_Click
to change the sorting:
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.SortIDAttribute
s
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.
1 public enum SortOrder
2 {
3
4 None,
5
6 ByNameAscending,
7
8 ByNameDescending,
9
10 ByIdAscending,
11
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:
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 enum
s.
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.
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.
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:
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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.