Your Privacy Matters: We use our own and third-party cookies to improve your experience on our website. By continuing to use the website we understand that you accept their use. Cookie Policy
200
xamChart - 99% Code Behind.
posted

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 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();

        }

 

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);

                    //    }

                    //}

                }

            }

        }

 

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);

        }

 

 

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()

 

 

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.