【问题标题】:Scroll WPF ListBox to the SelectedItem set in code in a view model将 WPF ListBox 滚动到视图模型中代码中设置的 SelectedItem
【发布时间】:2012-01-11 22:16:20
【问题描述】:

我有一个带有列表框的 XAML 视图:

<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}"
                       SelectedItem="{Binding SelectedFoo, Mode=TwoWay}"
                       ScrollSelectedItem="{Binding SelectedFoo}">
    <!-- data templates, etc. -->
</control:ListBoxScroll>

在我的视图中,选定的项目绑定到一个属性。当用户在列表框中选择一个项目时,我在视图模型中的 SelectedFoo 属性得到更新。当我在视图模型中设置 SelectedFoo 属性时,会在列表框中选择正确的项目。

问题是,如果在代码中设置的 SelectedFoo 当前不可见,我需要在列表框上另外调用 ScrollIntoView。由于我的 ListBox 在视图内,而我的逻辑在我的视图模型内......我找不到方便的方法来做到这一点。于是我扩展了 ListBoxScroll:

class ListBoxScroll : ListBox
{
    public static readonly DependencyProperty ScrollSelectedItemProperty = DependencyProperty.Register(
        "ScrollSelectedItem",
        typeof(object),
        typeof(ListBoxScroll),
        new FrameworkPropertyMetadata(
            null,
            FrameworkPropertyMetadataOptions.AffectsRender, 
            new PropertyChangedCallback(onScrollSelectedChanged)));
    public object ScrollSelectedItem
    {
        get { return (object)GetValue(ScrollSelectedItemProperty); }
        set { SetValue(ScrollSelectedItemProperty, value); }
    }

    private static void onScrollSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var listbox = d as ListBoxScroll;
        listbox.ScrollIntoView(e.NewValue);
    }
}

它基本上公开了一个新的依赖属性ScrollSelectedItem,我将它绑定到我的视图模型上的SelectedFoo 属性。然后我挂钩到依赖属性的属性更改回调并将新选择的项目滚动到视图中。

还有其他人知道在由视图模型支持的 XAML 视图上调用用户控件上的函数的更简单方法吗?有点跑来跑去:

  1. 创建依赖属性
  2. 向属性更改回调添加回调
  3. 在静态回调中处理函数调用

将逻辑放在ScrollSelectedItem { set { 方法中会很好,但依赖框架似乎偷偷摸摸地在没有实际调用的情况下设法工作。

【问题讨论】:

  • 设置SelectedIndex会容易得多。
  • 这听起来像是一个视图“关注”而不是视图模型。我不得不做类似的事情,但我把代码留在了视图中。见matthamilton.net/focus-a-virtualized-listboxitem
  • @MattHamilton - 此代码在技术上位于视图中(在控件内)。你会在 View(任何地方)中编写什么代码来完成调用 ScrollIntoView?请记住,我不能覆盖 SelectedItem 上的集合,因为它不是虚拟的。
  • @anatoliiG - SelectedIndex 是否会导致视图滚动到所选项目?
  • @JamesFassett 对于DataGrid - 是的。不幸的是,我还没有尝试过 ListBox

标签: c# wpf mvvm


【解决方案1】:

您是否尝试过使用 Behavior... 这是一个 ScrollInViewBehavior。我已经将它用于 ListView 和 DataGrid ......我认为它应该适用于 ListBox......

您必须添加对System.Windows.Interactivity 的引用才能使用Behavior&lt;T&gt; class

行为

public class ScrollIntoViewForListBox : Behavior<ListBox>
{
    /// <summary>
    ///  When Beahvior is attached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
    }

    /// <summary>
    /// On Selection Changed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void AssociatedObject_SelectionChanged(object sender,
                                           SelectionChangedEventArgs e)
    {
        if (sender is ListBox)
        {
            ListBox listBox = (sender as ListBox);
            if (listBox .SelectedItem != null)
            {
                listBox.Dispatcher.BeginInvoke(
                    (Action) (() =>
                                  {
                                      listBox.UpdateLayout();
                                      if (listBox.SelectedItem !=
                                          null)
                                          listBox.ScrollIntoView(
                                              listBox.SelectedItem);
                                  }));
            }
        }
    }
    /// <summary>
    /// When behavior is detached
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.AssociatedObject.SelectionChanged -=
            AssociatedObject_SelectionChanged;

    }
}

用法

XAML 的别名添加为xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

然后在你的Control

        <ListBox ItemsSource="{Binding Path=MyList}"
                  SelectedItem="{Binding Path=MyItem,
                                         Mode=TwoWay}"
                  SelectionMode="Single">
            <i:Interaction.Behaviors>
                <Behaviors:ScrollIntoViewForListBox />
            </i:Interaction.Behaviors>
        </ListBox>

现在,当ViewModel 中设置了“MyItem”属性时,列表将在重新反映更改时滚动。

【讨论】:

  • 这其实很酷。它类似于 crazyarabian 描述的附加属性方法,但感觉更干净。使用他的附加属性解决方案和您的行为解决方案,我将不得不编写一个新课程。扩展 ListBox 控件并在子类中进行事件处理似乎更有意义。我今天要试试,看看它是否和你的行为解决方案一样干净。
  • @JamesFassett Behavoir 通常用于让您控制它当前没有的功能.... IE....以避免扩展控件...。但我相信扩展是这么好..
  • 只使用 Express 版本。这个解决方案需要 Blend 吗?
  • @paul 它需要一个来自 blend Sdk System.Windows.Interactivity 的 dll,而且(我不确定,但是)在 express 版本中使用 dll 并没有限制。
  • 谢谢,这对我有用。但是,我必须在 ListBox 上设置 VirtualizingStackPanel.IsVirtualizing = false 才能使其始终如一地工作。
【解决方案2】:

查看答案后,出现了一个共同主题:外部类监听 ListBox 的 SelectionChanged 事件。这让我意识到依赖属性的方法是多余的,我可以让子类自己听:

class ListBoxScroll : ListBox
{
    public ListBoxScroll() : base()
    {
        SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged);
    }

    void ListBoxScroll_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ScrollIntoView(SelectedItem);
    }
}

我觉得这是做我想做的最简单的解决方案。

adcool2007 因提出行为而获得荣誉奖。这里有几篇文章供感兴趣的人参考:

http://blogs.msdn.com/b/johngossman/archive/2008/05/07/the-attached-behavior-pattern.aspx
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

我认为对于将添加到多个不同用户控件的通用行为(例如单击行为、拖动行为、动画行为等),附加行为很有意义。我不想在这种特殊情况下使用它们的原因是行为的实现(调用ScrollIntoView)不是一个通用操作,它不会发生在除 ListBox 之外的任何控件上。

【讨论】:

  • 简单有效,可能会因为技术不深入而受到较少关注,但得到了我的投票:)
  • 别忘了取消订阅活动:P
  • @adminSoftDK 无需取消订阅该活动。一个监听自己事件的对象可以被 GC 处理得很好。
  • 最简单的解决方案 - 谢谢。我只是在 vb.net Private Sub anyListBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles anyListBox.SelectionChanged sender.ScrollIntoView(sender.SelectedItem) End Sub 的代码中处理事件
【解决方案3】:

因为这严格来说是一个视图问题,所以没有理由不能为此目的在视图后面的代码中使用事件处理程序。收听ListBox.SelectionChanged 并使用它将新选择的项目滚动到视图中。

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ((ListBox)sender).ScrollIntoView(e.AddedItems[0]);
}

您也不需要派生的ListBox 来执行此操作。只需使用标准控件,当 ListBox.SelectedItem 值发生变化时(如您原来的问题中所述),上述处理程序将被执行并且项目将滚动到视图中。

    <ListBox
        ItemsSource="{Binding Path=FooCollection}"
        SelectedItem="{Binding Path=SelectedFoo}"
        SelectionChanged="ListBox_SelectionChanged"
        />

另一种方法是编写一个附加属性来侦听ICollectionView.CurrentChanged,然后为新的当前项目调用ListBox.ScrollIntoView。如果您需要多个列表框的此功能,这是一种更“可重用”的方法。您可以在这里找到一个很好的示例来帮助您入门:http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/

【讨论】:

  • 我不介意监听一个事件,但是“滚动到所选项目”的活动真的感觉像是控件的一部分,而不是周围的视图。但是,我可以在我的 ListBoxScroll 子类中编写事件侦听器 - 这将使我远离依赖属性回调,并将其转换为更清晰的事件委托。
  • 我喜欢这种方法,但是如果你有多个 ListBox 会有点麻烦,我可能会采用这个功能并派生出我自己的 ListBox。我同意这严格来说是一个视图问题,并且代码可以在视图中...
  • @AdriaanDavel 只是将所有 ListBox SelectionChanged 绑定到同一个处理程序,或者您可能已经这样做了,只是在谈论所有事件处理程序订阅语句很麻烦?
【解决方案4】:

我知道这是一个老问题,但我最近对同样问题的搜索把我带到了这个问题上。我想使用行为方法,但不想依赖 Blend SDK 只是为了给我Behavior&lt;T&gt; 所以这是我没有它的解决方案:

public static class ListBoxBehavior
{
    public static bool GetScrollSelectedIntoView(ListBox listBox)
    {
        return (bool)listBox.GetValue(ScrollSelectedIntoViewProperty);
    }

    public static void SetScrollSelectedIntoView(ListBox listBox, bool value)
    {
        listBox.SetValue(ScrollSelectedIntoViewProperty, value);
    }

    public static readonly DependencyProperty ScrollSelectedIntoViewProperty =
        DependencyProperty.RegisterAttached("ScrollSelectedIntoView", typeof (bool), typeof (ListBoxBehavior),
                                            new UIPropertyMetadata(false, OnScrollSelectedIntoViewChanged));

    private static void OnScrollSelectedIntoViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var selector = d as Selector;
        if (selector == null) return;

        if (e.NewValue is bool == false)
            return;

        if ((bool) e.NewValue)
        {
            selector.AddHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
        else
        {
            selector.RemoveHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
    }

    private static void ListBoxSelectionChangedHandler(object sender, RoutedEventArgs e)
    {
        if (!(sender is ListBox)) return;

        var listBox = (sender as ListBox);
        if (listBox.SelectedItem != null)
        {
            listBox.Dispatcher.BeginInvoke(
                (Action)(() =>
                    {
                        listBox.UpdateLayout();
                        if (listBox.SelectedItem !=null)
                            listBox.ScrollIntoView(listBox.SelectedItem);
                    }));
        }
    }
}

然后用法就是

<ListBox ItemsSource="{Binding Path=MyList}"
         SelectedItem="{Binding Path=MyItem, Mode=TwoWay}"
         SelectionMode="Single" 
         behaviors:ListBoxBehavior.ScrollSelectedIntoView="True">

【讨论】:

  • 效果很好,我也不想拖入那种依赖。
  • 您不应该删除if (e.NewValue is bool == false) 代码吗?如果e.NewValue 变为 false,处理程序应该被删除吗?
  • @Ziriax 这是一个测试 e.NewValue 的类型是布尔型的,而不是它是假的。
  • 哎呀,我完全看错了,傻我!我注意到的另一件事:很可能SelectedItem 保持不变,但SelectedIndex 发生了变化。例如,当使用ObservableCollection&lt;T&gt;.Move 方法时,SelectedItem 不会改变,SelectionChangedEvent 不会被触发。因此该项目不会被移入视图。那么需要添加一些额外的逻辑来处理这种情况吗?
  • @Dutts 默认情况下,本地命名空间是本地 [xmlns:local="clr-namespace:",那么 "behavior:ListBoxBehavior..." 将是 "local:ListBoxBehavior. ScrollSelectedIntoView="true" ", 安东尼奥
【解决方案5】:

我正在使用这个(在我看来)清晰而简单的解决方案

listView.SelectionChanged += (s, e) => 
    listView.ScrollIntoView(listView.SelectedItem);

listView 是 xaml 中 ListView 控件的名称,SelectedItem 受我的 MVVM 影响,代码插入到构造函数中在 xaml.cs 文件中。

【讨论】:

    【解决方案6】:

    试试这个:

    private void lstBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        lstBox.ScrollIntoView(lstBox.SelectedItem);
    }
    

    【讨论】:

    • 这是一个解决方案,但我认为这个问题需要一个与 MVVM 模式兼容的解决方案。
    • @user672951 这与此问题中的任何其他解决方案一样符合 MVVM 模式。
    【解决方案7】:

    综合各种方法后发现以下是最简单最好的

    lstbox.Items.MoveCurrentToLast();
    lstbox.ScrollIntoView(lstbox.Items.CurrentItem);
    

    【讨论】:

      【解决方案8】:

      我接受了 Ankesh 的回答,并使其不依赖于 blend sdk。我的解决方案的缺点是它将适用于您应用程序中的所有列表框。但好处是不需要自定义类。

      当您的应用正在初始化时...

          internal static void RegisterFrameworkExtensionEvents()
          {
              EventManager.RegisterClassHandler(typeof(ListBox), ListBox.SelectionChangedEvent, new RoutedEventHandler(ScrollToSelectedItem));
          }
      
          //avoid "async void" unless used in event handlers (or logical equivalent)
          private static async void ScrollToSelectedItem(object sender, RoutedEventArgs e)
          {
              if (sender is ListBox)
              {
                  var lb = sender as ListBox;
                  if (lb.SelectedItem != null)
                  {
                      await lb.Dispatcher.BeginInvoke((Action)delegate
                      {
                          lb.UpdateLayout();
                          if (lb.SelectedItem != null)
                              lb.ScrollIntoView(lb.SelectedItem);
                      });
                  }
              }
          }
      

      这会使您的所有列表框滚动到选定状态(我喜欢将其作为默认行为)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-07-28
        • 2011-01-02
        • 2011-02-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多