在视图模型中设置选定项并通过代码滚动WPF ListBox

44

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

<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}"
                       SelectedItem="{Binding SelectedFoo, Mode=TwoWay}"
                       ScrollSelectedItem="{Binding SelectedFoo}">
    <!-- data templates, etc. -->
</control:ListBoxScroll>
选定的项与视图中的一个属性绑定。当用户在列表框中选择项目时,我的视图模型中的SelectedFoo属性将得到更新。当我在视图模型中设置SelectedFoo属性时,列表框中将选择正确的项目。
问题是,如果在代码中设置的SelectedFoo当前不可见,我需要另外调用ListBox的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会更容易。 - Anatolii Gabuza
1
这似乎是一个视图的“关注点”,而不是ViewModel的。我曾经做过类似的事情,但我将代码留在了视图中。请参见http://matthamilton.net/focus-a-virtualized-listboxitem。 - Matt Hamilton
@MattHamilton - 这段代码在技术上是在视图中(控件内部)。您会在任何视图中编写什么代码来调用ScrollIntoView?请记住,由于它不是虚拟的,因此我无法覆盖SelectedItem上的set。 - James Fassett
@anatoliiG - SelectedIndex会导致视图滚动到所选项目吗? - James Fassett
@JamesFassett 对于 DataGrid - 是的。不幸的是,我还没有尝试过 ListBox - Anatolii Gabuza
@anatoliiG - 我刚刚测试了一下,SelectedIndex不会滚动ListBox。 - James Fassett
8个回答

58

你尝试使用行为了吗?这里有一个ScrollInViewBehavior。我在ListView和DataGrid中使用过它,我认为它应该适用于ListBox。

你必须添加对System.Windows.Interactivity 的引用才能使用Behavior<T>类

Behavior

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"

然后在您的控件中使用。

        <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控件并处理事件似乎更有意义。我今天会尝试一下,并查看它是否像您的行为解决方案一样干净。 - James Fassett
仅使用Express版本。这个解决方案需要Blend吗? - paul
不错!你在BeginInvoke委托中对listBox.SelectedItem进行第二次空值检查的原因是什么?我猜想由于你异步启动了滚动,所以SelectedItem有可能会发生变化? - C. Tewalt
ListBox listBox = (sender as ListBox); 更好的直接转换方式是:ListBox listBox = (ListBox)sender; 因为你已经在前一行使用了 'is' 进行了检查。 - juFo
3
谢谢,这个对我有用。不过,我得在我的“ListBox”上设置 VirtualizingStackPanel.IsVirtualizing = false 才能让它始终正常工作。 - Mash
显示剩余3条评论

43
在审查了答案后,出现了一个共同的主题:外部类监听ListBox的SelectionChanged事件。这让我意识到,依赖属性方法过于复杂了,我只需让子类监听自身即可。
class ListBoxScroll : ListBox
{
    public ListBoxScroll() : base()
    {
        SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged);
    }

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

我觉得这是实现我想要的最简单的解决方案。

感谢adcool2007提出行为(Behaviours)的方法。以下是一些对此感兴趣的文章:

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之外的任何控件都可以执行的通用操作。


简单而有效,可能因不够深入技术而得到较少关注,但它获得了我的支持 :) - Adriaan Davel
3
别忘记取消订阅该活动 :P - adminSoftDK
1
@adminSoftDK 没有必要取消订阅事件。一个监听自身事件的对象可以很好地进行垃圾回收。 - Sebastian Negraszus
最简单的解决方案 - 谢谢。我只需在vb.net的代码后台处理事件 Private Sub anyListBox_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles anyListBox.SelectionChanged sender.ScrollIntoView(sender.SelectedItem) End Sub - DrMarbuse

24

因为这严格是一个视图问题,所以你完全可以在你的视图的后台代码中有一个事件处理程序来实现这个目的。监听 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子类中编写事件侦听器,这将使我远离依赖属性回调并进入更清洁的事件委托。 - James Fassett
我喜欢这种方法,但如果你有多个ListBox,它会有点繁琐,我可能会采用这个功能并派生出自己的ListBox。我同意这严格是一个视图问题,代码可以放在视图中... - Adriaan Davel
1
@AdriaanDavel 只需将所有 ListBox 的 SelectionChanged 绑定到同一个处理程序,或者您可能已经这样做了,并且只是在谈论所有事件处理程序订阅语句很繁琐? - Zack

14

我正在使用这个(在我看来)清晰易懂的解决方案

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

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


13

我知道这是一个老问题,但我最近搜索同样的问题时找到了它。我想使用行为方法,但不想依赖Blend SDK来提供Behavior<T>,所以这里是我没有使用SDK的解决方案:

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">

非常好用,我更喜欢不引入那个依赖项。 - angularsen
你不应该删除 if (e.NewValue is bool == false) 代码吗?如果 e.NewValue 变成 false,处理程序不应该被移除吗? - Ziriax
@Ziriax 这是一项测试,e.NewValue 的类型是布尔型,而不是假的。 - Dutts
哎呀,我完全读错了,真傻!我还注意到另一件事:SelectedItem 可能保持不变,但 SelectedIndex 会改变。例如,在使用 ObservableCollection<T>.Move 方法时,SelectedItem 不会改变,SelectionChangedEvent 也不会触发。因此,该项将不会移动到视图中。因此,需要添加一些额外的逻辑来处理这种情况? - Ziriax
@Dutts 默认情况下,本地命名空间是 local [xmlns:local="clr-namespace:<yourAssemblyName>",然后 "behavior:ListBoxBehavior..." 将变为 "local:ListBoxBehavior.ScrollSelectedIntoView="true" "。Antonio - antonio

9

试试这个:

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

那是一个解决方案,但我认为问题要求与MVVM模式兼容的解决方案。 - user672951
2
@user672951,这个解决方案与此问题中的任何其他解决方案一样适用于MVVM模式。 - Zack

3
在尝试了各种方法后,我发现以下方法是最简单且最好的。
lstbox.Items.MoveCurrentToLast();
lstbox.ScrollIntoView(lstbox.Items.CurrentItem);

2

我参考了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);
                });
            }
        }
    }

这将使你所有的列表框滚动到所选项(我认为这是默认行为)。


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