MVVM如何使列表视图自动滚动到列表视图中的新项目

17

我正在使用MVVM模式,我有一个视图用于创建一个新的ViewModel,当用户点击保存后,该视图将关闭并打开一个单独的视图,在其中以ListView中显示一组视图模型。

这个ListView按字母顺序排序,因此新的ViewModel可能出现在ListBox的底部,这对用户来说不是立即可见的。

我的问题是如何使视图自动滚动到新添加的项目?

我想这将使用关联行为,并使用ListView上的ScrollIntoView事件,但我不确定我需要从GridView捕获哪个事件。

谢谢

5个回答

15

这个解决方案是用于ListBox的,但是它可以修改为ListView... 当你从ViewModel更改所选项目时,它将滚动所选项目到视图中。

类:

/// <summary>
/// ListBoxItem Behavior class
/// </summary>
public static class ListBoxItemBehavior
{
    #region IsBroughtIntoViewWhenSelected

    /// <summary>
    /// Gets the IsBroughtIntoViewWhenSelected value
    /// </summary>
    /// <param name="listBoxItem"></param>
    /// <returns></returns>
    public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem)
    {
        return (bool)listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
    }

    /// <summary>
    /// Sets the IsBroughtIntoViewWhenSelected value
    /// </summary>
    /// <param name="listBoxItem"></param>
    /// <param name="value"></param>
    public static void SetIsBroughtIntoViewWhenSelected(
      ListBoxItem listBoxItem, bool value)
    {
        listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
    }

    /// <summary>
    /// Determins if the ListBoxItem is bought into view when enabled
    /// </summary>
    public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
        DependencyProperty.RegisterAttached(
        "IsBroughtIntoViewWhenSelected",
        typeof(bool),
        typeof(ListBoxItemBehavior),
        new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));

    /// <summary>
    /// Action to take when item is brought into view
    /// </summary>
    /// <param name="depObj"></param>
    /// <param name="e"></param>
    static void OnIsBroughtIntoViewWhenSelectedChanged(
      DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        ListBoxItem item = depObj as ListBoxItem;
        if (item == null)
            return;

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

        if ((bool)e.NewValue)
            item.Selected += OnListBoxItemSelected;
        else
            item.Selected -= OnListBoxItemSelected;
    }

    static void OnListBoxItemSelected(object sender, RoutedEventArgs e)
    {
        // Only react to the Selected event raised by the ListBoxItem 
        // whose IsSelected property was modified.  Ignore all ancestors 
        // who are merely reporting that a descendant's Selected fired. 
        if (!Object.ReferenceEquals(sender, e.OriginalSource))
            return;

        ListBoxItem item = e.OriginalSource as ListBoxItem;
        if (item != null)
            item.BringIntoView();
    }

    #endregion // IsBroughtIntoViewWhenSelected
}

将xmlns添加到您的视图中:

xmlns:util="clr-namespace:YourNamespaceHere.Classes"
将样式添加到 Window/UserControl 的资源中:
<Window.Resources>
    <Style x:Key="ListBoxItemContainerStyle" TargetType="{x:Type ListBoxItem}"
        BasedOn="{StaticResource {x:Type ListBoxItem}}">
        <Setter Property="util:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected" Value="true"/>
    </Style>
</Window.Resources>

实现列表框:

<ListBox ItemsSource="{Binding MyView}"
         DisplayMemberPath="Title"
         SelectedItem="{Binding SelectedItem}" 
         ItemContainerStyle="{StaticResource ListBoxItemContainerStyle}"/>

Brent,谢谢你提供的内容,这正是我所需要的,非常感谢!唯一的问题是我已经禁用了我的Listview上的SelectedItem,因为每行都有一个“编辑”链接。是否有另一个事件可以将此依赖属性绑定到?谢谢。 - jpgooner
我修改了你的类以适应ListView,但似乎对我无效。我的ListView的SelectionMode是Multiple,我的ScrollViewer的CanContentScroll属性为true,VerticalScrollBarVisibility为Auto。此外,ListViewItem的IsSelected属性绑定为<Setter Property="IsSelected" Value="{Binding Selected}"/>。当用户手动选择项目时,OnListViewItemSelected函数会触发并滚动屏幕。然而,当我以编程方式更改VM项的Selected属性时,函数OnListViewItemSelected从未触发。 - River-Claire Williamson
我还应该指出的是,当程序选择项目时,ListView 是不可见的。我怀疑这会干扰滚动,但不会影响 OnListViewItemChanged 函数的触发。 - River-Claire Williamson
无法使其工作。不在视图中的项目不会响应选择。 - Jeroen van Langen

3

使用 ListBox 的另一种解决方案。为了实现自动滚动,您可以创建一个自定义控件!


C#

public class LoggingListBox : ListBox
{
    ///<summary>
    ///Define the AutoScroll property. If enabled, causes the ListBox to scroll to 
    ///the last item whenever a new item is added.
    ///</summary>
    public static readonly DependencyProperty AutoScrollProperty = 
        DependencyProperty.Register(
            "AutoScroll", 
            typeof(Boolean), 
            typeof(LoggingListBox), 
            new FrameworkPropertyMetadata(
                true, //Default value.
                FrameworkPropertyMetadataOptions.AffectsArrange | 
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
                AutoScroll_PropertyChanged));

    /// <summary>
    /// Gets or sets whether or not the list should scroll to the last item 
    /// when a new item is added.
    /// </summary>
    [Category("Common")] //Indicate where the property is located in VS designer.
    public bool AutoScroll
    {
        get { return (bool)GetValue(AutoScrollProperty); }
        set { SetValue(AutoScrollProperty, value); }
    }

    /// <summary>
    /// Event handler for when the AutoScroll property is changed.
    /// This delegates the call to SubscribeToAutoScroll_ItemsCollectionChanged().
    /// </summary>
    /// <param name="d">The DependencyObject whose property was changed.</param>
    /// <param name="e">Change event args.</param>
    private static void AutoScroll_PropertyChanged(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        SubscribeToAutoScroll_ItemsCollectionChanged(
            (LoggingListBox)d,
            (bool)e.NewValue);
    }

    /// <summary>
    /// Subscribes to the list items' collection changed event if AutoScroll is enabled.
    /// Otherwise, it unsubscribes from that event.
    /// For this to work, the underlying list must implement INotifyCollectionChanged.
    ///
    /// (This function was only creative for brevity)
    /// </summary>
    /// <param name="listBox">The list box containing the items collection.</param>
    /// <param name="subscribe">Subscribe to the collection changed event?</param>
    private static void SubscribeToAutoScroll_ItemsCollectionChanged(
        LoggingListBox listBox, bool subscribe)
    {
        INotifyCollectionChanged notifyCollection =
            listBox.Items.SourceCollection as INotifyCollectionChanged;
        if (notifyCollection != null)
        {
            if (subscribe)
            {
                //AutoScroll is turned on, subscribe to collection changed events.
                notifyCollection.CollectionChanged += 
                    listBox.AutoScroll_ItemsCollectionChanged;
            }
            else
            {
                //AutoScroll is turned off, unsubscribe from collection changed events.
                notifyCollection.CollectionChanged -= 
                    listBox.AutoScroll_ItemsCollectionChanged;
            }
        }
    }

    /// <summary>
    /// Event handler called only when the ItemCollection changes
    /// and if AutoScroll is enabled.
    /// </summary>
    /// <param name="sender">The ItemCollection.</param>
    /// <param name="e">Change event args.</param>
    private void AutoScroll_ItemsCollectionChanged(
        object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            int count = Items.Count;
            ScrollIntoView(Items[count - 1]);
        }
    }

    /// <summary>
    /// Constructor a new LoggingListBox.
    /// </summary>
    public LoggingListBox()
    {
        //Subscribe to the AutoScroll property's items collection 
        //changed handler by default if AutoScroll is enabled by default.
        SubscribeToAutoScroll_ItemsCollectionChanged(
            this, (bool)AutoScrollProperty.DefaultMetadata.DefaultValue);
    }
}

XAML

以下是在XAML中使用该控件的方法:

<tools:LoggingListBox/> <!-- AutoScroll="true" by default. -->

有时候你需要指定如何访问这个控件。这完全取决于你的项目设置。

xmlns:tools="clr-namespace:MyCustomControls;assembly=MyCustomControls"

如何工作

创建自定义控件只需要使用C#代码即可。我们通过扩展ListBox并添加一个属性AutoScroll来实现。由于它是一个依赖属性,因此它将参与WPF绑定系统,并在Visual Studio设计器中可用。
涵盖依赖属性是一个相当大的主题,但是创建自定义控件至关重要。您可以在控件作者概述依赖属性概述中了解更多信息。

我们的目标是订阅基础项集合的集合更改事件,以便在添加新项时滚动到底部进行响应。我们必须在两个地方订阅此事件。

  1. 每当将AutoScroll设置为true时,我们都需要订阅。 AutoScroll的值可能随时更改,我们应该能够做出相应的响应。 如果设置为false,则应指示控件停止向底部滚动,方法是取消订阅。
  2. 假设AutoScroll仅需要在编译时设置,我们需要一种在启动时订阅的方法。 这是通过使用控件的构造函数完成的。

为什么创建自定义控件

首先,我们已经尽可能简化了XAML。 我们只需要访问控件并可选地指定或绑定到AutoScroll属性。

它符合MVVM。 我们的视图模型不需要担心AutoScroll功能,因为它包含在控件中。 同时,视图模型可以提供一个属性,该属性绑定到AutoScroll属性,从而实现视图和视图模型的所需解耦。

此外,我们避免了使用行为。 这意味着我们从项目中删除了两个依赖项(尽管这是这些依赖项最初被包含的唯一原因)。 我们可以安全地省略System.Windows.InteractivityMicrosoft.Expressions.Interactions的项目引用。

缺点

这种方法只有一个缺点。 基础项集合必须实现INotifyCollectionChanged。 在大多数情况下,这不是问题。 如果您正在使用MVVM,则可能已经将项包装在ObservableCollection中,该集合已经实现了我们所需的接口。

享受! :-)


3

将选定的项DependecyProperty添加到包含集合的类中。将listview的SelectedItem绑定到它上面。在将新模型添加到集合后,设置选定的项DependencyProperty。


嗨Sascha,谢谢你的回复,这很有道理。唯一的问题是我已经关闭了Listview上的可聚焦属性,因为我只希望在网格视图上单击“编辑超链接”后高亮显示项目。是否可能使所选列表项透明高亮显示? - jpgooner
你可以创建一个项目的样式或模板,使所选列表项透明。 - Sascha

-2
谈论过度了,对于更简单的方法和我想象中大多数人会使用的方法...

对于列表视图,只需插入以下内容:

listView1.EnsureVisible(listView1.Items.Count - 1);

对于列表框,只需输入以下代码:

listBox1.SelectedIndex = listBox1.Items.Count - 1; 
listBox1.SelectedIndex = -1;

在你的列表视图项中添加(..etc)方法... ...或者将它放在计时器上。

以上方式在OP下似乎对我来说太麻烦了,我很懒... 所有的代码都可以自己解释。


也许吧,但问题是如何使用MVVM设计模式来实现。对于不使用设计模式的人来说,这种方法可能有效。 - Brent
1
对于 WPF 版本的 ListView,不存在 EnsureVisible 方法。 - Matt Davis

-2

这可能不适用于 WPF,但在 WinForms 中,代码类似于 lstData.EnsureVisible(itemIndex);


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