当集合发生变化时,保持绑定的WPF ListBox的滚动位置

11

我有一个绑定在ObservableCollection上的 WPF ListBox。当一些内容被添加到这个集合中时,ListBox 的滚动位置会根据新增条目的大小而移动。我想保留滚动位置,使得在列表中添加内容时当前可见的条目不动,有什么方法可以实现吗?


我刚刚完成了一个非常简单的WPF数据绑定到一个ListBox,当我添加项目时,视图中的项目保持在视图中...你还做了什么? - TheCodeMonk
尝试将新项目添加到集合的开头。随着新项目的添加,您正在查看的项目开始移出视口。您添加1个项目-视口保持在相同的位置(基于索引),这意味着1个项目进入了视口并离开了它。 - EvAlex
6个回答

3
我遇到了同样的问题,还有一个限制:用户不会选择项目,只会滚动。这就是为什么 ScrollIntoView() 方法无用的原因。这是我的解决方案。
首先,我创建了一个名为ScrollPreserver的类,继承自DependencyObject,并带有一个类型为bool的附加依赖属性PreserveScroll
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);
    }

    ...
}

属性变更回调函数假定该属性由ScrollViewer设置。回调方法将此ScrollViewer添加到私有字典中,并向其添加ScrollChanged事件处理程序:

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);
        }
    }
}

这个字典将保存应用程序中使用此类的所有ScrollViewers的引用。在我的情况下,项目是添加到集合的开头。问题是视口位置不会改变。当位置为0时,视口中的第一个元素始终是第一个项目,这是可以接受的。但是,当视口中的第一个元素具有与0不同的索引并且添加了新项目时,该元素的索引会增加,因此它会向下滚动。因此,bool值指示ScrollViewer的垂直偏移量是否不为0。如果是0,则无需执行任何操作,如果不是0,则需要将其位置相对于视口中的项目保持不变:

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在这里被用于表示添加的项目数量。在我的情况下,项目只在集合的开头添加,所以我只需要通过这个值增加ScrollViewer的VerticalOffset即可。 最后一件事:用法。以下是ListBox的示例:

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

Ta-da! 很好地运作了 :)

DependencyProperties都是静态的。你可以在这里阅读相关信息:https://dev59.com/pXA75IYBdhLWcg3w_umD - EvAlex
不,我没有考虑到它作为ScrollViewers的数量在我的情况下是不变的。例如,您可以通过将一些标志放置在ScrollViewers的Tag属性中来避免使用静态字典。 - EvAlex
1
你应该像使用PreserveScroll的AttachedProperty一样,为ScrollViewer的状态使用AttachedProperty。这样可以避免内存泄漏。 - TcKs
感谢您对此问题的关注。 - EvAlex
我使用了您的答案作为我的解决方案的起点。您值得我的+1 :)。谢谢。 - TcKs
显示剩余3条评论

1

首先通过 ListBox.SelectedIndex 找到当前位置,然后使用 ListBox.ScrollIntoView(/* 当前索引 */)。即使列表中添加了新项目,您的当前位置和视图也将保持不变。


0
创建一个CollectionView。 尝试以下方法:
    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);
    }

1
请进一步解释。这个答案似乎没有解决问题 - 它只报告了当前项目和位置。我遇到了同样的问题,使用一个ItemsPanel设置为VirtualizingStackPanel的ListBox。我认为问题在于,随着新项添加到Observable集合中,列表框项目会继续移动。虽然可以通过将项目移回它们原来的位置(例如,使用SelectedIndex)来解决这个问题,但这会创建一个抖动的运动。需要的是一种方法,在添加新项时防止滚动位置发生变化。 - CyberMonk
我有同样的问题,但你的解决方案假设用户已选择某些内容。但是如果用户不想选择任何东西呢?只需滚动到某个地方然后离开即可。 - EvAlex

0

我不确定是否有针对此的覆盖,但我怀疑没有。

如果不确定,使用手动绑定? :-} 说真的,默认的绑定行为将是 - 嗯 - 默认的,因此如果您需要特殊行为,则手动绑定是更好的选择。使用手动绑定,您可以保存滚动位置并在添加项目后重置它。


这引出了一个显而易见的问题 - 我如何确定当前滚动位置? - Kevin Dente

0
你控制什么时候添加事物吗?我的意思是,你是否有一个或两个定义好的数据改变点?如果是这样的话,一种简单的解决方案(可能不够优雅)就是使用一个本地int变量来保存当前ListBox.SelectedIndex。在数据改变之前,将selectedIndex保存到此变量中,在数据添加完成后,将listBox的SelectedIndex设置为此变量的值即可。

0
我曾经遇到过同样的问题,这是我所做的: 1. 找到你的列表框的滚动查看器,并在其加载时添加一个滚动更改事件。
var scrollViewer = FindScrollViewer(ListBoxOrders);
if (scrollViewer != null)
{
    scrollViewer.ScrollChanged += scrollViewer_ScrollChanged;
}

这是用于查找滚动视图的函数:

    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;
}
  • 在滚动改变时,存储垂直偏移量

    private double _verticalOffset; private void scrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { var sv = (ScrollViewer)sender; _verticalOffset = sv.VerticalOffset; }

  • 刷新后,滚动到先前的位置

    scrollViewer?.ScrollToVerticalOffset(_verticalOffset);


  • 网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接