Click here to Skip to main content
15,867,141 members
Articles / Desktop Programming / Windows Forms
Article

Using a TypeDescriptionProvider to support dynamic run-time properties

Rate me:
Please Sign up or sign in to vote.
5.00/5 (44 votes)
16 Jun 2008CPOL4 min read 123.7K   1.9K   78   17
This articles explains how to implement a TypeDescriptionProvider for a class to support multiple object types using a single class type

Image 1

Introduction

Recently at work, I had to use the TypeDescriptionProvider attribute while prototyping some new features for our product, and I thought it would be a good idea to demonstrate how to use this attribute and its related classes. In this article, I will go through a sample demo application which consists of a list of objects and a property grid that shows the properties for the selected object. For the demo, I assume that there will always be two types of objects - one that represents a book and the other that represents a movie. The only common property across the two objects is the Name property. This is what my Book object would look like:

NameThe title of the book
Amazon RankIts sales rank on Amazon
AuthorThe author of the book
HardCoverWhether it's available in hard-cover

And, here's what my Movie object would look like:

NameThe name of the movie
DirectorThe director of the movie
DurationDuration of the movie
RatingThe movie rating
Release DateWhen it was released

One way of designing for this scenario would be to have a base class (with just the Name property) and two derived classes - one for Book and one for Movie. But, consider the following two extra requirements:

  • New object types may be added at any time
  • New properties might be added for existing object types at any time (including at run time)

To support this would mean having to add new classes frequently and also to modify the existing classes. And, that still won't address dynamic adding of properties to an existing object. This is where using a TypeDescriptionProvider turns out to be very useful, and the rest of the article will explain how to implement the requirements using that approach.

Technical Implementation

The first step is to implement an enumeration for the type of object. When we add new object types, we would add those to this enumeration as and when required.

C#
enum TitleCategory
{
    Book,
    Movie
}

Now, we will add a single sealed class called Title which will define the type description provider and also provide for default properties.

C#
[TypeDescriptionProvider(typeof(TitleTypeDescriptionProvider))]
sealed class Title
{
    public Title(String name, TitleCategory category)
    {
        this.Name = name;
        this.Category = category;
    }

    public String Name { get; set; }

    [Browsable(false)]
    public TitleCategory Category { get; private set; }

    public override string ToString()
    {
        return Name;
    }

    private Dictionary<String, Object> customFieldValues = 
                               new Dictionary<String, Object>();

    public Object this[String fieldName]
    {
        get
        {
            Object value = null;
            customFieldValues.TryGetValue(fieldName, out value);
            return value;
        }

        set
        {
            customFieldValues[fieldName] = value;
        }
    }
}

In addition to the Name property, I also have a Category property which specifies the type of the object. In addition, you will see that I also have a dictionary for the custom property values and an indexer that can be used to get and set custom properties on any object. Also notice the TypeDescriptionProvider attribute on the class that specifies TitleTypeDescriptionProvider as the type description provider.

The next thing to do is to add a class to represent a custom property field. All it would need would be the name of the field and its data type.

Tech Note: In my demo, I do not validate the data for the data type, but obviously, that would be a mandatory chore when putting this to practical use. Else, you'd have mismatching data which would pollute the runtime object.
C#
class CustomField
{
    public CustomField(String name, Type dataType)
    {
        Name = name;
        DataType = dataType;
    }

    public String Name { get; private set; }

    public Type DataType { get; private set; }
}

Now, we can write the TitleTypeDescriptionProvider class:

C#
class TitleTypeDescriptionProvider : TypeDescriptionProvider
{
    private static TypeDescriptionProvider defaultTypeProvider = 
                   TypeDescriptor.GetProvider(typeof(Title));

    public TitleTypeDescriptionProvider() : base(defaultTypeProvider)
    {
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, 
                                                            object instance)
    {
        ICustomTypeDescriptor defaultDescriptor = 
                              base.GetTypeDescriptor(objectType, instance);

        return instance == null ? defaultDescriptor : 
            new TitleCustomTypeDescriptor(defaultDescriptor, instance);
    }
}

The important method here is the GetTypeDescriptor override. The default descriptor is returned when the instance is null, otherwise we return a TitleCustomTypeDescriptor object. The TitleCustomTypeDescriptor is written as follows:

C#
class TitleCustomTypeDescriptor : CustomTypeDescriptor
{
    public TitleCustomTypeDescriptor(ICustomTypeDescriptor parent, object instance)
        : base(parent)
    {
        Title title = (Title)instance;

        customFields.AddRange(CustomFieldsGenerator.GenerateCustomFields(title.Category)
            .Select(f => new CustomFieldPropertyDescriptor(f)).Cast<PropertyDescriptor>());

    }

    private List<PropertyDescriptor> customFields = new List<PropertyDescriptor>();

    public override PropertyDescriptorCollection GetProperties()
    {
        return new PropertyDescriptorCollection(base.GetProperties()
            .Cast<PropertyDescriptor>().Union(customFields).ToArray());
    }

    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        return new PropertyDescriptorCollection(base.GetProperties(attributes)
            .Cast<PropertyDescriptor>().Union(customFields).ToArray());
    }

}

The crux of this class is that we generate a list of custom fields for the object based on the Category property. Then, in the GetProperties overrides, we add these properties (fields) to the default list. It's here that the entire dynamic nature of this design is revealed. This allows us to dynamically add new properties to an object type. For the demo, I have a CustomFieldsGenerator class that returns the dynamic fields.

C#
internal static IEnumerable<CustomField> GenerateCustomFields(TitleCategory category)
{
    List<CustomField> customFields = new List<CustomField>();

    switch(category)
    {
    case TitleCategory.Book:
        customFields.Add(new CustomField("Author", typeof(String)));
        customFields.Add(new CustomField("HardCover", typeof(bool)));
        customFields.Add(new CustomField("Amazon Rank", typeof(int)));
        break;

    case TitleCategory.Movie:
        customFields.Add(new CustomField("Director", typeof(String)));
        customFields.Add(new CustomField("Rating", typeof(MovieRating)));
        customFields.Add(new CustomField("Duration", typeof(TimeSpan)));
        customFields.Add(new CustomField("Release Date", typeof(DateTime)));
        break;
    }

    return customFields;
}

And each field is stored using a CustomFieldPropertyDescriptor class which is responsible for establishing the behavior of the custom properties.

C#
class CustomFieldPropertyDescriptor : PropertyDescriptor
{
    public CustomField CustomField { get; private set; }

    public CustomFieldPropertyDescriptor(CustomField customField)
        : base(customField.Name, new Attribute[0])
    {
        CustomField = customField;
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override Type ComponentType
    {
        get 
        {
            return typeof(Title);
        }
    }

    public override object GetValue(object component)
    {
        Title title = (Title)component;
        return title[CustomField.Name] ?? (CustomField.DataType.IsValueType ? 
            (Object)Activator.CreateInstance(CustomField.DataType) : null);
    }

    public override bool IsReadOnly
    {
        get 
        {
            return false;
        }
    }

    public override Type PropertyType
    {
        get
        {
            return CustomField.DataType;
        }
    }

    public override void ResetValue(object component)
    {
        throw new NotImplementedException();
    }

    public override void SetValue(object component, object value)
    {
        Title title = (Title)component;
        title[CustomField.Name] = value;
    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

The important methods here are the GetValue and SetValue overrides. I use the indexer of the Title class for both methods. Notice how I also return a default value based on the type of the object if the dynamic field has not been set on the Title object when the value is requested. Also see the PropertyType property - this allows us to specify the exact data type for a specific field. For example, the movie rating is an enumeration of type MovieRating which is respected by the property grid.

C#
enum MovieRating
{
    G,
    PG,
    PG13,
    R,
    NC17
}

Image 2

That's pretty much what's there to it. It's not just the property grid that respects type descriptors, any control that supports data binding will work just as expected. So, you could pass this to a data grid view or to a third party data visualization control and it'll all work just fine. Note that in code, you cannot access the dynamic properties the normal way, instead you'd have to access them via the indexer as shown below:

C#
Title title = new Title(name, TitleCategory.Movie);
title["Director"] = director;
title["Rating"] = rating;
title["Duration"] = duration;
title["Release Date"] = releaseDate;

Alright, that's it from me. As usual, all sorts of feedback is thanked for in advance.

History

  • June 14, 2008 - Article first published.

License

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


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Comments and Discussions

 
GeneralMy vote of 5 Pin
BillWoodruff27-May-23 7:18
professionalBillWoodruff27-May-23 7:18 
QuestionBinding with DataGridView - problem Pin
Member 1079854026-Mar-18 2:03
Member 1079854026-Mar-18 2:03 
QuestionGreat example Pin
ggeurts6-Jun-12 23:42
ggeurts6-Jun-12 23:42 
QuestionConversion to VB.NET Pin
geoffhirst2-Oct-11 10:07
geoffhirst2-Oct-11 10:07 
QuestionUser-defined dynamic properties Pin
emoscosocam22-Apr-11 6:58
emoscosocam22-Apr-11 6:58 
JokeThanks very much Pin
Drew Noakes14-Jan-09 8:52
Drew Noakes14-Jan-09 8:52 
GeneralRe: Thanks very much Pin
Nish Nishant14-Jan-09 8:58
sitebuilderNish Nishant14-Jan-09 8:58 
JokeThanks Pin
rcollina28-Aug-08 4:04
rcollina28-Aug-08 4:04 
GeneralRe: Thanks Pin
Nish Nishant14-Jan-09 8:59
sitebuilderNish Nishant14-Jan-09 8:59 
Questionhow can i get all properties using Reflection? Pin
guaike16-Jun-08 15:37
guaike16-Jun-08 15:37 
AnswerRe: how can i get all properties using Reflection? Pin
Nish Nishant16-Jun-08 15:45
sitebuilderNish Nishant16-Jun-08 15:45 
GeneralRe: how can i get all properties using Reflection? Pin
guaike16-Jun-08 20:20
guaike16-Jun-08 20:20 
So,Whether can use it in Custom control?
GeneralNice article. Pin
tonyt16-Jun-08 9:10
tonyt16-Jun-08 9:10 
GeneralRe: Nice article. Pin
Nish Nishant16-Jun-08 14:02
sitebuilderNish Nishant16-Jun-08 14:02 
GeneralRe: Nice article. [modified] Pin
tonyt17-Jun-08 1:03
tonyt17-Jun-08 1:03 
GeneralVery nice! Pin
philippe dykmans16-Jun-08 4:21
philippe dykmans16-Jun-08 4:21 
GeneralRe: Very nice! Pin
Nish Nishant16-Jun-08 13:58
sitebuilderNish Nishant16-Jun-08 13:58 

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.