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?
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());
}
}
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With