I am trying to set the ItemSource for a chart in a Task method, like this:
Task ShowData()
{
return Task.Factory.StartNew(() => {
// do calculations here...
LineSeries1.ItemsSource = _chartLines;
}
}
However, when run, I get the following exception: Unhandled Exception: Java.Util.ConcurrentModificationException
When I change this to a normal void method it works fine. Any pointers as to how to fix this?
My Telerik.Xamarin.Android.Chart dll is v4.0.30319.
9 Answers, 1 is accepted
This should go at the end of that code snippet I posted:
, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
Without the rest of the code I can't give you a definitive answer. What happens when you marshal the task's work back to the UI thread when its done:
Task ShowData()
{
return
Task.Factory.StartNew(() => {
// do calculations here...
Device.BeginInvokeOnMainThread(()=> { LineSeries1.ItemsSource = _chartLines; };
}
}
If this doesn't help, can you please reply with the rest of the code, I particularly need to see the following:
- The chart definition (e.g. XAML)
- The methods that invoke the task (e.g. is ShowData in OnAppearing?)
- Any other code that will help me build the reprodicible (e.g. model class, view model class, etc)
Regards,
Lance | Tech Support Engineer, Sr.
Progress Telerik
Here's my chart xaml:
<
chart:RadCartesianChart
x:Name
=
"ChartResults"
VerticalOptions
=
"CenterAndExpand"
HorizontalOptions
=
"CenterAndExpand"
Margin
=
"1,1,1,1"
Grid.Row
=
"0"
Grid.Column
=
"0"
>
<
chart:RadCartesianChart.HorizontalAxis
>
<
chart:NumericalAxis
x:Name
=
"HorizontalAxis"
/>
</
chart:RadCartesianChart.HorizontalAxis
>
<
chart:RadCartesianChart.VerticalAxis
>
<
chart:NumericalAxis
x:Name
=
"YAxis2"
LabelFormat
=
"0.##"
/>
</
chart:RadCartesianChart.VerticalAxis
>
<
chart:RadCartesianChart.Grid
>
<
chart:CartesianChartGrid
MajorLinesVisibility
=
"XY"
/>
</
chart:RadCartesianChart.Grid
>
<
chart:RadCartesianChart.Series
>
<
chart:ScatterLineSeries
x:Name
=
"LineSeries1"
Stroke
=
"CornflowerBlue"
StrokeThickness
=
"1"
/>
</
chart:RadCartesianChart.Series
>
</
chart:RadCartesianChart
>
ShowData is called by a Timer. The Timer is initialised at the end of the OnAppearing method:
ShowWaveformDelegate showWaveform = DoShowWaveform
MyEvent.Callback = showWaveform
Globals.MyTimer = new Timer { Interval = 1000 };
Globals.MyTimer.Elapsed += OnTimedEvent;
Globals.MyTimer.Start();
private static void OnTimedEvent(object source, ElapsedEventArgs e
{
Globals.MyTimer.Stop();
MyEvent.Run();
}
private async void DoShowWaveform(Data data)
{
await ShowWaveform(data);
}
The idea here is to update the chart every second, but when the chart is being updated, stop the timer, then restart the timers after it has been updated.
MyEvent looks like this:
public static Delegate Callback { get; set; }
public static async void Run()
{
try
{
// collect data
Callback?.DynamicInvoke(data);
}
catch (Exception e)
{
// ignored
}
}
}
Unfortunately, the code is not complete enough for me to recreate the exception as there are missing pieces and infrastructure. Regardless, I suspect the reason for the error is the code is multiple threads are trying to update the series simultaneously.
It's recommended that you use Device.StartTimer in Xamarin.Forms as it will use the proper clock for the device. Here's a good discussion on StackOverflow about System Timer vs Device.StartTimer.
Demo
You could use the PropertyChanged event of the series to know when the last time the ItemsSource was set before starting the 1 second timer again.
// subscribe to Propertychanged
LineSeries1.PropertyChanged += LineSeries1_PropertyChanged;
// the event handler that will fire off another 1 second timer
private
void
LineSeries1_PropertyChanged(
object
sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// this will be true is the ItemsSource was reset
if
(e.PropertyName == nameof(ScatterLineSeries.ItemsSource))
{
// Get the next data in 1 second
Device.StartTimer(TimeSpan.FromMilliseconds(1000), OnTick);
}
}
To stop each timer, you return false (returning True continues the timer)
private
bool
OnTick()
{
Task.Run(async () =>
{
// Get Data From sensors
await Task.Delay(200);
// Set the ItemsSource
Device.BeginInvokeOnMainThread(() =>
{
LineSeries1.ItemsSource = _lineSeriesData;
});
});
// Stop the timer
return
false
;
}
With this approach you have a reliable order of operations. Here's the debug output from my attached demo to verify everything is occurringas intended:
[0:] OnTick
[0:] Reading Sensor Data
[0:] Setting ItemsSource
[0:] OnTick
[0:] Reading Sensor Data
[0:] Setting ItemsSource
[0:] OnTick
[0:] Reading Sensor Data
[0:] Setting ItemsSource
[0:] OnTick
[0:] Reading Sensor Data
[0:] Setting ItemsSource
Regards,
Lance | Tech Support Engineer, Sr.
Progress Telerik
Thanks for the example. It works, but the timer isn't being retriggered after the LineSeries1.ItemSource is set for the second time. Here's my debug output:
[0:] OnTick
[0:] stop timer
[0:] do reading
[0:] show waveform
[0:] start timer
[0:] OnTick
[0:] stop timer
[0:] do reading
[0:] show waveform
My code is the same as your, except I do await ShowWaveform(data) in the OnTick method, and I set LineSeries1 in my ShowWaveform method, like this:
Device.BeginInvokeOnMainThread(() =>
{
LineSeries1.ItemsSource?.Clear();
LineSeries1.ItemsSource = _chartLines;
});
I also tried omitting the Clear line, but still no joy.
You don't need to call Clear on the ItemsSource because you're resetting the value in the next line. Since ItemsSource is a DependencyProperty, setting it replaces the previous value in the control.
I can confirm that the LineSeries's ItemsSource is working as expected (as demonstrated in my attached demo). I've reattached it here as a runnable solution so you can see this first-hand. When the page launches, the ItemsSource is updated continuously.
I'm unable to directly diagnose the issue you're having, but as you suspect it might be due to using await in the tick instead of my recommended approach (you would do the async call where I have Task.Delay).
Further Assistance using Task
Telerik Forums specifically covers the Telerik controls and their APIs, the problem you're having isn't related to the Chart's APIs. For example, if you swapped out the Chart for a plain Xamarin ListView, you'd have the same problem.
If you'd like assistance in redesigning or debugging the architecture, I recommend posting in StackOverflow or the Xamarin Forums. This isn't an uncommon threading issue and you'll be able to get assistance from the Xamarin and .NET community who've previously built solutions for this scenario.
Professional Services
If you do get completely stuck, and are unable to find any solution, we have partners who are specialized work on development problems like this and help you build out the app. You can contact them here: Professional Services - Architecture Review or Professional Services - Custom App Dev
Regards,
Lance | Tech Support Engineer, Sr.
Progress Telerik
I'm not so sure that this is a general async issue. The first time the timer is triggered, the following line:
LineSeries1.ItemsSource = _chartLines;
Correctly causes the LineSeries1_PropertyChanged event to be triggered. The second time the ItemsSource is set, the LineSeries1_PropertyChanged event is not triggered.
This is what I'm referring to. The fact that PropertyChanged event is not firing, means that the logic in the task is not actually setting ItemsSource.
If you can isolate this in a separate runnable project, send that project to us in a support ticket (you can create one here). I'll ask the development team to to take a look and offer suggestions on how to restructure.
Regards,
Lance | Tech Support Engineer, Sr.
Progress Telerik