3

Ok, this is a tough one.

Introduction: My idea is to attach an instanciated QueryBuilder class which I wrote, to a PropertyGrid. The QueryBuilder class now contains a couple of fields, which are hardcoded like in the example below. Thus allowing a user to specify, which fields should be used in a query in what way (sorted, grouped, and so on). After the user having specified all the settings to these properties (by code or via the PropertyGrid GUI), the QueryBuilder is able to produce a query. Everything is working fine like that. Pseudo code:

class QueryBuilder {
  public QBField name {get; set;}
  public QBField prename {get; set;}
  public QBField zip {get; set;}
  // ...

  public void QueryBuilder() {
    name = new QBField();
    prename = new QBField();
    // ...
  }

  public getQuery() {
    // logic to build the query
  }
}

class QBField {
  public bool shown {get; set;}
  public bool sortby {get; set;}
  public bool groupby {get; set;}
}

Challenge: Now instead of hardcoding each field as public properties in the QueryBuilder class, I was wondering how I could use i.e. a List<string> containing all my fields to "populate" my instanciated QueryBuilder with these properties.

So this leads to three questions:

  1. Could this be accomplished by somehow overriding GetProperties() of the Type of the QueryBuilder class, and if yes, how is it best done?

  2. How can I then iterate through all of these at runtime generated QBField properties and instanciate them? Idea: PropertyDescriptors and Activators?

  3. How can I iterate through all of these properties to read the values of each QBField object? The problem I ran in was, that when reading the Properties of QBField with reflection and trying getValue(obj, null), of course the first parameter needed is an object, which I do not know since I have lots of these QBField objects. Perhaps putting all my QBFields into a List<QBField> and iterating through it? Would that work in this example?

I'm just a bit lost but I feel that I'm very close to the solution. Therefore any help or just pointers in the right direction are most greatly appreciated!

Gregor Favre
  • 155
  • 1
  • 10

1 Answers1

5

PropertyGrid can be influenced via TypeConverter, ICustomTypeDescriptor and/or TypeDescriptionProvider. Of these, TypeConverter is the simplest, by overriding GetProperties (and mark it as supported).

In any event, you will also need to write a PropertyDescriptor implementation that knows how to take a field and an object, and get/set the value, i.e.

public override void SetValue(object component, object value) {
    ((YourType)component)[fieldNameSetInConstructor] = value;
}

Here's a basic property bag that exposes everything as string; obviously as you extend this (different property types, change-notification, etc) it gets more complex very quickly. Note also that this TypeConverter approach only works for PropertyGrid; for DataGridView etc you'll need either ICustomTypeDescriptor or TypeDescriptionProvider. For collections you'll need ITypedList. And there are about 20 other interfaces around the edges for specific scenarios. But you get the point ;p The key thing is that our PropertyDescriptor acts as the translation between your actual model (the dictionary in my case), and the model you expose to TypeDescriptor (the fake properties per key).

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;


static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        var bag = new BasicPropertyBag { Properties = {
            new MetaProp("Name", typeof(string)),
            new MetaProp("Description", typeof(string)),
            new MetaProp("DateOfBirth", typeof(DateTime)
                , new CategoryAttribute("Personal"), new DisplayNameAttribute("Date Of Birth"))
        } };
        bag["Name"] = "foo";
        bag["DateOfBirth"] = DateTime.Today;
        Application.Run(new Form { Controls = { new PropertyGrid { Dock = DockStyle.Fill, SelectedObject = bag } } });
    }
}

public class MetaProp
{
    public MetaProp(string name, Type type, params Attribute[] attributes)
    {
        this.Name = name;
        this.Type = type;
        if (attributes != null)
        {
            Attributes = new Attribute[attributes.Length];
            attributes.CopyTo(Attributes, 0);
        }
    }
    public string Name { get; private set; }
    public Type Type { get; private set; }
    public Attribute[] Attributes { get; private set; }
}

[TypeConverter(typeof(BasicPropertyBagConverter))]
class BasicPropertyBag
{

    private readonly List<MetaProp> properties = new List<MetaProp>();
    public List<MetaProp> Properties { get { return properties; } }
    private readonly Dictionary<string, object> values = new Dictionary<string, object>();

    public object this[string key]
    {
        get { object value; return values.TryGetValue(key, out value) ? value : null; }
        set { if (value == null) values.Remove(key); else values[key] = value; }
    }

    class BasicPropertyBagConverter : ExpandableObjectConverter
    {
        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
        {
            PropertyDescriptor[] metaProps = (from prop in ((BasicPropertyBag)value).Properties
                                              select new PropertyBagDescriptor(prop.Name, prop.Type, prop.Attributes)).ToArray();
            return new PropertyDescriptorCollection(metaProps);
        }
    }
    class PropertyBagDescriptor : PropertyDescriptor
    {
        private readonly Type type;
        public PropertyBagDescriptor(string name, Type type, Attribute[] attributes)
            : base(name, attributes) {
            this.type = type;
        }
        public override Type PropertyType { get { return type; } }
        public override object GetValue(object component) { return ((BasicPropertyBag)component)[Name]; }
        public override void SetValue(object component, object value) { ((BasicPropertyBag)component)[Name] = (string)value; }
        public override bool ShouldSerializeValue(object component) { return GetValue(component) != null; }
        public override bool CanResetValue(object component) { return true; }
        public override void ResetValue(object component) { SetValue(component, null); }
        public override bool IsReadOnly { get { return false; } }
        public override Type ComponentType { get { return typeof(BasicPropertyBag); } }
    }

}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks Marc for pointing me to the right direction. Got the things so far working but have no idea how to create new properties. Have implemented GetProperties and am able to filter properties there. But how to add properties (i.e. from my List? Any idea? – Gregor Favre Nov 04 '10 at 21:53
  • @Gregor - what is the `string` in that case? I've written quite a few of these dynamic/property-bag models, and the first thing to uderstand is the exact scenario. I'll do a quick simple mockup, though... – Marc Gravell Nov 04 '10 at 22:42
  • @Gregor - note I have a number of other posts on SO and elsewhere discussing more complex corners of this. It is not a trivial area, I'm afraid. – Marc Gravell Nov 04 '10 at 22:59
  • @Marc - Thank you very much! This really helped me a lot, from this point on, I should be able to finish the scenario by using a List of objects instead of strings as properties. These objects have static properties, so this should be quite similar to your example. – Gregor Favre Nov 05 '10 at 08:10
  • @Gregor - indeed you should be able to *combine* the `PropertyDescritor` arrays to get a mix of reflective and dynamic properties. – Marc Gravell Nov 05 '10 at 08:15
  • Okay that works all marvelous, have now my objects as properties all loading dynamically and expandable. Just juggling a bit around with setting categories dynamically - ie not in PropertyBagDescriptor but directly in the Converter or (but would be much more complex) when adding a new object from the main routine (ie string in your example)... any chance for a last pointer? – Gregor Favre Nov 05 '10 at 15:45
  • @Gregor isn't Category one of the overrides? – Marc Gravell Nov 05 '10 at 16:44
  • @Marc yes that's true. But if I understood correctly, this ist just the main descriptor for all of my properties. I would like to set categories depending on certain content of the objects. I thought about making a descriptor for each category and assigning it then dynamically - but am not sure, if that's the correct way of doing it? – Gregor Favre Nov 08 '10 at 10:32
  • @Gregor - updated to illustrate; you can of course use any logic you need to set the category per-instance, etc – Marc Gravell Nov 08 '10 at 11:07
  • @Marc With your help I'm now able to set everything as dynamic as it could ever be :) Thank you very much! – Gregor Favre Nov 08 '10 at 20:36
  • Marc, have you ever had the issue, where you were updating a property (an object) in the propertygrid by gui, and the string representation of the object in the PG not being updated? Here it updates only when i click in the property or when I declare the properties by [RefreshProperties(RefreshProperties.All)]. With RefreshProperties, although the node collapses everytime I change something in the gui... – Gregor Favre Nov 17 '10 at 09:13
  • @Gregor - I've seen that on *related* properties i.e. I update `A` in the UI, which (via the setter) updates `B` - in the UI `A` is up to date but `B` is stale. IIRC `RefreshProperties` is the way to look at this, but you could *try* using the change events on the `PropertyDescriptor` (but do a simple test first, as I can't remember if this is respected by PG) – Marc Gravell Nov 17 '10 at 12:46
  • @Marc Thx, will try this. The RefreshProperties unfortunately resets the expanded/collapsed states of my expandable objects. I think I will open a new question, certainly someone encountered something similar (browsing the web approves this). Thx anyway for all your help! If you tell me your postal address, I'll send you a postcard from switzerland :) – Gregor Favre Nov 17 '10 at 20:42