user10466538
user10466538

Reputation:

WPF INotifyPropertyChanged and INotifyCollectionChanged not being called

I have implemented the INotifyCollectionChanged (probably incorrectly) and INotifyPropertyChanged for the individual items in my DataGrid. I amend the data once the Window is renderes(ContentRendered="Window_ContentRendered" ) to see if the DataGrid gets updated. I am having 2 issues.

  1. When I call "priceLadderData.data.Values[0].ChangePrice(1);" I can see that OnPropertyChanged gets hit but no change is visible. 2.When I call "priceLadderData.AddRow(new PriceLadderRowData(99, 99));" I get the error below:

Exception: Information for developers (use Text Visualizer to read this): This exception was thrown because the generator for control 'System.Windows.Controls.DataGrid Items.Count:5' with name 'dataGrid' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected: Accumulated count 4 is different from actual count 5. [Accumulated count is (Count at last Reset + #Adds - #Removes since last Reset).] At index 0: Generator's item '[364525, WPF_PriceLadder.PriceLadderRowData]' is different from actual item '[99, WPF_PriceLadder.PriceLadderRowData]'. At index 1: Generator's item '[364550, WPF_PriceLadder.PriceLadderRowData]' is different from actual item '[364525, WPF_PriceLadder.PriceLadderRowData]'. At index 2: Generator's item '[364575, WPF_PriceLadder.PriceLadderRowData]' is different from actual item '[364550, WPF_PriceLadder.PriceLadderRowData]'. (... 1 more instances ...)

One or more of the following sources may have raised the wrong events: System.Windows.Controls.ItemContainerGenerator System.Windows.Controls.ItemCollection MS.Internal.Data.EnumerableCollectionView * System.Collections.Generic.SortedList`2[[System.Single, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[WPF_PriceLadder.PriceLadderRowData, WPF_Console2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] (The starred sources are considered more likely to be the cause of the problem.)

The most common causes are (a) changing the collection or its Count without raising a corresponding event, and (b) raising an event with an incorrect index or item parameter.

The exception's stack trace describes how the inconsistencies were detected, not how they occurred. To get a more timely exception, set the attached property 'PresentationTraceSources.TraceLevel' on the generator to value 'High' and rerun the scenario. One way to do this is to run a command similar to the following:\n
System.Diagnostics.PresentationTraceSources.SetTraceLevel(myItemsControl.ItemContainerGenerator, System.Diagnostics.PresentationTraceLevel.High) from the Immediate window. This causes the detection logic to run after every CollectionChanged event, so it will slow down the application.

MainWindow.xaml:

<Window x:Class="WPF_PriceLadder.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:WPF_PriceLadder"
        mc:Ignorable="d"
        ContentRendered="Window_ContentRendered" 
        Title="MainWindow" Height="450" Width="400">

    <Grid>
        <DataGrid x:Name="dataGrid" 
                  HorizontalScrollBarVisibility="Hidden"
                  VerticalScrollBarVisibility="Hidden" 
                  IsManipulationEnabled="False" 
                  IsReadOnly="True"
                  AllowDrop="False"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  EnableColumnVirtualization="True"
                  CanUserReorderColumns="False"
                  CanUserSortColumns="False"
                  CanUserResizeRows="False" 
                  SelectionMode="Single"
                  FontWeight="Normal"
                  AutoGenerateColumns="False"
                  SelectionUnit="Cell"
                  >

            <DataGrid.Columns>
                <DataGridTextColumn Header="Price" Binding="{Binding Value.Price, UpdateSourceTrigger=PropertyChanged,NotifyOnSourceUpdated=True}">
                </DataGridTextColumn>
    
                <DataGridTextColumn Header="Volume" Binding="{Binding Value.Volume,UpdateSourceTrigger=PropertyChanged}">
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Collections.Specialized;

namespace WPF_PriceLadder
{
public partial class MainWindow : Window
{
    public PriceLadderData priceLadderData;

    public MainWindow()
    {
        priceLadderData = new PriceLadderData();
        FillUpWithDummyRows(priceLadderData);
        this.DataContext = priceLadderData;
        InitializeComponent();
        dataGrid.ItemsSource = priceLadderData.data;
    }

    public void FillUpWithDummyRows(PriceLadderData priceLadderData)
    {
        priceLadderData.AddRow(new PriceLadderRowData(364600, 37));
        priceLadderData.AddRow(new PriceLadderRowData(364575, 18));
        priceLadderData.AddRow(new PriceLadderRowData(364550, 30));
        priceLadderData.AddRow(new PriceLadderRowData(364525, 20));
    }

    public void Window_ContentRendered(object sender, EventArgs e)
    {
        priceLadderData.data.Values[0].ChangePrice(1);
        priceLadderData.AddRow(new PriceLadderRowData(99, 99));
    }
}

public class PriceLadderRowData : IComparable, INotifyPropertyChanged
{

    private float price;
    private int volume = 0;
    public event PropertyChangedEventHandler PropertyChanged;

    public float Price
    {
        get
        {
            return price;
        }
        private set
        {
            price = value;
        }
    }
    public int Volume
    {
        get
        {
            return volume;

        }
        private set
        {
            volume = value;

        }
    }

    public PriceLadderRowData(float price, int volume)
    {
        this.Price = price;
        this.Volume = volume;
    }

    public int CompareTo(object obj)
    {
        return Price.CompareTo(obj);
    }

    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
    public void ChangePrice(float price)
    {
        this.Price = price;
        OnPropertyChanged();
    }
}

public class PriceLadderData : INotifyCollectionChanged
{
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    public SortedList<float, PriceLadderRowData> data = new SortedList<float, PriceLadderRowData>();

    public PriceLadderData()
    {

    }

    protected void OnCollectionChanged([CallerMemberName] string name = null)
    {
        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
    }

    public void AddRow(PriceLadderRowData priceLadderRowData)
    {
        this.data.Add(priceLadderRowData.Price, priceLadderRowData);
        OnCollectionChanged();
    }
}

}

Does anyone have any what is wrong here? No keyboard has been broken yet but I got close to it few times while learning WPF & XAML so I really appriciate your help.

Upvotes: 0

Views: 492

Answers (2)

RolandJS
RolandJS

Reputation: 1035

Question 1. Update of price.

When you are changing the price, you are calling OnPropertyChanged() in the ChangePrice() function. But the "[CallerMemberName]" used in OnPropertyChanged is getting the name of the calling function as the property name, in this case "ChangePrice". But the property name is "Price". Normally OnPropertyChanged is called in the setter of the Price property:

   public float Price
    {
        get
        {
            return price;
        }
        private set
        {
            price = value;
            OnPropertyChanged();
        }
    }

Question 2. SortedList.

I agree with everyone else. Use an ObservableCollection instead. There are a lot of functionality that works with XAML.

But you want it sorted. Here I have changed the PriceLadderData class to handle this: (The purist will probably say that you should do it in XAML and you can, but here it is in code instead.)

public class PriceLadderData
{
    public ObservableCollection<PriceLadderRowData> data { get; } = new ObservableCollection<PriceLadderRowData>();

    public CollectionView dataView { get; private set; }

    public PriceLadderData()
    {
        dataView = (CollectionView)CollectionViewSource.GetDefaultView(data); 
        dataView.SortDescriptions.Add(new SortDescription(nameof(PriceLadderRowData.Price), ListSortDirection.Ascending));
    }

    public void AddRow(PriceLadderRowData priceLadderRowData)
    {
        this.data.Add(priceLadderRowData);
    }

    public void Refresh()
    {
    //Refresh the sort order
        dataView.Refresh();
    }
}

Comments: Added a CollectionView (dataView) which will be the sorted view of the data that will be bound to the DataGrid.

Set up the sort instructions for the dataView in the constructor of PriceLadderData.

Bind the dataView to the DataGrid in XAML:

    <DataGrid x:Name="dataGrid" ItemsSource="{Binding dataView}"

(remove the settings of ItemsSource in the MainWindow constructor)

If you are adding data or changing the sort key, you must refresh the sorting:

    public void Window_ContentRendered(object sender, EventArgs e)
    {
        priceLadderData.data[0].ChangePrice(1);
        priceLadderData.AddRow(new PriceLadderRowData(99, 99));
        priceLadderData.Refresh();
    }

You can also set a CollectionChanged event on data, but that will not take care of the situation when you only change a property.

If you want to change data in the DataGrid, you need to remove the "IsReadOnly=True" from the DataGrid and set that on the individual columns. You also need to remove the "private" on the propertys setter.

Upvotes: 2

Brannon
Brannon

Reputation: 5414

Your use of NotifyCollectionChangedEventArgs() is incorrect; you need to use the overload that takes newItems and oldItems and populate those.

Also, you're exposing your (should-be) encapsulated collection through the public data field. This is going to lead you down a bad path; it has already obscured the issue with the way you are calling the NotifyCollectionChangedEventArgs constructor. If you have calls in your code that look like x.y.z.w() you're doing something wrong.

Upvotes: 0

Related Questions