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?
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 :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With