Dynamic Properties for PropertyGrid
Introduction
In this article, I will discuss and show how to get dynamic behavior out of the PropertyGrid
control. I would emphasize the word "dynamic" here, and also "dynamic" here means the ability to do things at run-time. Some of the stuff here can also be done using code if you have enough knowledge of the extensibility mechanism of .NET. Dynamic or not, this solution will make your life simpler when it comes to controlling the PropertyGrid
control.
This article assumes you have some knowledge of the following:
PropertyGrid
controlAttribute
classTypeDescriptor
classTypeConverter
classUITypeEditor
classTypeDescriptionProvider
classICustomTypeDescriptor
interface- Localizing a Windows Forms application
Don't be nervous if you are not familiar with the above classes, simply read the first two articles in the reference section. That will bring you up-to-speed. All-in-all, this solution encapsulates usage of these classes, so you will work with more plain field by using this solution. Using this solution, you can achieve the following functionalities at run-time:
- Create properties at run-time and add it to your objects.
- Get property change notifications to your code for properties created at run-time.
- Show/hide any property.
- Enable/disable any property.
- Show/hide any member of an enumerated data type, including .NET's built-in enumerations.
- Enable/disable any member of an enumerated data type, including .NET's built-in enumerations.
- Add custom display name to any member of an enumerated data type, including .NET's built-in enumerations.
- Add custom description to any member of an enumerated data type, including .NET's built-in enumerations.
- Provide editor for enumeration data types that has
System.FlagsAttribute
. - Sort properties and/or categories in ascending or descending order using their names or by their IDs (discussed later).
- Localize your property name, category name, property description, enumeration name, and enumeration description (including .NET's built-in enumerations).
- A property with
Boolean
type can show Yes/No or any other display string instead of just True/False. - A property with
Boolean
type can be localized. - Show/Hide one or more state icons for properties along with tool tips. Icon size must by 8 pixels by 8 pixels. This is useful to visualize the state of the property (i.e., invalid data). You can show multiple state icons simultaneously. Icons are shown on the right of the property name on the same line of the property on the
PropertyGrid
. - Show value icon for a property value.
All these can be done at runtime (as well as during coding). During my search, I found some articles which I have listed at the end of this article. They have few of these features in limited form. What I tried to do with my solution was to make a generic solution for the developer so she/he can tell the PropertyGrid
what to display, when to display, and how to display from within their class.
Using the code
The sample source code provided in this article contains two C#, VS2008 projects:
Many features have been demonstrated on the sample application in the article. To cover them all in writing would be difficult task, rather one can get idea by looking at the code. But some features may need some explanation. Those features will be demonstrated in a questions and answers format.
Q1: How do I sort properties and categories?
Let's consider this simple class.
public class MyClass()
{
private DynamicCustomTypeDescriptor m_dctd = null;
public MyClass()
{
m_dctd = ProviderInstaller.Install(this);
m_dctd.PropertySortOrder = CustomSortOrder.AscendingByName;
m_dctd.CategorySortOrder = CustomSortOrder.DescendingById;
}
[Category("CatA")]
[DisplayName("PropertyA")]
[Description("Description of PropertyA")]
[Id(3, 1)] //here PropA gets an ID 3 and CatA gets in ID 1 as well.
public int PropA{...}
[Category("CatB")]
[DisplayName("PropertyB")]
[Description("Description of PropertyB")]
[Id(2, 2)] // here PropB gets an ID 2 and CatA gets in ID 2 as well.
public int PropB{...}
[Category("CatC")]
[DisplayName("PropertyC")]
[Description("Description of PropertyC")]
[Id(1, 3)] // here PropC gets an ID 1 and CatA gets in ID 3 as well.
public bool PropC{...}
[Category("CatC")]
[DisplayName("PropertyD")]
[Description("Description of PropertyD")]
[Id(4, 3)] //here PropD gets an ID 4 and CatA gets in ID 3 as well.
public int PropD{...}
}
In the constructor of the class, we are saying that we would like to sort the properties by name in ascending order and sort categories by ID in descending order.
CustomSortOrder
is an enumeration:
public enum CustomSortOrder
{
// no custom sorting
None,
// sort asscending using the property name or category name
AscendingByName,
// sort asscending using property id or categor id
AscendingById,
// sort descending using the property name or category name
DescendingByName,
// sort descending using property id or categor id
DescendingById,
}
Note that a property without the IdAttribute
is same as a property with [Id(0,0)]
, and IDs cannot be negative values. The PropertySort
property of PropertyGrid
effects the sorting. The sample application allows you combine all sorting features through GUI. You experiment with different combination.
Q2: How do I show property names, property description, and category names from satellite assemblies for localizing purpose?
Here we will consider the same class, MyClass
, from above. To make our class, it needs information to be be able to construct ResourceManager
. For that we use a class level attribute called - ClassResourceAttribute
. This attribute has three properties:
BaseName (string)
- Used inResourceManager
construction.Assembly (string)
- If specified, used inResourceManager
construction, otherwiseType.Assembly
is used.KeyPrefix (object)
- If specified, all resource keys must be prefixed with this string. This allows to create unique keys.
[ClassResource(BaseName="TypeDescriptorApp.Properties.Resources",
KeyPrefix="MyClass_", Assembly="")]
public class MyClass()
{
....
}
Note that all keys are case-sensitive.
Let's consider the property PropA
from our sample class. For display name, PropertyDescriptorManager
will search for a string in the following order:
- Look for a resource string in the target resource file with key
MyClass_PropA_Name
. The format is:<KeyPrefix>_<PropertyName>_Name
. - Use the
DisplayNameAttribute
if it exists. In our case, it will be "PropertyA". - Use the property name itself. In our case, it is "PropA".
For the description string of the property PropA
, PropertyDescriptorManager
will search for a string in the following order:
- Look for a resource string in the target resource file with key
MyClass_PropA_Desc
. The format is:<KeyPrefix>_<PropertyName>_Desc
. - Use the
DescriptionAttribute
if it exists. In our case, it will be "Description of PropertyA". - Otherwise, the description will be blank.
For the category string of property PropA
, PropertyDescriptorManager
will search for a string in the following order:
- Look for a resource string in the target resource file with key
MyClass_Cat3
. The format is:<KeyPrefix>_Cat<CategoryID>
. - Use the
CategoryAttribute
if it exists. In our case, it will be "CatA". - Otherwise, use the default category, which is "Misc".
Q3: How do I change the property display name, category string, and description string at run-time and also how do I show/hide/enable/disable a property at run-time?
Here we will consider the same class, MyClass
, from above. We will modify the constructor to modify some attributes of the property PropA
.
public MyClass()
{
m_pdm = new PropertyDescriptorManager (this);
m_pdm.PropertySortOrder = CustomSortOrder.AscendingByName;
m_pdm.CategorySortOrder = CustomSortOrder.DescendingByName;
// now lets modify some attribute of PropA
CustomPropertyDescriptor cpd = m_pdm.GetProperty("PropA");
cpd.SetDisplayName("New display name of PropA");
cpd.SetDescription("New description of PropA");
cpd.SetCategory("New Category of PropA");
cpd.SetIsReadOnly(true); // disables the property
cpd.SetIsBrowsable(true); // hides the property
cpd.CategoryID = 4;
}
Q4: I have a property of type Int32. This property represents the customer ID. In my database, I have a list of customer names and IDs. I would like for the PropertyGrid to show a drop-down box with customer names and when users pick one from the list, I would like the PropertyGrid to assign the ID to the property instead of the name. How do I do this?
Here we will consider the same class again, MyClass
, from above. We will modify the constructor to provide a lookup drop-down box for PropA
.
public MyClass()
{
m_pdm = new PropertyDescriptorManager (this);
m_pdm.PropertySortOrder = CustomSortOrder.AscendingByName;
m_pdm.CategorySortOrder = CustomSortOrder.DescendingByName;
CustomPropertyDescriptor cpd = m_pdm.GetProperty("PropA");
PopululateDropDownListFromDatabaseSource(cpd);
}
private void PopululateDropDownListFromDatabaseSource( CustomPropertyDescriptor cpd )
{
cpd.StatandardValues.Clear( );
string[] arrNames = {"Adam", "Brian",
"Russel", "Jones", "Jakob"};
for (int i = 101; i < 106; i++)
{
StandardValueAttribute sva =
new StandardValueAttribute(arrNames[i - 101] +
" ("+ i.ToString() + ")", i);
// you can also add an description for any StandardValueAttribute
sva.Description = "Description of " + sva.DisplayName + ".";
cpd.StatandardValues.Add(sva);
}
}
Notice that we have included a description as well. But when you run the code, you will not see the description. Because to show description, you will need a special UITypeEditor
. So to make the property PropA
to display the description, we will have to add the following attribute to PropA
:
[Editor(typeof(StandardValueEditor), typeof(UITypeEditor))]
StandardValueAttribute
has these properties:
DisplayName (string)
- What gets displayed on the screen.Value (object)
- The actual value. The actual type of this value must match the type of the property.Description (string)
- Description of the value. This requires theStandardValueEditor
to have an effect.Visible (bool)
- Indicates whether or not to show the value.Enabled (bool)
- Indicates whether or not to enable the value. This requires theStandardValueEditor
to have an effect.
Q5: In the beginning of this article, you said we can manipulate any member of an enumeration data type, how do I this?
Let's consider this following enumeration type and class:
[EnumResource("TypeDescriptorApp.Properties.Resources")]
[Editor(typeof(StandardValueEditor), typeof(UITypeEditor))]
public enum Position
{
[StandardValue("First", Description = "Excellen.")]
One
[StandardValue("Second", Description = "Good.")]
Two
}
[ClassResource("TypeDescriptorApp.Properties.Resources",
KeyPrefix="MyEnumClass1_")]
public class MyEnumClass1()
{
private PropertyDescriptorManager m_pdm = null;
public MyEnumClass()
{
m_pdm = new PropertyDescriptorManager (this);
}
public Position PropE{...}
public Position PropF{...}
}
public class MyEnumClass2()
{
private PropertyDescriptorManager m_pdm = null;
public MyEnumClass()
{
m_pdm = new PropertyDescriptorManager (this);
}
public Position PropG{...}
}
Note that we are applying StandardValueAttribute
on each field of the enumeration. So each field becomes an standard-value, thus you can use the properties of the StandardValueAttribute
to show/hide/enable/disable any member of the enumeration. On top of all that, enumeration members can be localized. As always, to localize your class
or enum
, you have to add the attribute ResourceBaseNameAttribute
on the enumeration itself and/or on the class that uses the enumeration. If you apply on both, the class has higher priority than the enumeration. This allows you to override resource information of this enumeration type that is defined in another library that you do not have access to in the source code. You can also override the enumeration resource information at the property level as well which has the highest priority.
Let's create a string table in a resource file that looks like this:
Key | Value | Comment |
Position_One_Name |
MyString1 |
enumeration level (priority = 3). Format: <KeyPrefix><Enum FieldName>_Name |
Position_One_Desc |
MyString2 |
enumeration level (priority = 3). Format: <KeyPrefix><Enum FieldName>_Desc |
Position_Two_Name |
MyString3 |
enumeration level (priority = 3) |
Position_Two_Desc |
MyString4 |
enumeration level (priority = 3) |
MyEnumClass1_Position_One_Name |
MyString5 |
class level override (priority = 2). Format: <KeyPrefix><Enum Name>_<Enum FieldName>_Name |
MyEnumClass1_Position_One_Desc |
MyString6 |
class level override (priority = 2). Format: <KeyPrefix><Enum Name>_<EnumFieldName>_Desc |
MyEnumClass1_Position_Two_Name |
MyString7 |
class level override (priority = 2) |
MyEnumClass1_Position_Two_Desc |
MyString8 |
class level override (priority = 2) |
MyEnumClass1_PropE_One_Name |
MyString9 |
property level override (priority = 1). Format: <KeyPrefix><Property Name>_<Enum FieldName>_Name |
MyEnumClass1_PropE_One_Desc |
MyString10 |
property level override (priority = 1). Format: <KeyPrefix><Property Name>_<Enum FieldName>_Desc |
MyEnumClass1_PropE_Two_Name |
MyString11 |
property level override (priority = 1) |
MyEnumClass1_PropE_Two_Desc |
MyString12 |
property level override (priority = 1) |
Note: 1 is the highest priority and 3 is the lowest priority.
PropE
will use the following keys:
MyEnumClass1_PropE_One_Name
MyEnumClass1_PropE_One_Desc
MyEnumClass1_PropE_Two_Name
MyEnumClass1_PropE_Two_Desc
Because the enumeration has been overridden at property level.
PropF
will use the following keys:
MyEnumClass1_Position_One_Name
MyEnumClass1_Position_One_Desc
MyEnumClass1_Position_Two_Name
MyEnumClass1_Position_Two_Desc
Because the enumeration has not been overridden at property for this property, only at class level.
PropG
will use the following keys:
Position_One_Name
Position_One_Desc
Position_Two_Name
Position_Two_Desc
The enumeration has not been overridden for this class, so it uses what is defined for the enumeration itself.
So as you can see, there is a fallback strategy in place for the enumeration type. That mans you can define all your enumeration in a library, including the resource information, and then another library can use that enumeration as it is or override the resource information as necessary. Just know how to override the keys in the resource.
Q6: I have a Boolean property in my class, I would like to show Yes\No on the PropertyGrid instead of True\False. How do I do this?
Here we will consider the same class, MyClass
, from above. PropC
is a Boolean
type property, so we can do this in the constructor:
public MyClass()
{
m_pdm = new PropertyDescriptorManager (this);
m_pdm.PropertySortOrder = CustomSortOrder.AscendingByName;
m_pdm.CategorySortOrder = CustomSortOrder.DescendingByName;
// now lets display Yes/No instead of True/False
CustomPropertyDescriptor cpd = m_pdm.GetProperty("PropC");
int i = 0;
foreach (StandardValueAttribute sva in cpd.StatandardValues)
{
if (i == 0) // means it is True part
{
sva.DisplayName = "Yes";
}
else
{
sva.DisplayName = "No";
}
i++;
}
}
Q7: Can a Boolean property be localized? If so, how?
Yes, a Boolean
property can be localized. So you can show "True"/"False" in different languages.
Think of Boolean
properties as enumeration properties, like this:
public enum Boolean
{
True = true;
False = false;
}
Note: This above enumeration does not exist in this solution or in .NET.
-- End of Questions and Answers Format--
So, now to localize this, you will have to override the enumeration at class level or property level, or both. Please see Q5 for details.
I hope by now you know how to use this solution. There are other features that are demonstrated in the sample application. If you have questions, feel free to post it here. If you like it, don't forget to vote for it generously, this is the only motivation to get it going.
Points of interest
When you get into the System.ComponentModel.TypeDescriptor
class, it is interesting to look at the number of classes that play a role in providing a description for a type.
References
发表评论
aN1yBD pretty helpful material, overall I believe this is well worth a bookmark, thanks