Search code examples
c#xamlchartsuwptelerik

How to resolve "Value cannot be null" Exception with ChartTrackBallBehavior


I have been working on building a program in UWP that utilises Telerik Charting controls to display incoming data to a user. In my latest testing with unsolicited data being received and drawn dynamically on the Chart, I have encountered that an unhandled exception (NullReferenceException) is occurring. This seems to only occur when the user has their mouse over the Chart with the Chart showing the TrackBall information (Telerik's ChartTrackBallBehavior). If the user's mouse is elsewhere or has otherwise not triggered the TrackBall information to show on the chart, this exception is never reached.

I have so far managed to track the NullException down to occurring within the function GetIntersectionTemplate() within ChartTrackBallBehavior.cs within Telerik's UI for UWP. Beyond that, I do not know what all I can about this issue to resolve it such that it never happens again.

Here is the typical Stack Trace when the exception happens:

System.ArgumentNullException: Value cannot be null.
   at Telerik.UI.Xaml.Controls.Chart.ChartTrackBallBehavior.GetIntersectionTemplate(DependencyObject instance)
   at Telerik.UI.Xaml.Controls.Chart.ChartTrackBallBehavior.UpdateIntersectionPoints(ChartDataContext context)
   at Telerik.UI.Xaml.Controls.Chart.ChartTrackBallBehavior.UpdateVisuals()
   at Telerik.UI.Xaml.Controls.Chart.RadChartBase.NotifyUIUpdated()
   at Telerik.UI.Xaml.Controls.Chart.PresenterBase.UpdateUI(ChartLayoutContext context)

I have tried disabling the ChartTrackBallBehavior before and after adding each Series to the Chart, to no avail. I have tried manually changing the focus to a different control other than the Chart, to no avail. I have tried manual calls to Chart.UpdateLayout() in different areas, only to result in these calls creating the same NullReferenceException within the same location (ChartTrackBallBehavior.cs).

At the core of the issue seems to be this "Value" that is erroneously set to null. I have not yet been able to determine what "Value" is set to null at all thus far, I can only assume it is hitting the NullReferenceException throw() call within the GetIntersectionTemplate() function call. But I don't know why it is happening or what I can do about it.

I have made a minimal Project that replicates the problem. Note that it clears all Series from the chart before then redrawing all of the Series on the chart. This is done to be equivalent to my own Project, and seems to be related to the issue itself. This Series Clear and re-add procedure is done as a user may at any time change which Series are to be shown on the chart.

It may be possible that I can change the coding structure around to approach this from a different direction, but currently I would like to better understand what is causing this issue and resolve it if I can, as otherwise I will likely need to rewrite a large portion of the code and unfortunately time is not on my side with this.

Here is the example code. Note that I am using Telerik.UI.for.UniversalWindowsPlatform version 1.0.1.5 for this code.

MainPage.xaml

<Page
    x:Class="ExceptionReplicator.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ExceptionReplicator"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:telerikChart="using:Telerik.UI.Xaml.Controls.Chart"
    xmlns:telerikPrimitives="using:Telerik.UI.Xaml.Controls.Primitives"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <telerikChart:RadCartesianChart x:Name="MainChart" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,10,10,10">
            <telerikChart:RadCartesianChart.Grid>
                <telerikChart:CartesianChartGrid MajorLinesVisibility="XY"/>
            </telerikChart:RadCartesianChart.Grid>

            <telerikChart:RadCartesianChart.Behaviors>
                <telerikChart:ChartPanAndZoomBehavior ZoomMode="Both" PanMode="Both"/>
                <telerikChart:ChartTrackBallBehavior x:Name="TrackBallBehaviour" InfoMode="Multiple" ShowIntersectionPoints="True">
                    <telerikChart:ChartTrackBallBehavior.LineStyle>
                        <Style TargetType="Polyline">
                            <Setter Property="Stroke" Value="Tomato"/>
                            <Setter Property="StrokeThickness" Value="2"/>
                            <Setter Property="StrokeDashArray" Value="1,2"/>
                        </Style>
                    </telerikChart:ChartTrackBallBehavior.LineStyle>
                    <telerikChart:ChartTrackBallBehavior.IntersectionTemplate>
                        <DataTemplate>
                            <Ellipse Width="10" Height="10" Fill="Tomato"/>
                        </DataTemplate>
                    </telerikChart:ChartTrackBallBehavior.IntersectionTemplate>

                </telerikChart:ChartTrackBallBehavior>
            </telerikChart:RadCartesianChart.Behaviors>

            <telerikChart:RadCartesianChart.VerticalAxis>
                <telerikChart:LinearAxis x:Name="Vertical" Title="Y Axis" Minimum="0"/>
            </telerikChart:RadCartesianChart.VerticalAxis>
            <telerikChart:RadCartesianChart.HorizontalAxis>
                <telerikChart:LinearAxis x:Name="Horizontal" Title="X Axis"/>
            </telerikChart:RadCartesianChart.HorizontalAxis>

        </telerikChart:RadCartesianChart>
    </Grid>
</Page>

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace ExceptionReplicator
{
    using System;
    using System.Threading;
    using Telerik.UI.Xaml.Controls.Chart;
    using Windows.UI.Core;

    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private Timer DataTimer;        // timer to periodically add data to the chart
        private int LineCount = 1;      // arbitrary line counter to differentiate lines from each other

        // custom class for holding the data to be displayed on the chart
        private class Data
        {
            public int XValue { get; set; }
            public int YValue { get; set; }
        }

        // overarching class that holds all of the data for ONE line/series
        private class DataToChart
        {
            // List of all the data points within this instance of DataToChart
            public List<Data> DataPoints;

            // Constructor to initialise DataPoints
            public DataToChart()
            {
                DataPoints = new List<Data>();
            }
        }

        // Overarching container to hold data for ALL lines/series
        private List<DataToChart> allData = new List<DataToChart>();

        public MainPage()
        {
            this.InitializeComponent();

            // set up the timer to call every 10s to add new data to the chart. warning: this will run infinitely
            DataTimer = new Timer(DataCallback, null, (int)TimeSpan.FromSeconds(10).TotalMilliseconds, Timeout.Infinite);
        }

        // Generic callback to call AddLineToChart() on the other thread to handle the Chart's data
        private void DataCallback(object state)
        {
            var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => AddLineToChart());
        }

        // Code to handle adding a line to the chart
        private void AddLineToChart()
        {
            // Using Random() to create random data
            Random rand = new Random();
            DataToChart dataToChart = new DataToChart();

            for (int i = 0; i < 50; i++)
            {
                dataToChart.DataPoints.Add(new Data
                {
                    XValue = i,
                    YValue = rand.Next(0, 100)
                });
            }

            // Add the data for this line/series to the overarching container
            allData.Add(dataToChart);

            // re-initialise the line count
            LineCount = 1;

            // Currently the code needs to clear the chart and redraw it each time new data is introduced
            MainChart.Series.Clear();

            // For each line/series in the main container
            foreach (DataToChart data in allData)
            {
                // Make a series for the line
                ScatterLineSeries scatterLineSeries = new ScatterLineSeries
                {
                    Name = $"Line {LineCount}",
                    ItemsSource = dataToChart.DataPoints,
                    XValueBinding = new PropertyNameDataPointBinding("XValue"),
                    YValueBinding = new PropertyNameDataPointBinding("YValue"),
                    DisplayName = $"Line {LineCount}",
                    LegendTitle = $"Line {LineCount}",
                };

                // Add the line to the Chart's Series collection
                MainChart.Series.Add(scatterLineSeries);

                // Increment arbitrary counter
                LineCount++;
            }

            // Re-set the timer to fire again in 10s
            DataTimer.Change((int)TimeSpan.FromSeconds(10).TotalMilliseconds, Timeout.Infinite);
        }
    }
}

I need to find a solution to ensure that this exception no longer occurs when new data is introduced. Any help would be greatly appreciated.

In the short term I've removed the ChartTrackBallBehavior altogether from my Chart (commented it out) until such time as I determine a solution. With the Behavior removed, this exception does not occur.


Solution

  • There're some issues in your code.

    1. Did you find that your IntersectionTemplate is not applied to your 'ScatterLineSeries' at all? I checked the Telerik document TrackBall Behavior. They put the IntersectionTemplate in the telerikChart:LineSeries, instead of telerikChart:RadCartesianChart.Behaviors. So, you need to apply the IntersectionTemplate to the ScatterLineSeries in code behind.
    2. You called MainChart.Series.Clear(); to clear the chart and redraw it each time when there's new data. It will cause performance issue when there's a large amount of data. I suggested that you just add the new ScatterLineSeries to the chart and keep the old data there.
    3. You declared the List<DataToChart> allData variable. I suggested you to use ObservableCollection Class. This class has implemented the INotifyPropertyChanged interface, when new data is added in the collection, it will notify the UI.

    With the above three points, I made a code sample for your reference:

    <Page.Resources>
        <DataTemplate x:Key="ChartTrackIntersectionTemplate">
            <Ellipse Width="10" Height="10" Fill="Tomato" />
        </DataTemplate>
    </Page.Resources>
    <Grid>
        <telerikChart:RadCartesianChart x:Name="MainChart" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,10,10,10">
            <telerikChart:RadCartesianChart.Grid>
                <telerikChart:CartesianChartGrid MajorLinesVisibility="XY" />
            </telerikChart:RadCartesianChart.Grid>
    
            <telerikChart:RadCartesianChart.Behaviors>
                <telerikChart:ChartPanAndZoomBehavior ZoomMode="Both" PanMode="Both" />
                <telerikChart:ChartTrackBallBehavior x:Name="TrackBallBehaviour" InfoMode="Multiple" ShowIntersectionPoints="True">
                    <telerikChart:ChartTrackBallBehavior.LineStyle>
                        <Style TargetType="Polyline">
                            <Setter Property="Stroke" Value="Tomato" />
                            <Setter Property="StrokeThickness" Value="2" />
                            <Setter Property="StrokeDashArray" Value="1,2" />
                        </Style>
                    </telerikChart:ChartTrackBallBehavior.LineStyle>
                </telerikChart:ChartTrackBallBehavior>
            </telerikChart:RadCartesianChart.Behaviors>
    
            <telerikChart:RadCartesianChart.VerticalAxis>
                <telerikChart:LinearAxis x:Name="Vertical" Title="Y Axis" Minimum="0" />
            </telerikChart:RadCartesianChart.VerticalAxis>
            <telerikChart:RadCartesianChart.HorizontalAxis>
                <telerikChart:LinearAxis x:Name="Horizontal" Title="X Axis" />
            </telerikChart:RadCartesianChart.HorizontalAxis>
        </telerikChart:RadCartesianChart>
    </Grid>
    
    public sealed partial class MainPage : Page
    {
        private Timer DataTimer;        // timer to periodically add data to the chart
        private int LineCount = 1;      // arbitrary line counter to differentiate lines from each other
        private DataTemplate ChartTrackIntersectionTemplate;
    
        // custom class for holding the data to be displayed on the chart
        private class Data
        {
            public int XValue { get; set; }
            public int YValue { get; set; }
        }
    
        // overarching class that holds all of the data for ONE line/series
        private class DataToChart
        {
            // List of all the data points within this instance of DataToChart
            public List<Data> DataPoints;
    
            // Constructor to initialise DataPoints
            public DataToChart()
            {
                DataPoints = new List<Data>();
            }
        }
    
        // Overarching container to hold data for ALL lines/series
        private ObservableCollection<DataToChart> allData = new ObservableCollection<DataToChart>();
    
        public MainPage()
        {
            this.InitializeComponent();
            // set up the timer to call every 10s to add new data to the chart. warning: this will run infinitely
            DataTimer = new Timer(DataCallback, null, (int)TimeSpan.FromSeconds(10).TotalMilliseconds, Timeout.Infinite);
            ChartTrackIntersectionTemplate = this.Resources["ChartTrackIntersectionTemplate"] as DataTemplate;
            allData.CollectionChanged += AllData_CollectionChanged;
        }
    
        private void AllData_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            foreach (DataToChart data in e.NewItems)
            {
                // Make a series for the line
                ScatterLineSeries scatterLineSeries = new ScatterLineSeries
                {
                    Name = $"Line {LineCount}",
                    ItemsSource = data.DataPoints,
                    XValueBinding = new PropertyNameDataPointBinding("XValue"),
                    YValueBinding = new PropertyNameDataPointBinding("YValue"),
                    DisplayName = $"Line {LineCount}",
                    LegendTitle = $"Line {LineCount}",
                };
                ChartTrackBallBehavior.SetIntersectionTemplate(scatterLineSeries, ChartTrackIntersectionTemplate);
                // Add the line to the Chart's Series collection
                MainChart.Series.Add(scatterLineSeries);
                // Increment arbitrary counter
                LineCount++;
            }
        }
    
        // Generic callback to call AddLineToChart() on the other thread to handle the Chart's data
        private async void DataCallback(object state)
        {
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => AddLineToChart());
        }
    
        // Code to handle adding a line to the chart
        private void AddLineToChart()
        {
            // Using Random() to create random data
            Random rand = new Random();
            DataToChart dataToChart = new DataToChart();
    
            for (int i = 0; i < 50; i++)
            {
                dataToChart.DataPoints.Add(new Data
                {
                    XValue = i,
                    YValue = rand.Next(0, 100)
                });
            }
    
            // Add the data for this line/series to the overarching container
            allData.Add(dataToChart);
            DataTimer.Change((int)TimeSpan.FromSeconds(10).TotalMilliseconds, Timeout.Infinite);
        }
    }