Click here to Skip to main content
15,867,288 members
Articles / Programming Languages / C#
Article

Unleash PropertyGrid with Dynamic Properties and Globalization

Rate me:
Please Sign up or sign in to vote.
4.97/5 (24 votes)
29 Jan 2004CPOL9 min read 125.8K   4.8K   88   20
The article presents a way to enhance the use of the PropertyGid control with dynamic properties and globalization

Image 1

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.

C#
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.

C#
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.

C#
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,

C#
Category 
and DisplayName members of the DynamicPropertyDescriptor class.

C#
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.

C#
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.

C#
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:

    C#
    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.
    1. For an isolate class. (you have an example in Person class)
    2. You have a class that is embedded into another one. (you have an example in Company and Employee classes)
    1. 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.
    2. 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.
    C#
    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

  • Original article.

License

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


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

Comments and Discussions

 
GeneralRe: Returning Descriptions Pin
panthersys24-Mar-04 11:22
panthersys24-Mar-04 11:22 
GeneralKudos to you! Pin
panthersys10-Mar-04 10:30
panthersys10-Mar-04 10:30 
GeneralKudos... Pin
Daniel Zaharia16-May-04 20:30
Daniel Zaharia16-May-04 20:30 
GeneralSimple IsBrowsable workaround Pin
Adam Plucinski7-Mar-04 9:56
Adam Plucinski7-Mar-04 9:56 
GeneralRe: Simple IsBrowsable workaround Pin
Daniel Zaharia13-Mar-04 5:13
Daniel Zaharia13-Mar-04 5:13 
QuestionHow to Call a Form from Propertygrid Pin
Raju Selvaraj11-Feb-04 19:06
Raju Selvaraj11-Feb-04 19:06 
AnswerRe: How to Call a Form from Propertygrid Pin
Daniel Zaharia14-Feb-04 6:21
Daniel Zaharia14-Feb-04 6:21 
GeneralRe: How to Call a Form from Propertygrid Pin
Duong Tien Nam22-Dec-04 22:29
Duong Tien Nam22-Dec-04 22:29 
GeneralRe: How to Call a Form from Propertygrid Pin
The_Mega_ZZTer2-Sep-05 13:32
The_Mega_ZZTer2-Sep-05 13:32 
GeneralOne more thing... Pin
Daniel Zaharia2-Feb-04 0:02
Daniel Zaharia2-Feb-04 0: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.