
Summary
This article presents a way to enhance the use of the PropertyGid control with dynamic properties and
globalization.
Contents
Introduction
Although not easily customizable, PropertyGrid, is one
of the most versatile and powerful controls in .NET Framework, and with a little
work it can spare a lot of programming time, being very fitted for run-time
editing of simple classes like application's setting. One requirement in using
it as a run-time editor is to be globalized (there are already very good
articles on that here at CodeProject). Another thing that you may stumble into
is that sometimes for the same object, you may need different editing levels.
Take for example, the case when you use it to edit your application settings.
Depending on the user's privileges, all or just part of the application's
settings should be visible or editable. And, of course, you want to use the same
editor.
This article will focus on how to dynamically change the visibility and
ReadOnly state of properties in a PropertyGrid
control, also providing a way of globalizing it.
Before reading this article, it would be good to read at least some of the
other articles on PropertyGrid that are here at
CodeProject.
When you open the solution for the first time, you should compile it before
doing anything else.
How it works
All the implementation is based on ICustomTypeDescriptor. This interface allows an object to provide
type information about itself. Typically, it is used when an object needs
dynamic type information. Most important member is GetProperties, which returns the properties for an instance of a
component, in the form of a PropertyDescriptorCollection, which
represents a collection of PropertyDescriptor objects.
The main idea is to implement the ICustomTypeDescriptor
for a class (DynamicTypeDescriptor in this case), and in
GetProperties method, substitute the collection of PropertyDescriptor, with a collection of DynamicPropertyDescriptor (which is derived from PropertyDescriptor). For the DynamicPropertyDescriptor class, override some key members: Description, Category and DisplayName in order to implement globalization, and IsReadOnly to play with the property's read only state. Although
it also has a member called IsBrowsable, overriding it
doesn't seem to affect the property visibility. So, in order to toggle the
visibility of a property, the solution consist in adding or not its DynamicPropertyDescriptor to the PropertyDescriptorCollection when GetProperties
method is called.
Lets say that you have a class that you want to edit it with the PropertyGrid, and you also want to make use of dynamic properties
and globalization. In most cases, the use of a surrogate class is best. A
surrogate class is a class parallel with the original one, that has only those
properties that you intend to expose to the user. First, you fill the values of
the properties of the surrogate object with the values of the original object,
let the user edit it with PropertyGrid, and then update the
values in the original object. Why? Because many time the original class has a
lot of properties and you want to expose only a minor fraction of them (take the
Control for example). Another problem is that it will be to hard to
implement ICustomTypeDescriptor interface for every class
that you want to edit it with PropertyGrid (since most of
the classes that we use already inherits from something else). Because a
surrogate class doesn't inherit from anything, you can make it inherit from
DynamicTypeDescriptor which already implements ICustomTypeDescriptorand spares you from all the troubles. The
use of a surrogate class can also help you to better structure your code
(implement a Model-View-Controller pattern) and to detect invalid user input
easier and is more reliable (by allowing you to perform a global validation
before updating the original object).
Dynamic Properties
This implementation of the dynamic properties allows programmer to change the
visibility and ReadOnly state of properties during run-time but also at
design-time. In order to have at run-time the settings made at design-time, the
DynamicPropertyDescriptor should have some mechanism to
persist those settings. The solution is to add to DynamicPropertyDescriptor class, two new collections: PropertyCommands and CategoryCommands. PropertyCommands is a collection of PropertyCommand that maps the real properties of the class. PropertyCommand has three members: Name,
ReadOnly and Visible, and it is here
that DynamicPropertyDescriptor stores information about the
desired state of its properties. The same with CategoryCommands, only that CategoryCommand
has only two members: Name and Visible.
For the ReadOnly state, the mechanism is very simple,
and consist in overriding the IsReadOnly member of the DynamicPropertyDescriptor.
public override bool IsReadOnly
{
get
{
PropertyCommand pc=
instance.PropertyCommands[this.Name] as PropertyCommand;
if(pc!=null)
{
return pc.ReadOnly;
}
else
{
return this.basePropertyDescriptor.IsReadOnly;
}
}
}
It simply looks to see if DynamicTypeDescriptor has a
PropertyCommand associated with this property, and if so it
returns the value of ReadOnly of the PropertyCommand, otherwise it returns the default value.
Since this trick didn't work with IsBrowsable, the control the
visibility of a property has a more radical implementation.
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
if (!CustomControls.Functions.General.IsInDesignMode() )
{
PropertyDescriptorCollection baseProps =
TypeDescriptor.GetProperties(this, attributes, true);
dynamicProps = new PropertyDescriptorCollection(null);
foreach( PropertyDescriptor oProp in baseProps )
{
if(oProp.Category!="Property Control" &&
(PropertyCommands[oProp.Name] ==null ||
PropertyCommands[oProp.Name].Visible) &&
(CategoryCommands[oProp.Category] ==null ||
CategoryCommands[oProp.Category].Visible))
{
dynamicProps.Add(new DynamicPropertyDescriptor(this,oProp));
}
}
return dynamicProps;
}
return TypeDescriptor.GetProperties(this, attributes, true);
}
As you can see, if PropertyCommands or
CategoryCommands has an entry with the property name or
respectively with the category name, and if at least one entry has the
Visible member false, that property is not
added to the PropertyDescriptorCollection that the function
returns. During design-time, we want to see all the properties. As far as I
figured it out, the other overloaded member of the GetProperties
function is called only in design-time so it has a normal implementation.
public PropertyDescriptorCollection GetProperties()
{
if ( dynamicProps == null)
{
PropertyDescriptorCollection baseProps =
TypeDescriptor.GetProperties(this, true);
dynamicProps = new PropertyDescriptorCollection(null);
foreach( PropertyDescriptor oProp in baseProps )
{
dynamicProps.Add(new DynamicPropertyDescriptor(this,oProp));
}
}
return dynamicProps;
}
More difficult was to persist the two collections. In order to persist a
member of a class, the class itself should be persisted. Normally, this means
that the class should implement IComponent or should have a
TypeConverter associated with it. Once this problem solved, the two
collections must be persisted, and this raises a special problem: if the
collections are filled with items in the constructor of the
DynamicTypeDescriptor class, every time the project is compiled,
the Designer adds all the items again, although they were already added before.
So after compiling the project three times, every item was added three times. To
solve this problem, the main class (the class that inherits from
DynamicTypeDescriptor) should implement both
IComponent and ISupportInitialize, and in the
EndInit method of the ISupportInitialize should check
to see if the collections were already filled with items, and if not, fill them
then. But I'm not too satisfied with this..
So, here is a problem: I have a class that have a collection of items. I want
to initialize the collection from the beginning with some items, and I also want
to persist that collection, in order to change it later. I'll appreciate any
comment on this topic.
Globalization
As it was mentioned before, the implementation of globalization consists
mainly in overriding Description, Category
and DisplayName members of the
DynamicPropertyDescriptor class.
public override string Description
{
get
{
return instance.GetLocalizedDescription(base.Name);
}
}
public override string Category
{
get
{
return instance.GetLocalizedName(base.Category);
}
}
public override string DisplayName
{
get
{
return instance.GetLocalizedName(base.Name);
}
}
As you can see, it relies on two methods of the
DynamicTypeDescriptor class in order to get the localized string.
DisplayName and Description make the
request with the same value (the property name), but they expect different
translations, so they call different functions.
DynamicTypeDescriptor gives a neutral implementation of the two
methods, leaving the inheritor to choose the appropriate way of translating the
message.
public virtual string GetLocalizedName(string Name)
{
return Name;
}
public virtual string GetLocalizedDescription(string Description)
{
return Description;
}
However, in the example that comes with the article, both
Company and Employee classes override these two
methods and give an implementation of the globalization issue.
public override string GetLocalizedName(string Name)
{
string name=CustomControls.Globalization.Dictionary.Translate(Name);
if(name!=null ){return name;}
return base.GetLocalizedName (Name);
}
public override string GetLocalizedDescription(string Description)
{
string descr =
CustomControls.Globalization.Dictionary.Translate(Description
+ "_Descr");
if(descr!=null ){return descr;}
return base.GetLocalizedName (Description);
}
They rely on the Dictionary class (you can find it in the
CustomControls project) to translate the strings. This looks for a file
Dictionary.resx, that should be located in the same directory as the
executable, load its content with a ResXResourceReader and store
the key- value pairs into a HashTable. If the file or the value
doesn't exist, it returns null. It has two overloaded
Translate methods: one receives a neutral string and it appends by
itself the two letter ISO name of the CurrentCulture and "_", while
the other receives both the neutral string and the desired culture two letter
ISO name.
For example, for the display name of the Age property, if the
current location is English (United States), hence CurentCulture is
en-US, GetLocalizedName passes to the Dictionary only
Age, this looks into the HashTable for a key EN_Age.
For the Age property description,
GetLocalizedDescription passes to the dictionary
Age_Descr, and this looks for a key like
EN_Age_Descr.
I chose to work with ResXResourceReader and
Hashtable because I wanted to have the resource file out of the
assembly. Otherwise every modification in the resource file would require to
recompile the project. If you don't intend to modify the resource file often, it
would be more suitable to embed it into the assembly and use a
ResourceManager.
How to use it
Depends on what you want to do:
- You want to make use of dynamic properties and globalization only at
run-time
This is the simplest case, and all you have to do is to inherit, directly or
through a surrogate class, from DynamicTypeDescriptor. Changing the
state for a property is straightforward:
if(company.PropertyCommands.Contains("Address"))
{
company.PropertyCommands["Address"].ReadOnly=
!company.PropertyCommands["Address"].ReadOnly;
}
else
{
company.PropertyCommands.Add(new
CustomControls.HelperClasses.PropertyCommand("Address",
true, true));
}
pg.Refresh();
First check to see if the PropertyCommands already contains a
PropertyCommand associated with the property that you want
to change, and if so change it, otherwise add a new
PropertyCommand for that property. Don't forget to refresh
the PropertyGrid! To localize the property, make sure that
the resource file Dictionary.resx is in the executable's directory, and
add the key-value pairs for the property display name and description.
- You want to make use of dynamic properties at design-time also.
- For an isolate class. (you have an example in
Person class)
- You have a class that is embedded into another one. (you have an example in
Company and Employee classes)
- You must implement
IComponent. This way your class and its
properties will be persisted into code, and it will also activate the
implementation of the ISupportInitialize interface by the
DynamicTypeDescriptor, which will spare you from calling
EndInit.
- In this case, the main class (the one that encapsulate the others), must
implement
IComponent and for all the others, create and associate a
TypeConverter. The first one must implement IComponent
in order to be persisted and to have EndInit called in the end of
the InitializeComponent method. Override the EndInit
for this class and call the EndInit method of all the
DynamicTypeDescriptor classes that it contains.
public override void EndInit()
{
base.EndInit();
foreach(Employee emp in Employees )
{
emp.EndInit();
}
}
Conclusions
Although in the beginning, working with PropertyGrid may seem
difficult and complicated, once dominated, the effort starts to pay off.
References
Revision history