Search code examples
c#.netxamluwpwindows-community-toolkit

Scroll to new item in ListView for UWP


I'm creating a chat application with a ListView that contains the messages. When a new message is sent/received, the ListView should scroll to the new message.

I'm using MVVM, so the ListView looks like

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

How can I do it?

EDIT: I tried to make this work in versions prior to the Anniversary Update creating a Behavior. This is what I have so far:

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);
        }
    }
}

The code uses VisualTreeExtensions from the UWP Community Toolkit

However, the position after the call to TransformPoint always returns {0, 0}

What am I doing wrong?


Solution

  • As of Windows 10, version 1607 you can use ItemsStackPanel.ItemsUpdatingScrollMode with the value KeepLastItemInView, which seems like the most natural fit for the job.

    There is an "Inverted Lists" example in MS UWP docs (2017-2-8) that would boil down to this XAML:

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

    On a side note, yes, I'd agree that you may want to get rid of a ScrollViewer as it's redundant as a ListView wrapper.

    Upd:

    KeepLastItemInView is not available for applications that target Windows 10 prior to the "Anniversary Edition". If that's the case, one way to make sure that a list always displays the last item after item collection is changed is to override OnItemsChanged and call ScrollIntoView. A basic implementation would look like this:

    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());
        }
    }