Clarify INotifyCollectionChanged

Ken Krugh 116 Reputation points
2020-06-11T12:01:25.887+00:00

I have a TreeView bound to an ObservableCollection with a ViewModel. The TreeView includes folder-like elements that will have child items. Most times opening the app we won't need to view those child items so I'm not filling them in when the app opens, rather each folder-like item has a place-holder item to indicate there ARE child items. I've included this in my TreeView XAML to call a routine when the TreeViewItem is expanded:

TreeViewItem.Expanded="TVExp"  

That is working fine but the new child items do not show in the TreeView unless I call .Refresh() on the data items.

Now, ObservableCollection states: "Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed." But that doesn't seem to really be the case as the child items added to the data don't show in the UI.

It appears I have to implement the INotifyCollectionChanged interface. True? VisualStudio (amazingly!) will add code designed to help with this but I'm not sure where to start. Does it get implemented on the ViewModel or on the ObservableCollection, or both?

I imagine it's implemented something like INotifyPropertyChanged but I've not manged to find a good example that's simple enough for my newbee .net knowledge to follow.

Can someone give an example of that, or point me to one?

Many thanks,
Ken

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,671 questions
{count} votes

3 answers

Sort by: Most helpful
  1. Ken Krugh 116 Reputation points
    2020-06-11T17:49:51.29+00:00

    Hi Again Peter! VB would be best.

    Thank you (again!)

    0 comments No comments

  2. Peter Fleischer (former MVP) 19,231 Reputation points
    2020-06-12T06:06:27.193+00:00

    Hi Ken, try to follow the demo. At first instead of child data dummy data will be included. The demo uses attached property (for TreeViewItem) to catch the Expanded event. If child data included only one dummy data object the real child data will be loaded.

    XAML:

    <Window x:Class="Window024"
            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:WpfApp1.WpfApp024"
            mc:Ignorable="d"
            Title="Ken Krugh TreeView lazy loading" Height="450" Width="800">
      <Window.DataContext>
        <local:ViewModel/>
      </Window.DataContext>
      <Grid>
        <TreeView x:Name="FolderView"
                  ItemsSource="{Binding Items}"
                  VirtualizingPanel.VirtualizationMode="Recycling"
                  VirtualizingPanel.IsVirtualizing="True">
          <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
              <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
              <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
              <Setter Property="local:TviBehavior.Expanded" Value="True"/>
            </Style>
          </TreeView.ItemContainerStyle>
          <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:Data1}" ItemsSource="{Binding Children}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
              </StackPanel>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:DataDummy}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
              </StackPanel>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:Data2}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
              </StackPanel>
            </HierarchicalDataTemplate>
          </TreeView.Resources>
        </TreeView>
      </Grid>
    </Window>
    

    Imports System.Collections.ObjectModel
    Imports System.ComponentModel
    Imports System.Runtime.CompilerServices
    
    Namespace WpfApp024
    
      Public Class ViewModel
    
        Private col As New ObservableCollection(Of Data0)
        Private cvs As New CollectionViewSource
    
        Public ReadOnly Property Items As ICollectionView
          Get
            If cvs.Source Is Nothing Then
              cvs.Source = col
              Model.LoadFirstData(col)
            End If
            Return cvs.View
          End Get
        End Property
    
      End Class
    
      Public Class Data0
        Implements INotifyPropertyChanged
        Public Property Name As String
        Public Property IsExpanded As Boolean = False
    
        Private _IsSelected As Boolean = True
        Public Property IsSelected As Boolean
          Get
            Return Me._IsSelected
          End Get
          Set(value As Boolean)
            Me._IsSelected = value
            OnPropChanged()
          End Set
        End Property
    
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
        Friend Sub OnPropChanged(<CallerMemberName> Optional propName As String = "")
          RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propName))
        End Sub
      End Class
    
      Public Class Data1
        Inherits Data0
        Public Property Children As New ObservableCollection(Of Data0)
      End Class
    
      Public Class Data2
        Inherits Data0
    
      End Class
    
      Public Class DataDummy
        Inherits Data0
    
      End Class
    
      Public Class TviBehavior
        Public Shared ReadOnly ExpandedProperty As DependencyProperty =
        DependencyProperty.RegisterAttached(
        "Expanded",
        GetType(Boolean),
        GetType(TviBehavior),
        New UIPropertyMetadata(False, AddressOf ViewModelChanged))
    
        Public Shared Function GetExpanded(tvi As TreeViewItem) As Boolean
          Return CType(tvi.GetValue(ExpandedProperty), Boolean)
        End Function
    
        Public Shared Sub SetExpanded(tvi As TreeViewItem, value As Boolean)
          tvi.SetValue(ExpandedProperty, value)
        End Sub
    
        Private Shared Sub ViewModelChanged(depObj As DependencyObject, e As DependencyPropertyChangedEventArgs)
          Dim item = TryCast(depObj, TreeViewItem)
          If item Is Nothing OrElse Not CType(e.NewValue, Boolean) Then Exit Sub
          If CType(e.NewValue, Boolean) Then
            AddHandler item.Expanded, AddressOf OnTreeViewItemExpanded
          Else
            RemoveHandler item.Expanded, AddressOf OnTreeViewItemExpanded
          End If
        End Sub
    
        Private Shared Sub OnTreeViewItemExpanded(sender As Object, e As RoutedEventArgs)
          If Not Object.ReferenceEquals(sender, e.OriginalSource) Then Exit Sub
          Dim item = TryCast(e.OriginalSource, TreeViewItem)
          Dim d = TryCast(item.DataContext, Data1)
          If (d.Children.Count = 1 AndAlso d.Children(0).GetType IsNot GetType(DataDummy)) Then Exit Sub
          d.Children.Clear()
          Model.LoadSecondData(d)
        End Sub
      End Class
    
      Module Model
        Friend Sub LoadFirstData(col As ObservableCollection(Of Data0))
          For i = 1 To 100
            Dim d As New Data1 With {.Name = $"Node {i}"}
            d.Children.Add(New DataDummy With {.Name = "Dummy"})
            col.Add(d)
          Next
        End Sub
        Friend Sub LoadSecondData(d1 As Data1)
          For i = 1 To 10
            Dim d2 As New Data2 With {.Name = $"Node {i}"}
            d1.Children.Add(d2)
          Next
        End Sub
      End Module
    
    End Namespace
    
    0 comments No comments

  3. Ken Krugh 116 Reputation points
    2020-06-16T01:41:40.007+00:00

    Thanks again Peter but I've got yet another newbee question.

    You seem to have implemented a custom property (is Attached Property the correct terminology?) that triggers on the ViewModel getting updated. Do I have that right?

    Does that trigger INotifyCollectionChanged? Just trying to better understand because I had expected to see the code "Implements INotifyCollectionChanged" similar to the use of "Implements INotifyPropertyChanged" in the ViewModel.

    All Best,
    Ken