New to Telerik ReportingStart a free 30-day trial

How to use ObjectDataSource with ExpandoObject

Environment

ProductProgress® Telerik® Reporting

Description

When binding ObjectDataSource component to a collection of ExpandoObject instances the Reporting engine expects the collection to contain a list of business objects with known properties. As ExpandoObject uses an internal dictionary of string and object (IDictionary<string,object>) to store the dynamically added properties the engine is not able to find them and throws an error: "The expression contains object 'PropertyName' that is not defined in the current context".

Solution

In order to expose ExpandoObject properties to Reporting engine, a custom TypeDescriptor needs to be provided that will determine the actual properties of the object.

C#
	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);
        }
    }
	public class ExpandoObjectTypeDescriptor : ICustomTypeDescriptor
    {
        private readonly IDictionary<string,object> m_Instance;
        public ExpandoObjectTypeDescriptor(dynamic instance)
        {
            m_Instance = instance as IDictionary<string, object>;
        }
        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 ICustomTypeDescriptor.GetEvents()
        {
            return TypeDescriptor.GetEvents(this, true);
        }
        public TypeConverter GetConverter()
        {
            return TypeDescriptor.GetConverter(this, true);
        }
        public object GetPropertyOwner(PropertyDescriptor pd)
        {
            return m_Instance;
        }
        public AttributeCollection GetAttributes()
        {
            return TypeDescriptor.GetAttributes(this, true);
        }
        public object GetEditor(Type editorBaseType)
        {
            return TypeDescriptor.GetEditor(this, editorBaseType, true);
        }
        public PropertyDescriptor GetDefaultProperty()
        {
            return null;
        }
        PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
        {
            return ((ICustomTypeDescriptor)this).GetProperties(new Attribute[0]);
        }
        public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
        {
            return new PropertyDescriptorCollection(
                m_Instance.Keys
                          .Select(x => new ExpandoObjectPropertyDescriptor(m_Instance, x))
                          .ToArray<PropertyDescriptor>());
        }
        class ExpandoObjectPropertyDescriptor : PropertyDescriptor
        {
            private readonly IDictionary<string, object> m_Instance;
            private readonly string m_Name;
            public ExpandoObjectPropertyDescriptor(IDictionary<string, object> instance, string name)
                : base(name, null)
            {
                m_Instance = instance;
                m_Name = name;
            }
            public override Type PropertyType
            {
                get { return m_Instance[m_Name].GetType(); }
            }
            public override void SetValue(object component, object value)
            {
                m_Instance[m_Name] = value;
            }
            public override object GetValue(object component)
            {
                return (((IDictionary<string, object>)component))[m_Name];
            }
            public override bool IsReadOnly
            {
                get
                {
                    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; }
            }
        }
    }

Custom provider can be attached to a single instance or to all instances of ExpandoObject type:

C#
//attach to a single instance
dynamic newObj = new ExpandoObject();
TypeDescriptor.AddProvider(new ExpandoObjectTypeDescriptionProvider(), newObj);
//attach to all instances
TypeDescriptor.AddProvider(new ExpandoObjectTypeDescriptionProvider(), typeof(ExpandoObject));

Suggested Workarounds

In case if you want to add a second data item with a second list of ExpandoObjects (having different properties) it shows error: "The expression contains object 'ProperyName' that is not defined in the current context." on the second one.

The Telerik Reporting engine expects the ObjectDataSource collection to include a list of business objects with known properties and there is no out-of-the-box functionality to contain collection of ExpandoObject. So, only the first ExpandoObject properties that are added to a collection would be available in the report to feed the ObjectDataSource.

However, the possible workaround is available by following these steps:

  1. Create one ExpandoObject with all properties that would be used later in the report;

  2. Add it to a collection (make sure that it is the first item in the collection);

  3. Register the ExpandoObject type in the application Main method using the following code:

    C#
    TypeDescriptor.AddProvider(new ExpandoObjectTypeDescriptionProvider(), typeof(ExpandoObject));
  4. Bind an ObjectDataSource to the created collection from step 2;

  5. In the report create a Data Item with property Visible = false;

  6. Set the DataSource of the hidden data item to be the newly created ObjectDataSource.