【问题标题】:mvvm how to make a list view auto scroll to a new Item in a list viewmvvm 如何使列表视图自动滚动到列表视图中的新项目
【发布时间】:2010-07-23 10:10:21
【问题描述】:

我正在使用 MVVM 模式,我有一个创建新 ViewModel 的视图,在用户单击保存后,此视图关闭并打开一个单独的视图,该视图在 ListView 中显示视图模型的集合.

ListView 按字母顺序排序,因此新的ViewModel 可能出现在ListBox 的底部,用户不会立即看到。

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

我猜它会使用附加行为,以及ListView 上的ScrollIntoView 事件,但是我不确定我需要从GridView 捕获哪个事件..

干杯

【问题讨论】:

    标签: c# wpf listview mvvm scroll


    【解决方案1】:

    此解决方案适用于 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}"/>
    

    【讨论】:

    • 布伦特感谢这个,这正是我正在寻找的,谢谢!唯一的事情是我在我的 Listview 上禁用了 SelectedItem,这是因为我在每一行上都有一个“编辑”链接,是否有另一个事件可以将此依赖属性绑定到?谢谢
    • 我修改了您的课程以使用 ListView,但它似乎对我不起作用。我的 ListView 的 SelectionMode 是 Multiple,我的 ScrollViewer 的 CanContentScroll 属性是 true,它的 VerticalScrollBarVisibility 是 Auto。此外,ListViewItem 的 IsSelected 属性绑定 。当用户手动选择一个项目时,OnListViewItemSelected 函数触发并且屏幕滚动。但是,当我以编程方式更改我的 VM 项的 Selected 属性时,永远不会触发函数 OnListViewItemSelected。
    • 我还应该注意,当以编程方式选择项目时,ListView 不可见。我怀疑这会干扰 scolling,但不会干扰 OnListViewItemChanged 函数的触发。
    【解决方案2】:

    使用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 设计器中可用。
    覆盖依赖属性是一个相当大的话题,但对于创建自定义控件是不可或缺的。您可以通过Control Authoring OverviewDependency Properties Overview 了解更多信息。

    目标是订阅底层项目集合的集合更改事件,以便我们可以在添加新项目时通过滚动到底部来响应。我们必须在两个地方订阅这个事件。

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

    为什么要创建自定义控件

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

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

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

    缺点

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

    享受吧! :-)

    【讨论】:

      【解决方案3】:

      将选定项 DependecyProperty 添加到包含该集合的类。将列表视图的 SelectedItem 绑定到它。将新模型添加到集合后,设置选定项 DependencyProperty。

      【讨论】:

      • 嗨 Sascha,感谢您的回复,这很有道理,唯一的原因是我关闭了 Listview 上的可聚焦属性,这是因为我只想突出显示他们单击的项目gridview上的“编辑超链接”,是否可以使选定的列表项高亮透明?
      • 您可以通过为项目创建样式或模板来使选定的列表项透明。
      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        嗯,关于矫枉过正,为了一种更简单的方法,我想大多数人都会使用....

        对于列表视图,只需敲入:

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

        而对于 Listbox,只需敲入:

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

        向您的列表视图项添加 (..etc) 方法... .. 或者在计时器滴答声中敲击它。

        OP下的上述方式对我来说似乎很重要,我很懒... 所有代码都解释了它自己。

        【讨论】:

        • mmm... 也许,但问题是如何使用 MVVM 设计模式来做到这一点。对于那些不使用设计模式的人来说,这种方法可能会奏效。
        • 对于WPF版本的ListView,EnsureVisible方法不存在。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-14
        • 1970-01-01
        • 2019-10-05
        • 2017-09-23
        相关资源
        最近更新 更多