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 ICustomTypeDescriptor
and 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
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.