How to use ObjectDataSource with ExpandoObject
Environment
Product | Progress® 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
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.
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:
//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.
Note that 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:
- Create one ExpandoObject with all properties that would be used later in the report;
- Add it to a collection (make sure that it is the first item in the collection);
-
Register the ExpandoObject type in the application Main method using the following code:
TypeDescriptor.AddProvider(new ExpandoObjectTypeDescriptionProvider(), typeof(ExpandoObject));
- Bind an ObjectDataSource to the created collection from step 2;
- In the report create a Data Item with property Visible = false;
- Set the DataSource of the hidden data item to be the newly created ObjectDataSource.