Click here to Skip to main content
15,867,568 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I'm populating a collection of data points and showing it on a line chart. A user control having IEnumerable dependency property which is bind to ObservableCollection. I'm using LiveCharts for plotting data points.

Issue: The CollectionChanged is not getting fired when I add data to the collection in VM_CaptureData.

Please help me with this issue. Thanks in advance. Please find the sample project here.

Here is my code,

What I have tried:

UserControl.xaml
<UserControl x:Class="ChartLibrary.LineChart"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:lvc="clr-namespace:LiveCharts.Wpf;assembly=LiveCharts.Wpf"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
<Grid>
    <lvc:CartesianChart x:Name="chart" Series="{Binding Series_Collection}" LegendLocation="Bottom"  
                        AnimationsSpeed="0:0:0.5" Hoverable="False" DataTooltip="{x:Null}" >
        <lvc:CartesianChart.AxisY>
            <lvc:Axis Name="Axis" Title="Readings" LabelFormatter="{Binding AxisYFormatter}" />
        </lvc:CartesianChart.AxisY>
        <lvc:CartesianChart.AxisX>
            <lvc:Axis Title="Data Points" Labels="{Binding XAxisLabel}" LabelsRotation="-87">
                <lvc:Axis.Separator>
                    <lvc:Separator StrokeThickness="1" StrokeDashArray="2" IsEnabled="True" Step="1">
                        <!--Here, see the IsEnabled => false-->
                        <lvc:Separator.Stroke>
                            <SolidColorBrush Color="#404F56" />
                        </lvc:Separator.Stroke>
                    </lvc:Separator>
                </lvc:Axis.Separator>
            </lvc:Axis>
        </lvc:CartesianChart.AxisX>
    </lvc:CartesianChart>
</Grid>

UserControl.cs
C#
public partial class LineChart : UserControl
{
    public IEnumerable<double> Values
    {
        get { return (IEnumerable<double>)GetValue(ValuesProperty); }
        set { SetValue(ValuesProperty, value); }
    }
    // Using a DependencyProperty as the backing store for Values.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ValuesProperty =
        DependencyProperty.Register("Values", typeof(IEnumerable<double>), typeof(LineChart), new PropertyMetadata(null, new PropertyChangedCallback(OnValueChanged)));
    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d != null && d is LineChart)
        {
            LineChart lineChart = d as LineChart;
            lineChart.ValueChanged(e);
        }
    }
    public LineChartData ChartData { get; set; }
    public LineChart()
    {
        InitializeComponent();
        Values = new ObservableCollection<double>();
        ChartData = new LineChartData();
        chart.DataContext = ChartData;
        Unloaded += LineChart_Unloaded;
    }
    private void LineChart_Unloaded(object sender, RoutedEventArgs e)
    {
        ChartData = null;
        chart.DataContext = null;
    }
    private void Values_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null && e.NewItems.Count > 0)
        {
            if (e.NewItems.Count == 1)
                ChartData.AddDataPoint(Convert.ToDouble(e.NewItems[0]));
            else
                ChartData.AddDataPointrange(e.NewItems.Cast<double>().GetEnumerator() as IEnumerable<double>);
        }
    }
    public void ValueChanged(DependencyPropertyChangedEventArgs e)
    {
        if (ChartData == null) return;
        if (e.NewValue == null) return;
        try
        {
            switch (e.Property.Name)
            {
                case "Values":
                    if (e.OldValue != null)
                    {
                        var oldCollection = e.OldValue as INotifyCollectionChanged;
                        if (oldCollection != null)
                        {
                            oldCollection.CollectionChanged -= Values_CollectionChanged;
                        }
                    }
                    var newCollection = e.NewValue as INotifyCollectionChanged;
                    if (newCollection != null)
                    {
                        newCollection.CollectionChanged += Values_CollectionChanged;
                        if (e.NewValue is ObservableCollection<double>)
                        {
                            ChartData.Clear();
                            ChartData.AddDataPointrange(e.NewValue as ObservableCollection<double>);
                        }
                    }
                    break;
                default:
                    break;
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

MainWindow.xaml
<Window x:Class="WpfTestDP_Coll_Changed.MainWindow"
    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:local="clr-namespace:WpfTestDP_Coll_Changed"
    xmlns:layouts="clr-namespace:WpfTestDP_Coll_Changed.Layouts"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.Resources>
    <BooleanToVisibilityConverter x:Key="VisibleIfTrueConverter"/>
</Window.Resources>
<DockPanel>
    <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,10">
        <Button Name="btnStart" Content="Start" Command="{Binding CaptureDataCommand}" CommandParameter="DemoTimerStart" Margin="0,0,10,0"/>
        <Button Name="btnStop" Content="Stop" Command="{Binding CaptureDataCommand}" CommandParameter="DemoTimerStop" />
    </StackPanel>
    <Grid>
        <layouts:ChartLayout2 Visibility="{Binding Path=IsChecked,ElementName=radLayout2, Converter={StaticResource VisibleIfTrueConverter}}"/>
    </Grid>
</DockPanel>

MainWindow.cs
public partial class MainWindow : Window
{
    VM_CaptureData VM_CaptureData;
    public MainWindow()
    {
        InitializeComponent();
        Unloaded += CaptureDataControl_Unloaded;
        VM_CaptureData = new VM_CaptureData();
        DataContext = VM_CaptureData;
    }

    private void CaptureDataControl_Unloaded(object sender, RoutedEventArgs e)
    {
        DataContext = null;
        VM_CaptureData.Deinitialize();
        VM_CaptureData = null;
    }
}

ViewModel for MainWindow
class VM_CaptureData : INotifyPropertyChanged
{
    private ObservableCollection<ChartLibrary.ChartDataPopulation> parametersData = new ObservableCollection<ChartLibrary.ChartDataPopulation>();
    public ObservableCollection<ChartLibrary.ChartDataPopulation> ParametersData
    {
        get { return parametersData; }
        set { parametersData = value; OnPropertyChanged(); }
    }
    private ChartLibrary.ChartDataPopulation selectedParameterData;
    public ChartLibrary.ChartDataPopulation SelectedParameterData
    {
        get { return selectedParameterData; }
        set { selectedParameterData = value; OnPropertyChanged(); }
    }
    DispatcherTimer dispatcherTimer;
    public RelayCommand<object> CaptureDataCommand { get; set; }
    public VM_CaptureData()
    {
        CaptureDataCommand = new RelayCommand<object>(Execute, CanExecute);

        ParametersData.Add(new ChartLibrary.ChartDataPopulation());
        ParametersData.Add(new ChartLibrary.ChartDataPopulation());
        ParametersData.Add(new ChartLibrary.ChartDataPopulation());

        selectedParameterData = ParametersData.First();

        dispatcherTimer = new DispatcherTimer();
        dispatcherTimer.Interval = TimeSpan.FromMilliseconds(1000);
        dispatcherTimer.Tick += DispatcherTimer_Tick;
    }
    Random random = new Random();
    private void DispatcherTimer_Tick(object sender, EventArgs e)
    {
        foreach (var item in ParametersData)
        {
            item.AddDataAndCalculate(random.NextDouble());
        }
    }
    private bool CanExecute(object arg)
    {
        return SelectedParameterData != null;
    }

    private void Execute(object obj)
    {
        try
        {
            if (obj != null)
            {
                switch (obj.ToString())
                {
                    case "DemoTimerStart": dispatcherTimer.Start(); break;
                    case "DemoTimerStop": dispatcherTimer.Stop(); break;
                    default:
                        break;
                }
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
    internal void Deinitialize()
    {
        try
        {
            dispatcherTimer.Stop();
            ParametersData.Clear();
            SelectedParameterData = null;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
    #region INotifyPropertyChangedImplementation
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (PropertyChanged != null) PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}
Posted
Updated 12-Jun-22 13:25pm

The collectionchanged event is only fired when something is added or removed.
One solution is to add and immediately remove it:

var newMylineSeries = new MyLineSeries(new Tag());
LineSeriesList.Add(newMylineSeries);
LineSeriesList.Remove(newMylineSeries);
 
Share this answer
 
Renewing, not clearing, an ObservableCollection will break the binding.
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900