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?

- 3,741
- 12
- 49
- 72

- 25,430
- 7
- 43
- 47
-
I just did a really simple wpf databinding to a listbox and when I add items the items that were in view stayed in view... What else are you doing? – TheCodeMonk May 15 '09 at 17:27
-
Try to add new items to the biginning of the collection. Items you are viewing start move out of viewport as new items are added. You add 1 item - viewport stays at the same position (index-based) which means that 1 item entered viewport and 1 item left it. – EvAlex Mar 11 '12 at 07:46
6 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 :)

- 2,888
- 1
- 20
- 24
-
-
DependencyProperties are all static. You can read about it for example here: http://stackoverflow.com/questions/2989431/why-are-dependency-properties-static – EvAlex Jun 10 '13 at 16:00
-
I know. But the "scrollViewers_States" field is not shortcut for dependency property. It stores the instances of ScrollViewer. Do you have some code which removes old instances from the dictionary? – TcKs Jun 10 '13 at 16:02
-
No, I didn't think about it as number of ScrollViewers doesn't change in my case. You can avoid using static dictionary by placing some flag to ScrollViewers' Tag property, for example. – EvAlex Jun 10 '13 at 16:24
-
And if another feature's author will have same idea (using Tag property) both features will be broken. – TcKs Jun 11 '13 at 09:49
-
1You should use AttachedProperty for the state of ScrollViewer like you used AttachedProperty for PreserveScroll. Then you will avoid memory leaks. – TcKs Jun 11 '13 at 09:53
-
-
I used your answer asi start point for my solution. You deserve my +1 :). Thanks. – TcKs Jun 13 '13 at 07:44
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.

- 11
- 1
I had the same problem, here is what I did: 1. Find the scrollviewer of your listbox when it is leaded and add a scrollchanged event.
var scrollViewer = FindScrollViewer(ListBoxOrders);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += scrollViewer_ScrollChanged;
}
Here is the function to find the scrollviewer:
private ScrollViewer FindScrollViewer(DependencyObject d)
{
if (d is ScrollViewer)
return d as ScrollViewer;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(d); i++)
{
var sw = FindScrollViewer(VisualTreeHelper.GetChild(d, i));
if (sw != null) return sw;
}
return null;
}
On scroll changed, store the vertical offset
private double _verticalOffset; private void scrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { var sv = (ScrollViewer)sender; _verticalOffset = sv.VerticalOffset; }
After refresh, scroll to the previous position
scrollViewer?.ScrollToVerticalOffset(_verticalOffset);

- 1,087
- 11
- 15
I'm not sure if there's an override for that, but I doubt it.
When in doubt use manual binding? :-} Seriously, the default binding behavior will be - well - default, so if you need special behavior manual binding is a better choice. With manual binding you can save the scroll position and reset it after you've added the items.

- 17,302
- 14
- 89
- 134
-
Which leads to an obvious question - how do I determine the current scroll position? – Kevin Dente May 15 '09 at 22:11
Create a CollectionView. Try this:
private ObservableCollection<int> m_Values;
private CollectionView m_View;
private void Bind()
{
m_Values = new ObservableCollection<int>();
m_View = new CollectionView(m_Values);
MyListBox.ItemsSource = m_View;
}
private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.WriteLine(m_View.CurrentItem);
Debug.WriteLine(m_View.CurrentPosition);
}

- 2,521
- 1
- 21
- 39
-
1Please explain further. This answer does not seem to solve the problem - it just reports the current item and position. I am having the same problem, using a ListBox with a ItemsPanel set to a VirtualizingStackPanel. The problem as I see it is that the list box items continue to move as new items are added to the Observable collection. While it is possible to move items back to where they were after a new item is added (e.g., by using SelectedIndex), this creates a jerky motion. What is needed is a way to prevent the scroll position from changing in the first place as new items are added. – CyberMonk Jun 25 '10 at 19:17
-
I have the same problem, but your solution assumes somethig is selected. But what if user does not want to select anything? Just scroll somewhere and leave it. – EvAlex Mar 11 '12 at 07:41
Do you control when things are added? I mean do you have one or two defined points where data gets changed? If so, one easy solution (perhaps not elegant) would be so have a local int variable to hold the current ListBox.SelectedIndex. Just before data changes, save the selectedIndex to this variable and after data is added, set the SelectedIndex of the listBox to the value of this variable.

- 10,527
- 23
- 71
- 104