Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preserving a bound WPF ListBox's scroll position when the collection changes

Tags:

wpf

listbox

I have a WPF ListBox bound to an ObservableCollection. When things are added to the collection, the ListBox scroll position shifts by the size of the added entries. I'd like to be able to preserve the scroll position, so that even as things are added to the list, the items currently in view don't move. Is there a way to accomplish this?

like image 309
Kevin Dente Avatar asked May 13 '09 22:05

Kevin Dente


2 Answers

I faced the same problem and one more restriction: user doesn't select items, but only scrolls. That's why the ScrollIntoView() method is useless. Here's my solution.

First, I created class ScrollPreserver deriving it from DependencyObject, with an attached dependency property PreserveScroll of type bool:

public class ScrollPreserver : DependencyObject
{
    public static readonly DependencyProperty PreserveScrollProperty =
        DependencyProperty.RegisterAttached("PreserveScroll", 
            typeof(bool),
            typeof(ScrollPreserver), 
            new PropertyMetadata(new PropertyChangedCallback(OnScrollGroupChanged)));

    public static bool GetPreserveScroll(DependencyObject invoker)
    {
        return (bool)invoker.GetValue(PreserveScrollProperty);
    }

    public static void SetPreserveScroll(DependencyObject invoker, bool value)
    {
        invoker.SetValue(PreserveScrollProperty, value);
    }

    ...
}

Property changed callback assumes the property is set by the ScrollViewer. The callback method adds this ScrollViewer to the private Dictionary and adds ScrollChanged event handler to it:

private static Dictionary<ScrollViewer, bool> scrollViewers_States = 
    new Dictionary<ScrollViewer, bool>();

private static void OnScrollGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ScrollViewer scrollViewer = d as ScrollViewer;
    if (scrollViewer != null && (bool)e.NewValue == true)
    {
        if (!scrollViewers_States.ContainsKey(scrollViewer))
        {
            scrollViewer.ScrollChanged += new    ScrollChangedEventHandler(scrollViewer_ScrollChanged);
            scrollViewers_States.Add(scrollViewer, false);
        }
    }
}

This dictionary will hold references to all ScrollViewers in application that use this class. In my case items are added in the beginning of the collection. The problem is that viewport position doesn't change. It is ok when the position is 0: first element in viewport will always be the first item. But when first element in viewport has another index than 0 and new items are added - that element's index increases so it's scrolled down. So bool value indicates whether ScrollViewer's vertical offset is not 0. If it is 0 there is no need to do anything, and if it is not 0 it is necessary to preserve its position relative to items in viewport:

static void scrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (scrollViewers_States[sender as ScrollViewer])
        (sender as ScrollViewer).ScrollToVerticalOffset(e.VerticalOffset + e.ExtentHeightChange);

    scrollViewers_States[sender as ScrollViewer] = e.VerticalOffset != 0;
}

ExtentHeightChange is used here to indicate the amount of added items. In my case items are added only at the beginning of the collection, so all I needed to do is to increase ScrollViewer's VerticalOffset by this value. And the last thing: usage. Here's the example for ListBox:

<Listbox ...> 
    <ListBox.Resourses>
        <Style TargetType="ScrollViewer">
            <Setter Property="local:ScrollPreserver.PreserveScroll" Value="True" />
        </Style>
    </ListBox.Resourses>
</ListBox>

Ta-da! Works nice :)

like image 195
EvAlex Avatar answered Dec 24 '22 03:12

EvAlex


Firstly find the current position by ListBox.SelectedIndex and then use ListBox.ScrollIntoView(/* Current Index */). Even if the new items are added in list your current position and view will be the same.

like image 36
Rushin Avatar answered Dec 24 '22 04:12

Rushin