0

I'm using WPF/MVVM TreeView to display some data. Since there can be thousands of records I display it using pages - 100 items per page, otherwise the tree will be severely lagging and unresponsive. I have encapsulated the paging functionality into a class represented below by the object DuplicateFilesPageView that exposes the ObservableCollection<> object - Collection which is bound to the TreeView. To change the page DuplicateFilesPageView calls ObservableCollection<>.Clear() and then ObservableCollection<>.Add(item) for each item to repopulate the collection.

The problem is that when items is cleared and then new one added, the vertical scroll bar does not change its position. If it was scrolled in the middle it will scroll the new view to the same position or to the end of the list if there is not enough items.

What I need it to do is when the items is reset, the vertical scroll bar position also need to be reset to 0.

I have tried raising the event OnPropertyChanged(name(DuplicateFilesPageView .Collection)) on my ViewModel, but that doesn't help. Also I don't see any properties that can change this behavior.

I'll appreciate any advise on how to solve this issue.

<TreeView Grid.Row="1" ItemsSource="{Binding DuplicateFilesPageView.Collection}" Margin="0,0,3,0">
    <TreeView.Resources>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
    </TreeView.Resources>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <Setter Property="IsExpanded" Value="True" />
            <Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

2 Answers2

0
/// <summary>
///     Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable.
/// </summary>
internal class BindableTreeViewSelectedItemBehavior : Behavior<TreeView>
{
    #region " SelectedItem "
    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableTreeViewSelectedItemBehavior), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));

    public object SelectedItem
    {
        get { return this.GetValue(SelectedItemProperty); }
        set { this.SetValue(SelectedItemProperty, value); }
    }

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue is TreeViewItem item)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
            item.Focus();
            return;
        }

        if (sender is BindableTreeViewSelectedItemBehavior behavior)
        {
            var treeView = behavior.AssociatedObject;
            if (treeView != null)
            {
                item = GetTreeViewItem(treeView, e.NewValue);
                if (item != null)
                {
                    item.IsSelected = true;
                    item.Focus();
                }
            }
        }
    }
    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged;
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged;
        }
    }

    #region " GetBringIndexIntoView "
    /// <summary>
    /// GetBringIndexIntoView
    /// </summary>
    /// <param name="itemsHostPanel"></param>
    /// <returns>Action<int></returns>
    private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
    {
        if (itemsHostPanel is VirtualizingStackPanel virtualizingPanel)
        {
            var method = virtualizingPanel.GetType().GetMethod("BringIndexIntoView", BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder, new[] { typeof(int) }, null);
            if (method != null)
            {
                return i => method.Invoke(virtualizingPanel, new object[] { i });
            }
        }

        return null;
    }
    #endregion

    #region " GetTreeViewItem "
    /// <summary>
    /// GetTreeViewItem
    /// Recursively search for an item in this subtree.
    /// </summary>
    /// <param name="container">The parent ItemsControl. This can be a TreeView or a TreeViewItem.</param>
    /// <param name="item">The item to search for.</param>
    /// <returns>TreeViewItem</returns>
    private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
    {
        if (container != null)
        {
            if (container.DataContext == item)
            {
                return container as TreeViewItem;
            }

            // Expand the current container
            if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
            {
                container.SetValue(TreeViewItem.IsExpandedProperty, true);
            }

            // Try to generate the ItemsPresenter and the ItemsPanel.
            // by calling ApplyTemplate.  Note that in the 
            // virtualizing case even if the item is marked 
            // expanded we still need to do this step in order to 
            // regenerate the visuals because they may have been virtualized away.
            container.ApplyTemplate();
            var itemsPresenter = (ItemsPresenter)container.Template.FindName("ItemsHost", container);
            if (itemsPresenter != null)
            {
                itemsPresenter.ApplyTemplate();
            }
            else
            {
                // The Tree template has not named the ItemsPresenter, 
                // so walk the descendents and find the child.
                itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
                if (itemsPresenter == null)
                {
                    container.UpdateLayout();
                    itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
                }
            }

            if (itemsPresenter != null)
            {
                var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

                // Ensure that the generator for this panel has been created.
#pragma warning disable 168
                var children = itemsHostPanel.Children;
#pragma warning restore 168

                var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
                for (int i = 0, count = container.Items.Count; i < count; i++)
                {
                    TreeViewItem subContainer;
                    if (bringIndexIntoView != null)
                    {
                        // Bring the item into view so 
                        // that the container will be generated.
                        bringIndexIntoView(i);
                        subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
                    }
                    else
                    {
                        subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);

                        // Bring the item into view to maintain the 
                        // same behavior as with a virtualizing panel.
                        subContainer.BringIntoView();
                    }

                    if (subContainer == null)
                    {
                        continue;
                    }

                    // Search the next level for the object.
                    var resultContainer = GetTreeViewItem(subContainer, item);
                    if (resultContainer != null)
                    {
                        return resultContainer;
                    }

                    // The object is not under this TreeViewItem
                    // so collapse it.
                    subContainer.IsExpanded = false;
                }
            }
        }

        return null;
    }
    #endregion
}

I have used this behavior in the past to get a treeview to update when a selecteditem changed via binding to get the item to scroll into view. You can use it like this:

<TreeView Grid.Column="1" Grid.Row="1" ItemsSource="{Binding MyTree}" MaxHeight="270" ScrollViewer.VerticalScrollBarVisibility="Auto" VirtualizingStackPanel.IsVirtualizing="True">
                <i:Interaction.Behaviors>
                    <h:BindableTreeViewSelectedItemBehavior SelectedItem="{Binding SelectedTV}"></h:BindableTreeViewSelectedItemBehavior>
                </i:Interaction.Behaviors>
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type wiz:TVTest}">
                        <Label Content="{Binding Name}"></Label>
                        <HierarchicalDataTemplate.ItemTemplate>
                            <HierarchicalDataTemplate ItemsSource="{Binding Children}" DataType="{x:Type wiz:TVTest}">
                                <Label Content="{Binding Name}"></Label>
                                <HierarchicalDataTemplate.ItemTemplate>
                                    <DataTemplate DataType="{x:Type wiz:TVTest}">
                                        <Label Content="{Binding Name}"></Label>
                                    </DataTemplate>
                                </HierarchicalDataTemplate.ItemTemplate>
                            </HierarchicalDataTemplate>
                        </HierarchicalDataTemplate.ItemTemplate>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>

Then you can set the selected item to the first item in the treeview and it should scroll to the top.

Here are the helper functions:

public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject
    {
        return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T);
    }

public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual)
    {
        if (visual == null)
        {
            yield break;
        }

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(visual, i);
            yield return child;

            if (VisualTreeHelper.GetChildrenCount(child) == 0)
            {
                continue;
            }

            foreach (DependencyObject subChild in GetVisualDescendants(child))
            {
                yield return subChild;
            }
        }
    }
Kevin Cook
  • 1,922
  • 1
  • 15
  • 16
  • Thank you! Looks like the correct solution, I'll be trying it this weekend (That's for an open source project). WPF continues to amaze me, - to do some simple things you need to pass through fire, but things that appear complex is usually done easily. – Dennis Reshetnyak Sep 06 '19 at 14:09
0

When you're selecting the new Page, an event is fired. As I don't quite understand where you're doing this I used "SelectedItemChanged" from Treeview.

Then you'll have to: get the Scrollviewer first, following this: Get datagrid's scrollviewer

with that Scrollviewer call yourScrollviewer.ScrollToTop()

private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        GetScrollViewer(sender as UIElement).ScrollToTop();
    }

    public static ScrollViewer GetScrollViewer(UIElement element)
    {
        if (element == null) return null;

        ScrollViewer retour = null;
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element) && retour == null; i++)
        {
            if (VisualTreeHelper.GetChild(element, i) is ScrollViewer)
            {
                retour = (ScrollViewer)(VisualTreeHelper.GetChild(element, i));
            }
            else
            {
                retour = GetScrollViewer(VisualTreeHelper.GetChild(element, i) as UIElement);
            }
        }
        return retour;
    }
snicz
  • 61
  • 7