First of all, i'd like to thank Mark Austin for his endless patience and rapid responses.
On to the matter at hand...here is a scenario, and a few code pieces to illustrate it:
Requirement 1- The chart must be bindeable
Requirement 2- We must be able to bind our generic interface, and obtain a proper chart
Requirement 3- We want to encapsulate xamChart in a user control, and create styles, axis, series, etc dynamically. So that not every single person needs to understand xamChart, but rather bind their implementation of our IChart2D, and our control can translate it into a proper xamChart.
Requirement 4- We use MVVM, and this control should support it, or be adaptable to it.
The interface supports a collection of what we call ISeries2D. Each ISeries2D supports 1 x axis with n y axis data, so you would have, for instance: { X:Day 1, Y1: Min, Y2: Max, Y3: Avg } (not necessarily json, just as a sample).
Now, i've tried to make this in two different ways, to no avail.
First attemp:
Make a IValueConverter that transforms our generic ISeries2D into an ObservableCollection<DataPoint>. So for each Y axis in my collection, i would create a series and pass it a Binding, using my ISeries2D instance as the source, and a new converter with the Y index as parameter.
//This creates an styled series. Series aSeries = this.CreateSeries(s, index, chartInfo); Binding binding = new Binding(); binding.Source = s; //s is a valid instance of ISeries2D ISeries2DToObservableConverter converter = new ISeries2DToObservableConverter(); converter.Index = index; binding.Converter = converter; aSeries.DataSource = binding; aSeries.DataMapping = "Value = Value; Label = Label";
//This creates an styled series.
Series aSeries = this.CreateSeries(s, index, chartInfo);
Binding binding = new Binding();
binding.Source = s; //s is a valid instance of ISeries2D
ISeries2DToObservableConverter converter = new ISeries2DToObservableConverter();
converter.Index = index;
binding.Converter = converter;
aSeries.DataSource = binding;
aSeries.DataMapping = "Value = Value; Label = Label";
This failed to work. Probably i'm mistaking the concept.
Second Attempt:
Handle the DataContextChanged in the xamChart. Once inside the event, manually create all the ObservableCollection<DataPoint>. Store this collections in a dictionary variable, registering the CollectionChanged event, so that if a CollectionChanged event fires, i can refresh the xamChart DataSource thru DataSourceRefresh(). This worked to an extent...but i had some problems.
First i was forced to define a dummy converter (it does nothing but return the same value), so that DataContextChanged was fired every time. And second, which i coudn't solve, even if i use Invoke method from the xamChart Dispatcher...i still get the cross thread ui error on an internal chart method.
Here is the three things i use:
1- Helper extension method from CodeProject: InvokeIfRequired()
public static void InvokeIfRequired(this DispatcherObject control, Action methodcall, DispatcherPriority priorityForCall) { //see if we need to Invoke call to Dispatcher thread if (control.Dispatcher.Thread != Thread.CurrentThread) control.Dispatcher.Invoke(priorityForCall, methodcall); else methodcall(); }
public static void InvokeIfRequired(this DispatcherObject control, Action methodcall, DispatcherPriority priorityForCall)
{
//see if we need to Invoke call to Dispatcher thread
if (control.Dispatcher.Thread != Thread.CurrentThread)
control.Dispatcher.Invoke(priorityForCall, methodcall);
else
methodcall();
}
2 - DataContextChanged
private void HandleDataContextChange(IChart2D chartInfo) { Caption title = new Caption(); title.Text = chartInfo.Title; Axis xAxis = new Axis(); xAxis.AxisType = AxisType.PrimaryX; Axis yAxis = new Axis(); yAxis.AxisType = AxisType.PrimaryY; this.igChart.InvokeIfRequired(() => { this.igChart.Caption = title; this.igChart.Axes.Add(xAxis); this.igChart.Axes.Add(yAxis); }, DispatcherPriority.Background); foreach (string name in chartInfo.Series.Keys) { ISeries2D s = chartInfo.Series[name]; Series aSeries = null; foreach (int index in s.YValues.Keys) { this.igChart.InvokeIfRequired(() => { foreach (Series gs in this.igChart.Series) { if (gs.Name.Equals(s.Labels[index])) { aSeries = gs; break; } } }, DispatcherPriority.Background); ObservableCollection<DataPoint> data = new ObservableCollection<DataPoint>(); data.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(data_CollectionChanged); if (this.seriesData.ContainsKey(s.Labels[index])) { data = this.seriesData[s.Labels[index]]; } if (aSeries == null) { aSeries = this.CreateSeries(s, index, chartInfo); aSeries.DataSource = data; aSeries.DataMapping = "Value = Value; Label = Label"; this.igChart.InvokeIfRequired(() => { this.igChart.Series.Add(aSeries); }, DispatcherPriority.Background); } lock (s.LockObject) { for (int i = 0; i < s.XValues.Count; i++) { DataPoint dp = new DataPoint(); dp.Label = Convert.ToString(s.XValues[i]); dp.Value = Convert.ToDouble(s.YValues[index][i]); if (!data.Contains(dp)) { data.Add(dp); } } } if (!this.seriesData.ContainsKey(s.Labels[index])) { this.seriesData.Add(s.Labels[index], data); } //if (this.Paginate) //{ // if (this.Graph.Series[0].MaxXValue() > this.Graph.Axes.Bottom.Maximum) // { // double difference = this.Graph.Axes.Bottom.Maximum - this.Graph.Axes.Bottom.Minimum; // this.Graph.Axes.Bottom.SetMinMax(this.Graph.Axes.Bottom.Maximum, this.Graph.Axes.Bottom.Maximum + difference); // } //} } } }
private void HandleDataContextChange(IChart2D chartInfo)
Caption title = new Caption();
title.Text = chartInfo.Title;
Axis xAxis = new Axis();
xAxis.AxisType = AxisType.PrimaryX;
Axis yAxis = new Axis();
yAxis.AxisType = AxisType.PrimaryY;
this.igChart.InvokeIfRequired(() =>
this.igChart.Caption = title;
this.igChart.Axes.Add(xAxis);
this.igChart.Axes.Add(yAxis);
}, DispatcherPriority.Background);
foreach (string name in chartInfo.Series.Keys)
ISeries2D s = chartInfo.Series[name];
Series aSeries = null;
foreach (int index in s.YValues.Keys)
foreach (Series gs in this.igChart.Series)
if (gs.Name.Equals(s.Labels[index]))
aSeries = gs;
break;
ObservableCollection<DataPoint> data = new ObservableCollection<DataPoint>();
data.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(data_CollectionChanged);
if (this.seriesData.ContainsKey(s.Labels[index]))
data = this.seriesData[s.Labels[index]];
if (aSeries == null)
aSeries = this.CreateSeries(s, index, chartInfo);
aSeries.DataSource = data;
this.igChart.Series.Add(aSeries);
lock (s.LockObject)
for (int i = 0; i < s.XValues.Count; i++)
DataPoint dp = new DataPoint();
dp.Label = Convert.ToString(s.XValues[i]);
dp.Value = Convert.ToDouble(s.YValues[index][i]);
if (!data.Contains(dp))
data.Add(dp);
if (!this.seriesData.ContainsKey(s.Labels[index]))
this.seriesData.Add(s.Labels[index], data);
//if (this.Paginate)
//{
// if (this.Graph.Series[0].MaxXValue() > this.Graph.Axes.Bottom.Maximum)
// {
// double difference = this.Graph.Axes.Bottom.Maximum - this.Graph.Axes.Bottom.Minimum;
// this.Graph.Axes.Bottom.SetMinMax(this.Graph.Axes.Bottom.Maximum, this.Graph.Axes.Bottom.Maximum + difference);
// }
//}
3- OnCollectionChanged from all ObservableCollection<DataPoint> sources
void data_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { this.igChart.InvokeIfRequired(() => { this.igChart.DataSourceRefresh(); }, DispatcherPriority.Background); }
void data_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
this.igChart.DataSourceRefresh();
This is where i get: "The calling thread cannot access this object because a different thread owns it."
StackTrace:
at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks) at System.Delegate.DynamicInvokeImpl(Object[] args) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter) at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler) at System.Windows.Threading.DispatcherOperation.InvokeImpl() at System.Threading.ExecutionContext.runTryCode(Object userData) at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Windows.Threading.DispatcherOperation.Invoke() at System.Windows.Threading.Dispatcher.ProcessQueue() at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter) at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler) at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Boolean isSingleParameter) at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam) at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg) at System.Windows.Threading.Dispatcher.TranslateAndDispatchMessage(MSG& msg) at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame) at System.Windows.Application.RunInternal(Window window) at Tenaris.Manager.NDT.EMI.OperativeScreen.App.Main() in C:\Development\Tenaris\deen\Process\Ndt\Emi\Manager\NDTManager\sandbox\1.xx-devel\source\views\Tenaris.Manager.NDT.EMI.OperativeScreen\obj\Debug\App.g.cs:line 0 at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args) at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly() at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.ThreadHelper.ThreadStart()
at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
at System.Delegate.DynamicInvokeImpl(Object[] args)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Boolean isSingleParameter)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Boolean isSingleParameter, Delegate catchHandler)
at System.Windows.Threading.DispatcherOperation.InvokeImpl()
at System.Threading.ExecutionContext.runTryCode(Object userData)
at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Windows.Threading.DispatcherOperation.Invoke()
at System.Windows.Threading.Dispatcher.ProcessQueue()
at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Boolean isSingleParameter)
at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.TranslateAndDispatchMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
at System.Windows.Application.RunInternal(Window window)
at Tenaris.Manager.NDT.EMI.OperativeScreen.App.Main() in C:\Development\Tenaris\deen\Process\Ndt\Emi\Manager\NDTManager\sandbox\1.xx-devel\source\views\Tenaris.Manager.NDT.EMI.OperativeScreen\obj\Debug\App.g.cs:line 0
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart()
PS: I cannot find a way to stylish the code parts, if you know how...please let me know...or feel free to edit the post.
EDIT:
A few things that i've changed:
I no longer use a converter or the DataContextChanged. I've subscribed the user control to the change notification in my ViewModel and there i call HandleDataContextChange, and check if i have to update or create the new series, etc. But i still attach the series source as one of the ObservableCollection<DataPoint> and still have the issue of cross thread reference.
That worked well. Thanks a lot for your help.
Try Application.Current.Dispatcher.Invoke(...)
Also, see this article for more details: http://msdn.microsoft.com/en-us/magazine/cc163328.aspx
Another strategy is to not have your view model explicitely build the chart, but have objects in you view react to the requirements the view model describes through data binding and display the correct chart. Its your view models job to get the data into a state thats easily consumable by your chart (in the view), but the views job to decide exactly how that data is shown on the screen. If your view model is constructing portions of your view, then this couples your view and view model together more tightly than is, perhaps, appropriate. I'll link below one way of letting your view model's data drive the construction of the chart without explicitely creating any view objects. There are certainly other ways of going about this, though:
http://community.infragistics.com/forums/p/38169/219968.aspx#219968
The xamChart was authored before the MVVM pattern became well established so requires a little adaption to be entirely constructed based on the binding of view model data. Another strategy for approaching this is to wrap your xamChart in a new control for your view layer that accepts bound data from your view model and dynamically constructs the required chart to display it. The key though is that all this happens in your view, which is owned by the UI thread. Your view model and model layers are free to be as asynchronous as they desire, but interaction with the View and its UI thread must be marshalled through the appropriate dispatchers, or through WPFs built in data binding mechanisms, that marshal the interactions for you.
-Graham
Thanks for the quick reply.
The series objects are created in view model, thus I dont have access to the chart. How do i call the CreateSeries on the main thread from view model. I did try Dispatcher.CurrentDispatcher.Invoke(...), but still no luck....
Hi,
You can't create the Series on the background thread, they need to be associated with the same thread as owns the chart (the UI thread). You will have to perform the logic that creates and populates the series on the UI thread. You can initiate that logic from the background thread, though, by invoking it using the chart's dispatcher. For example if you have a method called "CreateSeries" which creates a series and assigns it to the chart then you can call if from the background thread like this:
chart1.Dispatcher.BeginInvoke(new ThreadStart(CreateSeries), null);
Which will invoke just the logic that creates and assigns the series on the UI thread.
Does this help?
Hi Graham,
We create the series dynamically for our xamchart, which works fine. But now we are getting the results on a background thread and depending on the results, we need to create the Series objects(still on background thread). Facing issues with it. How do we handle such situations?
Thanks