滚动到 UWP 的 ListView 中的新项目

Scroll to new item in ListView for UWP

我正在使用包含消息的 ListView 创建聊天应用程序。当新消息 sent/received 时,ListView 应滚动到新消息。

我正在使用 MVVM,所以 ListView 看起来像

<ScrollViewer>
    <ItemsControl Source="{Binding Messages}" />
</ScrollViewer>

我该怎么做?

编辑:我试图在创建行为的周年纪念更新之前的版本中完成这项工作。这是我目前所拥有的:

public class FocusLastBehavior : Behavior<ItemsControl>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Items.VectorChanged += ItemsOnVectorChanged;
    }

    private void ItemsOnVectorChanged(IObservableVector<object> sender, IVectorChangedEventArgs @event)
    {
        var scroll = VisualTreeExtensions.FindVisualAscendant<ScrollViewer>(AssociatedObject);
        if (scroll == null)
        {
            return;
        }

        var last = AssociatedObject.Items.LastOrDefault();

        if (last == null)
        {
            return;
        }

        var container = AssociatedObject.ContainerFromItem(last);


        ScrollToElement(scroll, (UIElement)container);
    }

    private static void ScrollToElement(ScrollViewer scrollViewer, UIElement element,
        bool isVerticalScrolling = true, bool smoothScrolling = true, float? zoomFactor = null)
    {
        var transform = element.TransformToVisual((UIElement)scrollViewer.Content);
        var position = transform.TransformPoint(new Point(0, 0));

        if (isVerticalScrolling)
        {
            scrollViewer.ChangeView(null, position.Y, zoomFactor, !smoothScrolling);
        }
        else
        {
            scrollViewer.ChangeView(position.X, null, zoomFactor, !smoothScrolling);
        }
    }
}

代码使用 UWP Community Toolkit

中的 VisualTreeExtensions

但是调用TransformPoint后的位置总是returns{0, 0}

我做错了什么?

这里vm.newmessageItem是新消息。我用它来获取你想要滚动的 listviewitem。

   if (listview != null)
    {
    listview.UpdateLayout();
    listview.ScrollIntoView(vm.newmessageItem);
     var listViewItem = (FrameworkElement)listview.ContainerFromItem(vm.newmessageItem);
        while (listViewItem == null)
        {
        await Task.Delay(1);
        listViewItem = (FrameworkElement)listview.ContainerFromItem(vm.newmessageItem);
        }
        ScrollViewer scroll = Utility.FindFirstElementInVisualTree<ScrollViewer>(listview);

        var topLeft =
        listViewItem .TransformToVisual(listview)                                         .TransformPoint(new Point()).Y;
         var lvih = listViewItem.ActualHeight;
        var lvh = listview.ActualHeight;
         var desiredTopLeft = (lvh - lvih) ;

        var currentOffset = scroll.VerticalOffset;
         var desiredOffset = currentOffset + desiredTopLeft;
        listview.UpdateLayout();
        scroll.ChangeView(null, desiredOffset,null);
        scroll.UpdateLayout();
    }

从 Windows 10 版本 1607 开始,您可以使用 ItemsStackPanel.ItemsUpdatingScrollMode 和值 KeepLastItemInView,这似乎最适合这项工作。

MS UWP 文档 (2017-2-8) 中有一个 "Inverted Lists" 示例可以归结为 XAML:

<ListView Source="{Binding Messages}">
   <ListView.ItemsPanel>
       <ItemsPanelTemplate>
           <ItemsStackPanel
               VerticalAlignment="Bottom"
               ItemsUpdatingScrollMode="KeepLastItemInView"
           />
       </ItemsPanelTemplate>
   </ListView.ItemsPanel>
</ListView>

附带说明,是的,我同意您可能想要摆脱 ScrollViewer,因为它作为 ListView 包装器是多余的。

更新:

KeepLastItemInView 不适用于在 "Anniversary Edition" 之前以 Windows 10 为目标的应用程序。如果是这种情况,确保列表在项目集合更改后始终显示最后一项的一种方法是覆盖 OnItemsChanged and call ScrollIntoView。基本实现如下所示:

using System.Linq;
using Windows.UI.Xaml.Controls;

public class ChatListView : ListView
{
    protected override void OnItemsChanged(object e)
    {
        base.OnItemsChanged(e);
        if(Items.Count > 0) ScrollIntoView(Items.Last());
    }
}

这一行代码将自动转到您请求的列表中的项目。例如,如果您有一个聊天客户端并且您的 ListView 会填充每个条目。这将在列表底部显示最新条目。因此,用户不必每次都滚动到底部。这里设置为转到最后一项。在 UWP 应用程序中工作。

MyListView?.ScrollIntoView(MyListView.Items[allMyItemsInList.Count - 1], ScrollIntoViewAlignment.Leading);