Multiple Threads

When the data is changing on another thread(s), there is a chance that while the chart is measured, the data changes at the same time, this could cause a InvalidOperationException (Collection Was Modified) and some other sync errors; We need to let the chart know that the data is changing on multiple threads so it can handle it and prevent concurrency hazards.

There are 2 alternatives you can follow to prevent this issue, 1. use the lock keyword to wrap any change in your data, 2. Invoke the changes on the UI tread.

sample image

Locking changes (Alternative 1)

From MsDocs:

(the lock keyword) executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released. The lock statement ensures that at maximum only one thread executes its body at any time moment.

Additionally from locking your data changes, you must also let LiveCharts know your lock object, then LiveCharts will grab the lock while it is measuring a chart.

View model

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;

namespace ViewModelsSamples.General.MultiThreading;

public partial class ViewModel : ObservableObject
{
    private readonly Random _r = new();
    private readonly int _delay = 100;
    private readonly ObservableCollection<int> _values;
    private int _current;

    public ViewModel()
    {
        // lets create some initial data. // mark
        var items = new List<int>();
        for (var i = 0; i < 1500; i++)
        {
            _current += _r.Next(-9, 10);
            items.Add(_current);
        }

        _values = new ObservableCollection<int>(items);

        // create a series with the data // mark
        Series = new ISeries[]
        {
            new LineSeries<int>
            {
                Values = _values,
                GeometryFill = null,
                GeometryStroke = null,
                LineSmoothness = 0,
                Stroke = new SolidColorPaint(SKColors.Blue, 1)
            }
        };

        _delay = 1;
        var readTasks = 10;

        // Finally, we need to start the tasks that will add points to the series. // mark
        // we are creating {readTasks} tasks // mark
        // that will add a point every {_delay} milliseconds // mark
        for (var i = 0; i < readTasks; i++)
        {
            _ = Task.Run(ReadData);
        }
    }

    public ISeries[] Series { get; set; }

    public object Sync { get; } = new object();

    public bool IsReading { get; set; } = true;

    private async Task ReadData()
    {
        await Task.Delay(1000);

        // to keep this sample simple, we run the next infinite loop // mark
        // in a real application you should stop the loop/task when the view is disposed // mark

        while (IsReading)
        {
            await Task.Delay(_delay);

            _current = Interlocked.Add(ref _current, _r.Next(-9, 10));

            lock (Sync)
            {
                _values.Add(_current);
                _values.RemoveAt(0);
            }
        }
    }
}

XAML

<UserControl
    x:Class="UnoWinUISample.General.MultiThreading.View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:lvc="using:LiveChartsCore.SkiaSharpView.WinUI"
    xmlns:vms="using:ViewModelsSamples.General.MultiThreading"
    mc:Ignorable="d">

    <UserControl.DataContext>
        <vms:ViewModel/>
    </UserControl.DataContext>
    <Grid>
        <lvc:CartesianChart
            SyncContext="{Binding Sync}"
            Series="{Binding Series}">
        </lvc:CartesianChart>
    </Grid>

</UserControl>

Notice that we also set the chart SyncContext property so the chart knows our sync object.

Invoke the changes on the UI thread (Alternative 2)

You can also force the change to happen on the same thread where the chart is measured, this will prevent concurrency hazards because everything is happening on the same tread, but you must consider that now the UI tread is doing more operations.

View model

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;

namespace ViewModelsSamples.General.MultiThreading2;

public partial class ViewModel : ObservableObject
{
    private readonly Random _r = new();
    private readonly int _delay = 100;
    private readonly ObservableCollection<int> _values;
    private static int s_current;
    private readonly Action<Action> _uiThreadInvoker;

    public ViewModel(Action<Action> uiThreadInvoker)
    {
        // lets create some initial data. // mark
        var items = new List<int>();
        for (var i = 0; i < 1500; i++)
        {
            s_current += _r.Next(-9, 10);
            items.Add(s_current);
        }

        _values = new ObservableCollection<int>(items);

        // create a series with the data // mark
        Series = new ISeries[]
        {
            new LineSeries<int>
            {
                Values = _values,
                GeometryFill = null,
                GeometryStroke = null,
                LineSmoothness = 0,
                Stroke = new SolidColorPaint(SKColors.Blue, 1)
            }
        };

        // There are simplier ways to do this, but since we are using a MVVM pattern, // mark
        // we need to inject a delegate that will run an action in the UI thread. // mark
        _uiThreadInvoker = uiThreadInvoker;
        _delay = 1;
        var readTasks = 10;

        // Finally, we need to start the tasks that will add points to the series. // mark
        // we are creating {readTasks} tasks // mark
        // that will add a point every {_delay} milliseconds // mark
        for (var i = 0; i < readTasks; i++)
        {
            _ = Task.Run(ReadData);
        }
    }

    public ISeries[] Series { get; set; }

    public bool IsReading { get; set; } = true;

    public async Task ReadData()
    {
        await Task.Delay(1000);

        // to keep this sample simple, we run the next infinite loop // mark
        // in a real application you should stop the loop/task when the view is disposed // mark

        while (IsReading)
        {
            await Task.Delay(_delay);

            // force the change to happen in the UI thread. // mark
            _uiThreadInvoker(() =>
            {
                s_current += _r.Next(-9, 10);
                _values.Add(s_current);
                _values.RemoveAt(0);
            });
        }
    }
}

XAML

<UserControl
    x:Class="UnoWinUISample.General.MultiThreading2.View"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:lvc="using:LiveChartsCore.SkiaSharpView.WinUI"
    mc:Ignorable="d">
    <Grid>
        <lvc:CartesianChart
            Series="{Binding Series}">
        </lvc:CartesianChart>
    </Grid>

</UserControl>

View code behind

using System;
using Microsoft.UI.Xaml.Controls;
using ViewModelsSamples.General.MultiThreading2;

namespace UnoWinUISample.General.MultiThreading2;

public sealed partial class View : UserControl
{
    public View()
    {
        InitializeComponent();

        var vm = new ViewModel(InvokeOnUIThread);
        DataContext = vm;
    }

    // this method takes another function as an argument.
    // the idea is that we are invoking the passed action in the UI thread
    // but the UI framework will let the view model how to do this.
    // we will pass the InvokeOnUIThread method to our view model so the view model knows how
    // to invoke an action in the UI thred.
    private void InvokeOnUIThread(Action action)
    {
        // the InvokeOnUIThread method provided by livecharts is a simple helper class
        // that handles the invoke in the multiple platforms Uno supports.
        LiveChartsCore.SkiaSharpView.WinUI.Helpers.UnoPlatformHelpers.InvokeOnUIThread(action, DispatcherQueue);
    }
}

Articles you might also find useful: