How to display a dynamic object in property grid?
Asked Answered
G

3

4

I have a custom object type which has to be editable in PropertyGrid:

public class CustomObjectType
{
    public string Name { get; set; }        
    public List<CustomProperty> Properties {get; set;}
}

Which has a list of custom properties:

public class CustomProperty
{
    public string Name { get; set; }
    public string Desc { get; set; }
    public Object DefaultValue { get; set; }    
    Type type;

    public Type Type
    {
        get
        {
            return type;
        }
        set
        {
                type = value;
                DefaultValue = Activator.CreateInstance(value);
        }              
    }
}

The main problem here is that the PropertyGrid control doesn't allow to edit, nor uses appropriate editing tools for property DefaultValue which is beforehand instantiated by setting value of CustomProperty's field Type.

Type of DefaultValue is only known at runtime.

Moreover I need to supply a custom TypeConverter for CustomProperty's property Type to show a drop-down list of supported types (for example, Int, String, Color, MyOwnClass).

How would i do that?

Grozny answered 16/8, 2010 at 8:19 Comment(0)
K
18

To go down this route, you would need to create a custom PropertyDescriptor per property. You would then expose that via a custom TypeConverter, or (alternatively) ICustomTypeDescriptor/TypeDescriptionProvider. Example:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
[TypeConverter(typeof(CustomObjectType.CustomObjectConverter))]
public class CustomObjectType
{
    [Category("Standard")]
    public string Name { get; set; }
    private readonly List<CustomProperty> props = new List<CustomProperty>();
    [Browsable(false)]
    public List<CustomProperty> Properties { get { return props; } }

    private Dictionary<string, object> values = new Dictionary<string, object>();

    public object this[string name]
    {
        get { object val; values.TryGetValue(name, out val); return val; }
        set { values.Remove(name); }
    }

    private class CustomObjectConverter : ExpandableObjectConverter
    {
        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
        {
            var stdProps = base.GetProperties(context, value, attributes);
            CustomObjectType obj = value as CustomObjectType;
            List<CustomProperty> customProps = obj == null ? null : obj.Properties;
            PropertyDescriptor[] props = new PropertyDescriptor[stdProps.Count + (customProps == null ? 0 : customProps.Count)];
            stdProps.CopyTo(props, 0);
            if (customProps != null)
            {
                int index = stdProps.Count;
                foreach (CustomProperty prop in customProps)
                {
                    props[index++] = new CustomPropertyDescriptor(prop);
                }
            }
            return new PropertyDescriptorCollection(props);
        }
    }
    private class CustomPropertyDescriptor : PropertyDescriptor
    {
        private readonly CustomProperty prop;
        public CustomPropertyDescriptor(CustomProperty prop) : base(prop.Name, null)
        {
            this.prop = prop;
        }
        public override string Category { get { return "Dynamic"; } }
        public override string Description { get { return prop.Desc; } }
        public override string Name { get { return prop.Name; } }
        public override bool ShouldSerializeValue(object component) { return ((CustomObjectType)component)[prop.Name] != null; }
        public override void ResetValue(object component) { ((CustomObjectType)component)[prop.Name] = null; }
        public override bool IsReadOnly { get { return false; } }
        public override Type PropertyType { get { return prop.Type; } }
        public override bool CanResetValue(object component) { return true; }
        public override Type ComponentType { get { return typeof(CustomObjectType); } }
        public override void SetValue(object component, object value) { ((CustomObjectType)component)[prop.Name] = value; }
        public override object GetValue(object component) { return ((CustomObjectType)component)[prop.Name] ?? prop.DefaultValue; }
    }
}


public class CustomProperty
{
    public string Name { get; set; }
    public string Desc { get; set; }
    public object DefaultValue { get; set; }
    Type type;

    public Type Type
    {
        get
        {
            return type;
        }
        set
        {
                type = value;
                DefaultValue = Activator.CreateInstance(value);
        }              
    }
}

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        var obj = new CustomObjectType
        {
            Name = "Foo",
            Properties =
            {
                new CustomProperty { Name = "Bar", Type = typeof(int), Desc = "I'm a bar"},
                new CustomProperty { Name = "When", Type = typeof(DateTime), Desc = "When it happened"},
            }
        };
        Application.Run(new Form { Controls = { new PropertyGrid { SelectedObject = obj, Dock = DockStyle.Fill } } });
    }
}
Kleon answered 16/8, 2010 at 9:33 Comment(5)
"To go down this route" - is there a more elegant way to achieve this?Grozny
@Marc Gravell, CustomObjectType.this[string].set{} should be values[name] = value; if properties are to be modified. Great bit of code, BTW.Ephemeron
The typeConverter seems to make the object incompatible with JsonConvert, is there a way to fix that or to turn it on/off as needed? also you are a lifesaver, code is amazingTilla
@TheLemon well, you can use TypeDescriptor.AddProvider/RemoveProvider, but that's ... kinda gnarly and I don't want to even start debugging that!Kleon
Awesome, one other question (sorry to keep bugging you), Your code works well for enums, but I can't get the property grid to display the default/prepopulated value. It stays blank until the user selects an optionTilla
G
3

I think Marc Gravell might have misunderstood the context a little.

I was trying to edit properties of CustomObjectTypes not the "CustomObjects" themselves.

Here's modified Marc's code that does that:

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

public class CustomObjectType
{
    [Category("Standard")]
    public string Name { get; set; }
    [Category("Standard")]
    public List<CustomProperty> Properties {get;set;}

    public CustomObjectType()
    {
        Properties = new List<CustomProperty>();
    }
}

[TypeConverter(typeof(ExpandableObjectConverter))]
public class Person
{
    public string Name {get;set;}
    public DateTime DateOfBirth { get; set; }
    public int Age { get; set; }
}

[TypeConverter(typeof(CustomProperty.CustomPropertyConverter))]
public class CustomProperty
{
    public CustomProperty()
    {
        Type = typeof(int);
        Name = "SomeProperty";    
    }

    private class CustomPropertyConverter : ExpandableObjectConverter
    {
        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
        {
            var stdProps = base.GetProperties(context, value, attributes);
            CustomProperty obj = value as CustomProperty;            
            PropertyDescriptor[] props = new PropertyDescriptor[stdProps.Count + 1];
            stdProps.CopyTo(props, 0);
            props[stdProps.Count] = new ObjectDescriptor(obj);

            return new PropertyDescriptorCollection(props);
        }
    }
    private class ObjectDescriptor : PropertyDescriptor
    {
        private readonly CustomProperty prop;
        public ObjectDescriptor(CustomProperty prop)
            : base(prop.Name, null)
        {
            this.prop = prop;
        }
        public override string Category { get { return "Standard"; } }
        public override string Description { get { return "DefaultValue"; } }
        public override string Name { get { return "DefaultValue"; } }
        public override string DisplayName { get { return "DefaultValue"; } }
        public override bool ShouldSerializeValue(object component) { return ((CustomProperty)component).DefaultValue != null; }
        public override void ResetValue(object component) { ((CustomProperty)component).DefaultValue = null; }
        public override bool IsReadOnly { get { return false; } }
        public override Type PropertyType { get { return prop.Type; } }
        public override bool CanResetValue(object component) { return true; }
        public override Type ComponentType { get { return typeof(CustomProperty); } }
        public override void SetValue(object component, object value) { ((CustomProperty)component).DefaultValue = value; }
        public override object GetValue(object component) { return ((CustomProperty)component).DefaultValue; }
    }

    private class CustomTypeConverter: TypeConverter
    {
        public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
        {
            return true;
        }

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
                return true;

            return base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            if (value.GetType() == typeof(string))
            {
                Type t = Type.GetType((string)value);

                return t;
            }

            return base.ConvertFrom(context, culture, value);

        }

        public override System.ComponentModel.TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
        {
            var types = new Type[] { 
                typeof(bool), 
                typeof(int), 
                typeof(string), 
                typeof(float),
                typeof(Person),
                typeof(DateTime)};

            TypeConverter.StandardValuesCollection svc =
                new TypeConverter.StandardValuesCollection(types);
            return svc;
        }
    }

    [Category("Standard")]
    public string Name { get; set; }
    [Category("Standard")]
    public string Desc { get; set; }

    [Browsable(false)]

    public object DefaultValue { get; set; }

    Type type;

    [Category("Standard")]
    [TypeConverter(typeof(CustomTypeConverter))]       
    public Type Type
    {
        get
        {
            return type;
        }
        set
        {
            type = value;
            if (value == typeof(string))
                DefaultValue = "";
            else
                DefaultValue = Activator.CreateInstance(value);
        }
    }
}

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        var obj = new CustomObjectType
        {
            Name = "Foo",
            Properties =
            {
                new CustomProperty { Name = "Bar", Type = typeof(int), Desc = "I'm a bar"},
                new CustomProperty { Name = "When", Type = typeof(DateTime), Desc = "When it happened"},
            }
        };
        Application.Run(new Form { Controls = { new PropertyGrid { SelectedObject = obj, Dock = DockStyle.Fill } } });
    }
}

It works, however, I find it a rather awkward solution. Because i'm supplying a PropertyDescriptor for Object and CustomPropertyConverter for CustomProperty, both of which doesn't actually do anything meaningful. Yet I can't remove them either.

Is there an elegant way to allow editing properties of type Object(like the DefaultValue) using appropriate editors according to runtime information of the object?

Grozny answered 16/8, 2010 at 13:33 Comment(0)
D
0
public override void SetValue(object component, object value)           
{
    //((CustomObjectType)component)[prop.Name] = value;

    CustomObjectType cot = (CustomObjectType)component;

    CustomProperty cp = cot.Properties.FirstOrDefault(r => r.Name.Equals(prop.Name));
    if (cp == null) return;

    cp.DefaultValue = value;
}
Drawer answered 11/7, 2019 at 8:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.