none
Building Large TreeView - Binding Faster? RRS feed

  • Question

  • Only my second .net / WPF app so bear with my cluelessness.

    I have two classes that look like this:

        Public Class FntData
            Implements ICloneable
            Public Function Clone() As Object Implements ICloneable.Clone
                Return DirectCast(Me.MemberwiseClone(), FntData)
            End Function
            Public Property Flag As String              'a generic flag to be used for whatever is needed
            Public Property Family As String            'the font family name
            Public Property FontSty As String           'the style (italic, bold...)
            Public Property DisplayName As String       'the name that displays in the list
            Public Property PSName As String            'PS font name
            Public Property FullName As String          'full font name
            Public Property FName As String             'filename with path
            Public Property RegValWrite As String       'the value for the registry entry
            Public Property RegValRead As String        'the name read from the registry (for testing)
            Public Property FontType As String          'T1, TT, OT
            Public Property Copyright As String         'the copyright in the font
            Public Property IsOK As Boolean             'determines whether the font has everything it needs to be loaded
            Public Property IsElsewhere As Boolean      'indicates the font is also loaded outside of any sets or in other sets
            Public Property Msg As String               'to record info when reading, loading or removing a font file
        End Class
        Public Class FntSetData
            Public Property SetName As String
            Public Property SetFName As String
            Public Property Fnts As New List(Of FontUtils.FntData)
        End Class
    

    I'm currently reading them into an unbound TreeView with multiple FntSetData elements appearing at the start of the tree and FntData elements following. Looks like this:

    The "folders" are a list(of FntSetData) and appear in the tree first. The "loose" items are a list(of FntData) and read into the tree following all the "folders." The "TT", "O" and "a" icons are determined by the .FontType in the FntData class.

    There will be many, many FntData things reading into the bottom part of the TreeView and currently refreshing the TreeView is the slowest part of the app (too slow for everything I'd like to have there), even slower than reading all the font data from the files on the disk. :o/

    I'm trying to decide whether to try and spend what will likely be gobs of time trying to figure out the binding or whether to redesign and break up by two lists (folders and loose elements).

    So, two questions someone may be able to answer:

    1. Can something like this even be bound where I kind of have a hierarchical list at the top of the tree but non-hierarchical elements at the bottom of the tree?

    2. This may be the more important of the two questions: Will having it bound speed up the refresh of the tree at all?

    Thanks very much,
    Ken

    Monday, January 6, 2020 4:22 AM

All replies

  • Hi,

    You can refer to the link below to improve the performance of TreeView,

    VirtualizingStackPanel.IsVirtualizing="True"
    VirtualizingStackPanel.VirtualizationMode="Recycling"

    https://docs.microsoft.com/en-us/dotnet/framework/wpf/controls/how-to-improve-the-performance-of-a-treeview

    Best Regards,

    Alex


    MSDN Community Support Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Tuesday, January 7, 2020 7:10 AM
    Moderator
  • Hi,
    use Binding and the VirtualizationMode like in following demo. In the list of data use a base class and at first load the FntSetData with sub items and then load the FntData.

    XAML:

    <Window x:Class="Window63"
            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"
            mc:Ignorable="d"
            Title="Window63" Height="450" Width="800">
      <Window.DataContext>
        <local:Window63VM/>
      </Window.DataContext>
      <Grid>
        <TreeView ItemsSource="{Binding Items}"
                  VirtualizingPanel.VirtualizationMode="Recycling"
                  VirtualizingPanel.IsVirtualizing="True"
                  VirtualizingPanel.ScrollUnit="Item">
          <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:Window63FntSetData}" 
                                      ItemsSource="{Binding Fnts}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding SetName}" />
              </StackPanel>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:Window63FntData}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding Family}" />
              </StackPanel>
            </HierarchicalDataTemplate>
          </TreeView.Resources>
        </TreeView>
      </Grid>
    </Window>

    And the classes:

    Imports System.Collections.ObjectModel
    Imports System.ComponentModel
    
    Public Class Window63VM
    
      Private col As New ObservableCollection(Of Window63FntBase)
      Private cvs As New CollectionViewSource
    
      Public ReadOnly Property Items As ICollectionView
        Get
          If cvs.Source Is Nothing Then GetData()
          Return cvs.View
        End Get
      End Property
    
      Private Sub GetData()
        For i = 1 To 1000
          Dim d As New Window63FntSetData() With {.SetName = $"Set {i}"}
          For k = 1 To 20
            d.Fnts.Add(New Window63FntData With {.Family = $"Family {i} {k}"})
          Next
          col.Add(d)
        Next
        For i = 1 To 1000
          Dim d As New Window63FntData() With {.Family = $"Family {i}"}
          col.Add(d)
        Next
        cvs.Source = col
      End Sub
    
    End Class
    
    Public Class Window63FntBase
      Public Property Fnts As New List(Of Window63FntData)
    End Class
    
    Public Class Window63FntData
      Inherits Window63FntBase
      Implements ICloneable
      Public Function Clone() As Object Implements ICloneable.Clone
        Return DirectCast(Me.MemberwiseClone(), Window63FntData)
      End Function
      Public Property Flag As String              'a generic flag to be used for whatever is needed
      Public Property Family As String            'the font family name
      Public Property FontSty As String           'the style (italic, bold...)
      Public Property DisplayName As String       'the name that displays in the list
      Public Property PSName As String            'PS font name
      Public Property FullName As String          'full font name
      Public Property FName As String             'filename with path
      Public Property RegValWrite As String       'the value for the registry entry
      Public Property RegValRead As String        'the name read from the registry (for testing)
      Public Property FontType As String          'T1, TT, OT
      Public Property Copyright As String         'the copyright in the font
      Public Property IsOK As Boolean             'determines whether the font has everything it needs to be loaded
      Public Property IsElsewhere As Boolean      'indicates the font is also loaded outside of any sets or in other sets
      Public Property Msg As String               'to record info when reading, loading or removing a font file
    End Class
    
    Public Class Window63FntSetData
      Inherits Window63FntBase
      Public Property SetName As String
      Public Property SetFName As String
    End Class


    --
    Best Regards / Viele Grüße
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks

    Tuesday, January 7, 2020 8:05 AM
  • I'd forgetten about the Virtualizing (found it very early on) but it breaks (sometimes?) the keyboard navigation in the TreeView, where it selects the item but doesn't scroll it into view. I'm seeing now that the keyboard navigation sometimes comes back (???) so maybe there's a solution to that. With so many items in the list, the keyboard navigation will be important, but for now the speed with which it's loading is at least tolerable.

    I also decided to try eliminating by progress bar which was a HUGE time consumer and cut the loading again by a quarter?!?!?! I had this procedure to update the progress bar and a text box using a dispatcher and was calling it as each thing loaded. Amazing that a progress bar is so difficult to get to work in WPF. CERTAINLY not worth doing it this way with the time it was taking. Any hints on the progress bar thing are very welcome.

        Public Sub ProgBarUpdate(TheVal As Double, TheTxt$)
            Dim UpdProg As New UpdateProgDele(AddressOf Me.ProgBar.SetValue)
            Dim UpdTxt As New UpdateProgDele(AddressOf Me.LablProgText.SetValue)
            Dispatcher.Invoke(UpdProg, System.Windows.Threading.DispatcherPriority.Background, New Object() {ProgressBar.ValueProperty, TheVal})
            Dispatcher.Invoke(UpdTxt, System.Windows.Threading.DispatcherPriority.Background, New Object() {Label.ContentProperty, TheTxt$})
        End Sub
    

    WOW Viele! Thanks for taking all that time for that amazing example of binding, having it in this context will be much easier to work out. I'll try playing with that this weekend for sure.

    Again, I can't thank you guys enough,
    Ken

    Tuesday, January 7, 2020 2:15 PM
  • Hi Ken,
    try following demo with async populating data and ProgressBar.

    XAML:

    <Window x:Class="Window63"
            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"
            mc:Ignorable="d"
            Title="Window63" Height="450" Width="800">
      <Window.DataContext>
        <local:Window63VM/>
      </Window.DataContext>
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition/>
          <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TreeView ItemsSource="{Binding Items}"
                  VirtualizingPanel.VirtualizationMode="Recycling"
                  VirtualizingPanel.IsVirtualizing="True"
                  VirtualizingPanel.ScrollUnit="Item">
          <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:Window63FntSetData}" 
                                      ItemsSource="{Binding Fnts}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding SetName}" />
              </StackPanel>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:Window63FntData}">
              <StackPanel Orientation="Horizontal">
                <TextBlock VerticalAlignment="Center" Text="{Binding Family}" />
              </StackPanel>
            </HierarchicalDataTemplate>
          </TreeView.Resources>
        </TreeView>
        <ProgressBar Grid.Row="1" Value="{Binding Progress}" Height="20"/>
      </Grid>
    </Window>

    And classes:

    Imports System.Collections.ObjectModel
    Imports System.ComponentModel
    Imports System.Runtime.CompilerServices
    
    Public Class Window63VM
      Implements INotifyPropertyChanged
    
      Private col As New ObservableCollection(Of Window63FntBase)
      Private cvs As New CollectionViewSource
    
      Public ReadOnly Property Items As ICollectionView
        Get
          If cvs.Source Is Nothing Then
            cvs.Source = col
            StartGetData()
          End If
          Return cvs.View
        End Get
      End Property
    
      Private _progress As Double = 0
    
      Public Property Progress As Double
        Get
          Return Me._progress
        End Get
        Set(value As Double)
          Me._progress = value
          OnPropChanged()
        End Set
      End Property
    
      Private Sub StartGetData()
        Task.Factory.StartNew(New Action(AddressOf GetData))
      End Sub
    
      Private Sub GetData()
        For i = 1 To 1000
          Dim d As New Window63FntSetData() With {.SetName = $"Set {i}"}
          For k = 1 To 20
            d.Fnts.Add(New Window63FntData With {.Family = $"Family {i} {k}"})
          Next
          Threading.Thread.Sleep(100)
          Dim pr = i / 20
          Application.Current.Dispatcher.Invoke(New Action(Sub()
                                                             col.Add(d)
                                                             Progress = pr
                                                           End Sub))
        Next
        For i = 1 To 1000
          Dim d As New Window63FntData() With {.Family = $"Family {i}"}
          Threading.Thread.Sleep(100)
          Dim pr = 50 + i / 20
          Application.Current.Dispatcher.Invoke(New Action(Sub()
                                                             col.Add(d)
                                                             Progress = pr
                                                           End Sub))
        Next
    
      End Sub
    
      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 Window63FntBase
      Public Property Fnts As New List(Of Window63FntData)
    End Class
    
    Public Class Window63FntData
      Inherits Window63FntBase
      Implements ICloneable
      Public Function Clone() As Object Implements ICloneable.Clone
        Return DirectCast(Me.MemberwiseClone(), Window63FntData)
      End Function
      Public Property Flag As String              'a generic flag to be used for whatever is needed
      Public Property Family As String            'the font family name
      Public Property FontSty As String           'the style (italic, bold...)
      Public Property DisplayName As String       'the name that displays in the list
      Public Property PSName As String            'PS font name
      Public Property FullName As String          'full font name
      Public Property FName As String             'filename with path
      Public Property RegValWrite As String       'the value for the registry entry
      Public Property RegValRead As String        'the name read from the registry (for testing)
      Public Property FontType As String          'T1, TT, OT
      Public Property Copyright As String         'the copyright in the font
      Public Property IsOK As Boolean             'determines whether the font has everything it needs to be loaded
      Public Property IsElsewhere As Boolean      'indicates the font is also loaded outside of any sets or in other sets
      Public Property Msg As String               'to record info when reading, loading or removing a font file
    End Class
    
    Public Class Window63FntSetData
      Inherits Window63FntBase
      Public Property SetName As String
      Public Property SetFName As String
    End Class


    --
    Best Regards
    Peter Fleischer (former MVP for Developer Technologies)
    Homepage, Tipps, Tricks


    Tuesday, January 7, 2020 3:12 PM
  • Thank you Peter!

    Looking at implementing this ton of code you guys provided (thank you again!) but I'm feeling overwhelmed by it. I simply may not have enough base knowledge to get it working.

    I'll start with what is probably a basic question on the  data template:
    Your XML code included these two lines:

    xmlns:local="clr-namespace:WpfApp1"
    
    <HierarchicalDataTemplate DataType="{x:Type local:FntSetData}" ItemsSource="{Binding Fnts}">

    In my XML I added similar lines:

    xmlns:local="clr-namespace:FontInstaller"
    
    <HierarchicalDataTemplate DataType="{x:Type local:FntSetData}" ItemsSource="{Binding Fnts}">
    

    But I'm getting the error: The name "FntSetData" does not exist in the namespace "clr-namespace:FontInstaller"

    I suspect this has to do with the fact that my classes are defined in a module and not the main window but can't find what the XML syntax should be for declaring FntSetData in the Module.

    Thanks again (and again, and again...),
    Ken

    Saturday, January 18, 2020 9:30 PM