Exposing properties of an ExpandoObject
Asked Answered
N

2

7

I've got an ExpandoObject that I'm sending to an external library method which takes an object. From what I've seen this external lib uses TypeDescriptor.GetProperties internally and that seems to cause some problems with my ExpandoObject.

I could go with an anonymous object instead and that seems to work but it much more convenient for me to use the ExpandoObject.

Do I need to construct my own DynamicObject and take care of it myself by implementing ICustomTypeDescriptor or am I missing something here.

Ideas?


Update

Besides the answer by somedave below (as per the comments), I added this class

public class ExpandoObjectTypeDescriptionProvider : TypeDescriptionProvider
{
    private static readonly TypeDescriptionProvider m_Default = TypeDescriptor.GetProvider(typeof(ExpandoObject));

    public ExpandoObjectTypeDescriptionProvider()
        :base(m_Default)
    {
    }

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

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

and registered it like this:

dynamic parameters = new ExpandoObject();
TypeDescriptor.AddProvider(new ExpandoObjectTypeDescriptionProvider(), parameters);
Nihility answered 15/5, 2013 at 14:6 Comment(2)
Do you have a list of the property names on your end?Filch
not ahead of time(compile time)...hence the ExpandoObjectNihility
E
11

Implementing ICustomTypeDescriptor actually isn't all that hard. Here is some sample code I adapted from some work I did with WinForms property grids (which uses TypeDescriptor and PropertyDescriptor). The trick is to also implement an appropriate PropertyDescriptor class that you can pass back from ICustomTypeDescriptor.GetProperties(). Thankfully the ExpandoObject makes this pretty simple by implementing IDictionary<string, object> for dynamic retrieval of it's keys and values. Keep in mind that this may or may not work correctly (I haven't tested it) and it probably won't work for ExpandoObjects with lots of nested properties.

public class ExpandoTypeDescriptor : ICustomTypeDescriptor
{
    private readonly ExpandoObject _expando;

    public ExpandoTypeDescriptor(ExpandoObject expando)
    {
        _expando = expando;
    }

    // Just use the default behavior from TypeDescriptor for most of these
    // This might need some tweaking to work correctly for ExpandoObjects though...

    public string GetComponentName()
    {
        return TypeDescriptor.GetComponentName(this, true);
    }

    public EventDescriptor GetDefaultEvent()
    {
        return TypeDescriptor.GetDefaultEvent(this, true);
    }

    public string GetClassName()
    {
        return TypeDescriptor.GetClassName(this, true);
    }

    public EventDescriptorCollection GetEvents(Attribute[] attributes)
    {
        return TypeDescriptor.GetEvents(this, attributes, true);
    }

    EventDescriptorCollection System.ComponentModel.ICustomTypeDescriptor.GetEvents()
    {
        return TypeDescriptor.GetEvents(this, true);
    }

    public TypeConverter GetConverter()
    {
        return TypeDescriptor.GetConverter(this, true);
    }

    public object GetPropertyOwner(PropertyDescriptor pd)
    {
        return _expando;
    }

    public AttributeCollection GetAttributes()
    {
        return TypeDescriptor.GetAttributes(this, true);
    }

    public object GetEditor(Type editorBaseType)
    {
        return TypeDescriptor.GetEditor(this, editorBaseType, true);
    }

    public PropertyDescriptor GetDefaultProperty()
    {
        return null;
    }

    // This is where the GetProperties() calls are
    // Ignore the Attribute for now, if it's needed support will have to be implemented
    // Should be enough for simple usage...

    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
        return ((ICustomTypeDescriptor)this).GetProperties(new Attribute[0]);
    }

    public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        // This just casts the ExpandoObject to an IDictionary<string, object> to get the keys
        return new PropertyDescriptorCollection(
            ((IDictionary<string, object>)_expando).Keys
            .Select(x => new ExpandoPropertyDescriptor(((IDictionary<string, object>)_expando), x))
            .ToArray());
    }

    // A nested PropertyDescriptor class that can get and set properties of the
    // ExpandoObject dynamically at run time
    private class ExpandoPropertyDescriptor : PropertyDescriptor
    {
        private readonly IDictionary<string, object> _expando;
        private readonly string _name;

        public ExpandoPropertyDescriptor(IDictionary<string, object> expando, string name)
            : base(name, null)
        {
            _expando = expando;
            _name = name;
        }

        public override Type PropertyType
        {
            get { return _expando[_name].GetType(); }
        }

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

        public override object GetValue(object component)
        {
            return _expando[_name];
        }

        public override bool IsReadOnly
        {
            get
            {
                // You might be able to implement some better logic here
                return false;
            }
        }

        public override Type ComponentType
        {
            get { return null; }
        }

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

        public override void ResetValue(object component)
        {
        }

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

        public override string Category
        {
            get { return string.Empty; }
        }

        public override string Description
        {
            get { return string.Empty; }
        }
    }
}
Epicycloid answered 15/5, 2013 at 14:41 Comment(3)
That's great but I'm actually sending my ExpandoObject to the external library and ExpandoObj is sealed so I can't inherit from it. So how do I tell it to use my own implementation of ICustomTypeDescriptor then...?Nihility
Ah, I didn't fully understand the problem. I think the trick is going to be creating one additional class, a TypeDescriptionProvider implementation. It has a method GetTypeDescriptor() (with several overloads) that returns an ICustomTypeDescriptor for a given object or type. Rig that up to return an instance of the ExpandoTypeDescriptor above. Then register the provider using TypeDescriptor.AddProvider(). I think that should get everything working together. Does that make sense?Epicycloid
I'll mark your answer as the correct one since it worked with the things you added in the comments...I'll edit my question to highlight that. tnx!!Nihility
L
1

The only way I could get the OP's code to work with Marc's ExpandoTypeDescriptor code was to modify the OP's call to GetTypeDescriptor, casting the return value to ExpandoObject.

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

     return instance == null ? defaultDescriptor :
            new ExpandoTypeDescriptor((ExpandoObject)instance);
}
Lamelliform answered 18/9, 2020 at 12:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.