Hi,
I need to bind a xamDataChart to a collection of objects of type "dynamic", but the chart doesn't draw the points. If I bind the collection to a DataGrid I can see the values of each dynamic object. My dynamic object has a class property "X", and for each serie that I add on runtime, I add a new property "Y" to my object:
X, Y1, Y2, ..., Yn
The xamDataChart supports binding to objects of type dynamic?
Thanks.
Thank you Graham, It works great...
However, I see that with many series and many points every second this is slow (I can have 15 series or more receiving 10 point every second sometimes), but I think this is the best solution in this momment using dynamic objects.
As I don't know the property name of the dynamic object until I add a new serie on runtime, I need to use my DynamicPointViewModel (instead of ExpandoObject) with the capability:
myObj["Y" + idSerie] = value;
but this makes slower your solution, I don´t know why... I'll check it.
I have another solution not using dynamic objects, It's using collections for each serie and works fine too.
I'll decide wich solution to implement.
Thank you so much!
This is just one way of how you could make a collection of dynamic objects "solidify" into a collection of concrete typed objects for the chart to reflect against:
<Window.Resources> <local:TestData x:Key="dynamicData" /> <local:SolidifiedView x:Key="data" Inner="{StaticResource dynamicData}" /> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <igChart:XamDataChart x:Name="theChart" HorizontalZoomable="True" VerticalZoomable="True"> <igChart:XamDataChart.Axes> <igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}" Label="{}{Label}"/> <igChart:NumericYAxis x:Name="yAxis"/> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:LineSeries x:Name="line" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data}" ValueMemberPath="Value"/> <igChart:LineSeries x:Name="line2" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data}" ValueMemberPath="Value2"/> </igChart:XamDataChart.Series> </igChart:XamDataChart> <Button x:Name="theButton" Content="Click" Click="theButton_Click" Grid.Row="1"/> </Grid>
public partial class MainWindow : System.Windows.Window { public MainWindow() { InitializeComponent(); } private void theButton_Click( object sender, System.Windows.RoutedEventArgs e) { var td = Resources["dynamicData"] as TestData; dynamic newObj = new ExpandoObject(); newObj.Label = td.Count.ToString(); newObj.Value = (double)td.Count; newObj.Value2 = (double)td.Count + 1; td.Add(newObj); } } public class TestData : ObservableCollection<IDynamicMetaObjectProvider> { public TestData() { for (int i = 0; i < 10; i++) { dynamic newObj = new ExpandoObject(); newObj.Label = i.ToString(); newObj.Value = (double)i; newObj.Value2 = (double)i + 1; Add(newObj); } } } public class SolidifiedView : ObservableCollection<object> { private IList<IDynamicMetaObjectProvider> _inner; public IList<IDynamicMetaObjectProvider> Inner { get { return _inner; } set { IList<IDynamicMetaObjectProvider> oldValue = _inner; _inner = value; OnInnerChanged(oldValue, _inner); } } private void OnInnerChanged( IList<IDynamicMetaObjectProvider> oldValue, IList<IDynamicMetaObjectProvider> newValue) { if (oldValue != null && oldValue is INotifyCollectionChanged) { ((INotifyCollectionChanged)oldValue).CollectionChanged -= DataSolidifier_CollectionChanged; } if (newValue != null && newValue is INotifyCollectionChanged) { ((INotifyCollectionChanged)newValue).CollectionChanged += DataSolidifier_CollectionChanged; } ResetItems(); } private void ResetItems() { this.Clear(); if (Inner == null) { return; } foreach (var item in Inner) { var newItem = Proxy(item); Add(newItem); } } protected override void ClearItems() { foreach (var item in this) { if (item is ProxyBase) { ((ProxyBase)item) .Detach(); } } } void DataSolidifier_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: foreach (var item in e.NewItems) { var newItem = Proxy(item); this.Add(newItem); } break; case NotifyCollectionChangedAction.Remove: foreach (var item in e.OldItems) { var toRemove = this[e.OldStartingIndex]; if (toRemove is ProxyBase) { ((ProxyBase)toRemove).Detach(); } RemoveAt(e.OldStartingIndex); } break; case NotifyCollectionChangedAction.Replace: foreach (var item in e.OldItems) { var toRemove = this[e.OldStartingIndex]; if (toRemove is ProxyBase) { ((ProxyBase)toRemove).Detach(); } RemoveAt(e.OldStartingIndex); } int index1 = e.NewStartingIndex; foreach (var item in e.NewItems) { var newItem = Proxy(item); this.Insert(index1++, newItem); } break; case NotifyCollectionChangedAction.Move: List<object> toMove = this.Skip(e.OldStartingIndex) .Take(e.OldItems.Count).ToList(); for (int i = 0; i < e.OldItems.Count; i++) { RemoveAt(e.OldStartingIndex); } int index2 = e.NewStartingIndex; foreach (var item in toMove) { Insert(index2++, item); } break; case NotifyCollectionChangedAction.Reset: ResetItems(); break; } } private object Proxy(object item) { if (item is IDynamicMetaObjectProvider) { var list = new PropertyInfoList( item as IDynamicMetaObjectProvider); var proxy = ProxyCreator.GetProxyInstance( list, item as IDynamicMetaObjectProvider); return proxy; } else { return item; } } } public class PropertyInfoItem { public string PropertyName { get; set; } public Type PropertyType { get; set; } public override string ToString() { return "{" + PropertyName + "}:[" + PropertyType.FullName + "]"; } } public class PropertyInfoList : List<PropertyInfoItem> { public PropertyInfoList(IDynamicMetaObjectProvider dyn) { foreach (var member in dyn.GetMetaObject( Expression.Constant(dyn)) .GetDynamicMemberNames()) { Add(new PropertyInfoItem() { PropertyName = member, PropertyType = typeof(object) }); } } public override string ToString() { var sb = new StringBuilder(); var props = from item in this orderby item.PropertyName select item; foreach (var prop in props) { sb.Append(prop.ToString()); } return sb.ToString(); } } public abstract class ProxyBase : INotifyPropertyChanged { private IDynamicMetaObjectProvider _inner; public ProxyBase(IDynamicMetaObjectProvider inner) { _inner = inner; if (_inner is INotifyPropertyChanged) { ((INotifyPropertyChanged)_inner).PropertyChanged += ProxyBase_PropertyChanged; } } public void Detach() { if (_inner == null) { return; } if (_inner is INotifyPropertyChanged) { ((INotifyPropertyChanged)_inner).PropertyChanged -= ProxyBase_PropertyChanged; } } protected Dictionary<string, bool> _existingProperties = new Dictionary<string, bool>(); void ProxyBase_PropertyChanged( object sender, PropertyChangedEventArgs e) { RaisePropertyChanged(e.PropertyName); } public event PropertyChangedEventHandler PropertyChanged; private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged( this, new PropertyChangedEventArgs( propertyName)); } } private Dictionary<string, CallSite<Func<CallSite, object, object>>> _sites = new Dictionary<string, CallSite<Func<CallSite, object, object>>>(); public object GetPropertyByName(string propertyName) { CallSite<Func<CallSite, object, object>> site; if (!_sites.TryGetValue(propertyName, out site)) { site = CallSite<Func<CallSite, object, object>>.Create( Microsoft.CSharp.RuntimeBinder.Binder.GetMember( CSharpBinderFlags.None, propertyName, typeof(ProxyBase), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })); _sites.Add(propertyName, site); } return site.Target.Invoke(site, _inner); } } public static class ProxyCreator { private static Dictionary< string, Func<IDynamicMetaObjectProvider, object>> _cachedTypes = new Dictionary< string, Func<IDynamicMetaObjectProvider, object>>(); private static object _lock = new object(); public static object GetProxyInstance( PropertyInfoList list, IDynamicMetaObjectProvider toWrap) { var type = GetProxyType(list); return type(toWrap); } private static Func<IDynamicMetaObjectProvider, object> GetProxyType(PropertyInfoList list) { string key = list.ToString(); Func<IDynamicMetaObjectProvider, object> output = null; lock (_lock) { if (_cachedTypes.TryGetValue(key, out output)) { return output; } } output = CreateProxyType(list); lock (_lock) { if (!_cachedTypes.ContainsKey(key)) { _cachedTypes.Add(key, output); } } return output; } private static AssemblyBuilder _ab = null; private static ModuleBuilder _mb = null; private static Func<IDynamicMetaObjectProvider, object> CreateProxyType(PropertyInfoList list) { lock (_lock) { if (_ab == null) { AssemblyName assmName = new AssemblyName("DynamicAssembly"); _ab = AppDomain.CurrentDomain.DefineDynamicAssembly( assmName, AssemblyBuilderAccess.Run); _mb = _ab.DefineDynamicModule(assmName.Name); _getPropertyValue = typeof(ProxyBase).GetMethod( "GetPropertyByName"); _baseCons = typeof(ProxyBase).GetConstructor( new Type[] { typeof(IDynamicMetaObjectProvider) }); } var tb = _mb.DefineType( Guid.NewGuid().ToString() + "__proxy", TypeAttributes.Public | TypeAttributes.Class, typeof(ProxyBase)); foreach (var prop in list) { CreateProperty(tb, prop); } ConstructorBuilder cons = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(IDynamicMetaObjectProvider) } ); ILGenerator ilGen = cons.GetILGenerator(); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldarg_1); ilGen.Emit(OpCodes.Call, _baseCons); ilGen.Emit(OpCodes.Ret); Type type = tb.CreateType(); ConstructorInfo typeCons = type.GetConstructor( new Type[] { typeof (IDynamicMetaObjectProvider) }); ParameterExpression param = Expression.Parameter( typeof(IDynamicMetaObjectProvider)); Func<IDynamicMetaObjectProvider,object> res = Expression.Lambda<Func<IDynamicMetaObjectProvider,object>>( Expression.New(typeCons, param), param).Compile(); return res; } } private static MethodInfo _getPropertyValue; private static ConstructorInfo _baseCons; private static void CreateProperty(TypeBuilder tb, PropertyInfoItem prop) { PropertyBuilder pb = tb.DefineProperty( prop.PropertyName, PropertyAttributes.None, prop.PropertyType, null); MethodBuilder mb = tb.DefineMethod( "get_" + prop.PropertyName, MethodAttributes.Public | MethodAttributes.HideBySig, typeof(object), null); ILGenerator ilGen = mb.GetILGenerator(); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldstr, prop.PropertyName); ilGen.Emit(OpCodes.Call, _getPropertyValue); ilGen.Emit(OpCodes.Ret); pb.SetGetMethod(mb); } }
Hope this helps!
-Graham
The chart expects to be able to reflect against the objects that are in its collection. When you present dynamic objects they do not actually have the properties to reflect against. Some of the WPF controls use TypeDescriptor information to get the appropriate property info from dynamic objects in order to know what properties they contain. The chart does not do any of this, however.
You have multiple ways available to you to get the data into a format that the chart expects. The first I described to you was to use a proxy that manually copies the dynamic property values into a concrete class by using the fact that the dynamic objects in question implemented IDictionary. If you are using the same collection for multiple series, then you would need to add additional concrete properties to the concrete class, or create multiple instances of the proxy that map to different properties of the underlying collection.
Below I'll present another way that you can go about automatically turning a collection of dynamic objects into a collection of concrete objects so that the chart can reflect against them appropriately.
Hi Graham, thank you for your reply.
I think this solution is for one serie because the class ChartDataItem only has one property Value, but I need to bind to many series (starting with 0 and on runtime I will add the series that I want to see), so my object should have many properties Value, one for each serie added on runtime, that's because I have to create a dynamic object.I have created a class that inherits from DynamicObject, I tried with ExpandoObject too, but it's the same result.This is my dynamic object class: public class DynamicPointViewModel : DynamicObject, INotifyPropertyChanged { private DateTime x; Dictionary<string, object> dynamicProperties = new Dictionary<string, object>(); public event PropertyChangedEventHandler PropertyChanged; public DateTime X { get { return this.x; } set { if (this.x != value) { this.x = value; this.OnPropertyChanged("X"); } } } protected virtual void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { var e = new PropertyChangedEventArgs(propertyName); this.PropertyChanged(this, e); } } public override bool TryGetMember(GetMemberBinder binder, out object result) { if (this.dynamicProperties.ContainsKey(binder.Name)) { result = this.dynamicProperties[binder.Name]; } else { this.dynamicProperties[binder.Name] = null; result = null; } return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { this.dynamicProperties[binder.Name] = value; this.OnPropertyChanged(binder.Name); return true; } }In my ViewModel I have an ObservableCollection:
private ObservableCollection<dynamic> guideSerie;
public ObservableCollection<dynamic> GuideSerie { get { return this.guideSerie; } set { if (this.guideSerie != value) { this.guideSerie = value; base.OnPropertyChanged("GuideSerie"); } } }When I receive a new point I add the values to my object. In this momment for testing I have the follow hardcode:public void OnSerieNewSample(object sender, TagChangedEventArgs e){ System.Windows.Application.Current.Dispatcher.Invoke((System.Threading.ThreadStart)delegate { SerieViewModel serie = sender as SerieViewModel; int index = this.Series.IndexOf(serie); dynamic pointValue = new DynamicPointViewModel(); pointValue.X = e.History.AcquisitionBegin.DateTime; if (index == 0) { pointValue.Y1 = e.History.Value; } else if (index == 1) { pointValue.Y2 = e.History.Value; } if (this.GuideSerie.Count > 0) { dynamic lastPointValue = this.GuideSerie.Last(); if (pointValue.Y1 == null) { pointValue.Y1 = lastPointValue.Y1; } else if (pointValue.Y2 == null) { pointValue.Y2 = lastPointValue.Y2; } } this.GuideSerie.Add(pointValue); }}When I receive a new point, I set the value to the corresponding serie position in the DynamicObject (Y1 or Y2), and set null or the last value for the other serie position.And this is my xaml: <ig:XamDataChart Name="chart" DataContext="{Binding GuideSerie}"> <ig:XamDataChart.Axes> <ig:CategoryDateTimeXAxis x:Name="XAxis" ItemsSource="{Binding}" DateTimeMemberPath="X" Label="{}{0:G}" /> <ig:NumericYAxis x:Name="YAxis" /> </ig:XamDataChart.Axes> <ig:XamDataChart.Series> <ig:LineSeries Title="Serie Y1" XAxis="{Binding ElementName=XAxis}" YAxis="{Binding ElementName=YAxis}" MarkerType="Circle" ItemsSource="{Binding}" ValueMemberPath="Y1" /> <ig:LineSeries Title="Serie Y2" XAxis="{Binding ElementName=XAxis}" YAxis="{Binding ElementName=YAxis}" MarkerType="Circle" ItemsSource="{Binding}" ValueMemberPath="Y2" /> </ig:XamDataChart.Series> </ig:XamDataChart>As you say if the chart doesn't support binding to dynamic objects, this won't work, do you have any workaround?Thank you.
Here's how you could proxy a list of ExpandoObjects to bind them to the chart:
<Window.Resources> <local:TestData x:Key="dynamicData" /> <local:ChartData x:Key="data" LabelPath="Label" ValuePath="Value" ItemsSource="{StaticResource dynamicData}" /> </Window.Resources> <Grid> <igChart:XamDataChart x:Name="theChart" > <igChart:XamDataChart.Axes> <igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}" Label="{}{Label}"/> <igChart:NumericYAxis x:Name="yAxis"/> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:LineSeries x:Name="line" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ItemsSource="{StaticResource data}" ValueMemberPath="Value"/> </igChart:XamDataChart.Series> </igChart:XamDataChart> </Grid>
And Code behind:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } public class TestData : List<dynamic> { public TestData() { for (int i = 0; i < 10; i++) { dynamic newObj = new ExpandoObject(); newObj.Label = i.ToString(); newObj.Value = (double)i; Add(newObj); } } } public class ChartData : List<ChartDataItem> { private string _labelPath; public string LabelPath { get { return _labelPath; } set { _labelPath = value; Refresh(); } } private string _valuePath; public string ValuePath { get { return _valuePath; } set { _valuePath = value; Refresh(); } } private IEnumerable<dynamic> _itemsSource; public IEnumerable<dynamic> ItemsSource { get { return _itemsSource; } set { _itemsSource = value; Refresh(); } } public void Refresh() { Clear(); if (ValuePath != null && LabelPath != null && ItemsSource != null) { foreach (dynamic obj in ItemsSource) { if (obj is IDictionary<string, object>) { ChartDataItem item = new ChartDataItem(); item.Label = (string)((IDictionary<string, object>)obj)[LabelPath]; item.Value = (double)((IDictionary<string, object>)obj)[ValuePath]; Add(item); } } } } } public class ChartDataItem { public string Label { get; set; } public double Value { get; set; } }
If you would like the chart to natively work with something like ExpandoObject, you could make a feature request.