4

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?

iehrlich
  • 3,572
  • 4
  • 34
  • 43
SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • Do you get the listviewitem to which you want to scroll? – Archana May 04 '17 at 11:25
  • Don't wrap `ListView` in a `ScrollViewer`, it is scrollable automatically. To access its internal `ScrollViewer` (e.g., to invoke its `ChangeView` method for scrolling to a given pixel offset), use `ListView.GetScrollViewer()`. – andreask May 04 '17 at 11:36
  • Did you check [this](http://stackoverflow.com/questions/16866309/listbox-scroll-into-view-with-mvvm)? – AVK May 04 '17 at 19:49

3 Answers3

7

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());
    }
}
DK.
  • 3,173
  • 24
  • 33
  • This is the ideal solution, but unfortunately my project must target build 10586. Is it feasible to implement ItemsStackPanel? what options are there in my case? – SuperJMN May 05 '17 at 08:37
  • 1
    Unfortunatly `Items​Updating​Scroll​Mode` enum doesn't have `KeepLastItemInView` in the builds 10586 and below, so you'll have to call `ListView.ScrollIntoView` yourself. There are multiple options where to place this call. I'd probably choose to extend `ListView` and call it in the `OnItemsChanged` override, similar to a WPF solution discussed in [this question](http://stackoverflow.com/questions/16866309/listbox-scroll-into-view-with-mvvm). – DK. May 05 '17 at 09:58
  • Nice! BTW, I have switched to ItemsControl because I don't need all the selection tracking features that ListView provides. Will this work for ItemsControl, too? Regarding the ScrollViewer, will it be redundant with the ItemsControl instead the ListView, too? Thanks! – SuperJMN May 05 '17 at 10:53
  • Sure, if you replace `ListView` with an `ItemsControl` you have to provide your own scroll handling and `ScrollViewer` would make total sense there. You can still override `ItemsControl.OnItemsChanged` (I like this approach as it allows to entirely decouple UI controls code from the model), but if you go this way you'll need to either pass `ScrollViewer` reference to that method or look it up in the visual tree and then scroll to the bottom. – DK. May 05 '17 at 11:15
  • Please, take a look to the edit question above. I'm dealing with it using a Behavior (for the sake of reusability) and the TransformPoint call always returns the point 0,0, so it doesn't work. – SuperJMN May 09 '17 at 10:53
1

Here vm.newmessageItem is new message. I used that to fetch listviewitem where you want to scroll.

   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();
    }
Archana
  • 3,213
  • 1
  • 15
  • 21
0

This one line of code will automatically go to the item in the list you request. For instance, if you have a chat client and your ListView gets populated with each entry. This will show the latest entry at the bottom of the list. So, user does not have to scroll to the bottom each time. Here it is set to go to the last item. Works in a UWP app.

MyListView?.ScrollIntoView(MyListView.Items[allMyItemsInList.Count - 1], ScrollIntoViewAlignment.Leading);
Azurespot
  • 3,066
  • 3
  • 45
  • 73