【问题标题】:Preserving a bound WPF ListBox's scroll position when the collection changes集合更改时保留绑定的 WPF ListBox 的滚动位置
【发布时间】:2009-05-13 22:33:09
【问题描述】:

我有一个 WPF ListBox 绑定到 ObservableCollection。将事物添加到集合中时,ListBox 滚动位置会根据添加条目的大小移动。我希望能够保留滚动位置,以便即使将内容添加到列表中,当前视图中的项目也不会移动。有没有办法做到这一点?

【问题讨论】:

  • 我刚刚对列表框做了一个非常简单的 wpf 数据绑定,当我添加项目时,视图中的项目仍然在视图中......你还在做什么?
  • 尝试将新项目添加到集合的 biginning 中。随着新项目的添加,您正在查看的项目开始移出视口。您添加 1 个项目 - 视口保持在同一位置(基于索引),这意味着 1 个项目进入视口,1 个项目离开它。

标签: wpf listbox


【解决方案1】:

我遇到了同样的问题,还有一个限制:用户不选择项目,而只是滚动。这就是ScrollIntoView() 方法没用的原因。这是我的解决方案。

首先,我创建了从 DependencyObject 派生的类 ScrollPreserver,并附加了 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 添加到私有 Dictionary 并为其添加 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);
        }
    }
}

该字典将保存对应用程序中使用此类的所有 ScrollViewer 的引用。在我的情况下,项目被添加到集合的开头。问题是视口位置不会改变。位置为 0 时可以:视口中的第一个元素将始终是第一项。但是当视口中的第一个元素有另一个索引而不是 0 并且添加了新项目时 - 该元素的索引会增加,因此它会向下滚动。所以bool值表示ScrollViewer的垂直偏移量是否不为0,如果为0则不需要做任何事情,如果不为0则需要保留其相对于viewport中item的位置:

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来表示增加的item的数量。在我的情况下,项目只在集合的开头添加,所以我需要做的就是将 ScrollViewer 的 VerticalOffset 增加这个值。 最后一件事:使用。下面是 ListBox 的示例:

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

哒哒!很好用:)

【讨论】:

  • 不会因为静态字段中的字典而导致内存泄漏吗?
  • DependencyProperties 都是静态的。例如,您可以在这里阅读:stackoverflow.com/questions/2989431/…
  • 我知道。但是“scrollViewers_States”字段不是依赖属性的快捷方式。它存储 ScrollViewer 的实例。你有一些从字典中删除旧实例的代码吗?
  • 不,我没有考虑过,因为 ScrollViewers 的数量在我的情况下没有改变。例如,您可以通过在 ScrollViewers 的 Tag 属性中放置一些标志来避免使用静态字典。
  • 您应该将 AttachedProperty 用于 ScrollViewer 的状态,就像您将 AttachedProperty 用于 PreserveScroll。这样你就可以避免内存泄漏。
【解决方案2】:

首先通过 ListBox.SelectedIndex 找到当前位置,然后使用 ListBox.ScrollIntoView(/* Current Index */)。即使在列表中添加了新项目,您当前的位置和视图也将是相同的。

【讨论】:

    【解决方案3】:

    我不确定是否有替代方案,但我对此表示怀疑。

    如有疑问,请使用手动装订? :-} 说真的,默认绑定行为将是-嗯-默认,所以如果您需要特殊行为,手动绑定是更好的选择。使用手动绑定,您可以保存滚动位置并在添加项目后将其重置。

    【讨论】:

    • 这引出了一个明显的问题——我如何确定当前的滚动位置?
    【解决方案4】:

    创建一个集合视图。 试试这个:

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

    【讨论】:

    • 请进一步解释。这个答案似乎没有解决问题 - 它只是报告当前项目和位置。我遇到了同样的问题,使用 ListBox 并将 ItemsPanel 设置为 VirtualizingStackPanel。我看到的问题是,随着新项目被添加到 Observable 集合中,列表框项目继续移动。虽然可以在添加新项目后将项目移回它们所在的位置(例如,通过使用 SelectedIndex),但这会产生不稳定的运动。我们需要一种方法来防止滚动位置随着新项目的添加而发生变化。
    • 我有同样的问题,但您的解决方案假定选择了某些东西。但是如果用户不想选择任何东西怎么办?只需滚动某处并离开它。
    【解决方案5】:

    您是否控制添加内容的时间?我的意思是你有一个或两个数据被改变的定义点吗?如果是这样,一个简单的解决方案(可能并不优雅)就是有一个本地 int 变量来保存当前的 ListBox.SelectedIndex。在数据更改之前,将 selectedIndex 保存到该变量中,添加数据后,将 listBox 的 SelectedIndex 设置为该变量的值。

    【讨论】:

      【解决方案6】:

      我遇到了同样的问题,这是我所做的: 1.找到你的listbox的scrollviewer,当它被引导时,添加一个scrollchanged事件。

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

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

      2. 刷新后,滚动到上一个位置

        scrollViewer?.ScrollToVerticalOffset(_verticalOffset);

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-08-20
        • 1970-01-01
        • 2023-04-06
        • 1970-01-01
        • 1970-01-01
        • 2012-12-15
        • 2011-07-15
        • 1970-01-01
        相关资源
        最近更新 更多