2

I'm porting an application from MAUI to WPF, it's a chat application so I made this CollectionView in MAUI that contains all the messages from all the users inside a single chat and like a real chat application, the layout of messages is from top to bottom, the most recent message is at the bottom and I wanted to reproduce this behavior using simply: ItemsUpdatingScrollMode="KeepLastItemInView".

I want the ListView to automatically scroll to the last message when:

  • the view is opened (messages already present)
  • a new message is added while the user is already at the bottom.

Expected behavior

When the window opens or when I press "Send", the last message should be visible (like a typical chat). If the user scrolls up manually, auto-scroll should be disabled until the user returns to bottom or sends a new message.

Actual behavior

The ListView opens at the top. When I add a message, it may end up hidden below the input area unless I manually scroll.

In the MAUI version I simply access the chat I want to write to, and it automatically scrolls down to show the last message, or if the user write a message on the box and press Send, the view scrolls down automatically and focuses on the last message sent.

The thing is that now I have to reproduce the same thing in WPF, but obviously in WPF there isn't a CollectionView tag or even an ItemsUpdatingScrollMode property for a ListView.

Here's what I have in my XAML:

<ListView x:Name="MessagesList"
          Background="Transparent" Margin="8,0,0,0" BorderThickness="0"
          Grid.Row="1" ItemTemplate="{StaticResource ChatItemTemplate}"
          ScrollViewer.HorizontalScrollBarVisibility="Disabled"
          ScrollViewer.VerticalScrollBarVisibility="Auto"
          ItemsSource="{Binding Messages}"/>

Question
What is the correct WPF approach to keep a ListView automatically scrolled to the last item (like a chat), similar to ItemsUpdatingScrollMode="KeepLastItemInView" in MAUI?

I've already tried handling CollectionChanged and calling ScrollIntoView() from code-behind and from an attached behavior, but none of them worked reliably. I'm looking for the idiomatic WPF way to achieve this chat-like auto-scroll behavior.

8
  • 1
    You could use the answer here to implement scrolling to a selected object, and then use a two way binding for the selected iem(s) of your ListView so your view model can control which item to scroll to. Commented Oct 16 at 9:47
  • 1
    Then of course it's not working. My comment said you'd also need to bind the ListView's selected items property to your ViewModel. The ScrollToSelectedListBoxItemBehavior only scrolls to the selected either when the SelectionChanged or the IsVisibleChanged event from the ListView is triggered. You'd now need to trigger the SelectionChanged event by changing the selected item (i.e. via 2 way binding on your view model) Commented Oct 16 at 12:50
  • 1
    The problem is your "mental model" of how "logging" works; and how everything is (naturally) geared to expanding "down" and/or right. Eveything is simpler if you "push down" messages instead of trying to "push them up". "ScrollIntoView" would then also be more predictable with the last entry "at the top" (when inserting at index "0"). Commented Oct 16 at 15:58
  • 1
    As a note, you would not use a ListView when you don't set its View property. Use the simpler ListBox instead. Or even only an ItemsControl in a ScrollViewer when you don't need selection. Commented Oct 17 at 7:09
  • 1
    You could invert the top-to-bottom behavior by assigning a ScaleTransform with ScaleY=-1 to the LayoutTransform of the ScrollViewer and the item container, e.g. the ContentPresenter of an ItemsControl. Commented Oct 17 at 7:31

1 Answer 1

2

After a lot of attempts I finally did it!

I found out that the use of ListView was unnecessary so I used, instead, an ItemsControl inside a ScrollViewer and moved the Grid.Row="1" instead of being an attribute of the ListView, now it's a real Grid so I could assign a VerticalAlignment="Bottom" like this:

<Grid Grid.Row="1" VerticalAlignment="Bottom">
    <ScrollViewer ScrollViewer.VerticalScrollBarVisibility="Auto" attachedProperty:ScrollToBottomOnLoadProperty.Value="True">
        <ItemsControl x:Name="MessagesList"
                      Background="Transparent" Margin="8,0,0,0" BorderThickness="0"
                      ItemTemplate="{StaticResource ChatItemTemplate}"
                      ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                      VerticalAlignment="Bottom" VerticalContentAlignment="Bottom"
                      ItemsSource="{Binding Messages}"/>
    </ScrollViewer>
</Grid>

With this I created an AttachedProperty in order to make it work, but first I created the BaseAttachedProperty so I could then write my custom attached property that inherit from BaseAttachedProperty:

public abstract class BaseAttachedProperty<Parent, Property>
    where Parent : BaseAttachedProperty<Parent, Property>, new()
{
    public static Parent Instance { get; private set; } = new Parent();

    public event Action<DependencyObject, DependencyPropertyChangedEventArgs> ValueChanged = (sender, e) => { };

    public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached("Value", typeof(Property), typeof(BaseAttachedProperty<Parent, Property>), new PropertyMetadata(new PropertyChangedCallback(OnValuePropertyChanged)));
    
    private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Instance.OnValueChanged(d, e);
        Instance.ValueChanged(d, e);
    }

    public static Property GetValue(DependencyObject d) => (Property)d.GetValue(ValueProperty);

    public static void SetValue(DependencyObject d, Property value) => d.SetValue(ValueProperty, value);

    public virtual void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { }
}

Then this is the real AttachedProperty assigned to the ItemsControl:

using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace ViktoriaApp.AttachedProperties
{
    public class ScrollToBottomOnLoadProperty : BaseAttachedProperty<ScrollToBottomOnLoadProperty, bool>
    {
        public override void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (DesignerProperties.GetIsInDesignMode(sender))
                return;

            if (!(sender is ScrollViewer scrollViewer))
                return;

            scrollViewer.Loaded += ScrollViewer_Loaded;
        }

        private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
        {
            if (sender is ScrollViewer scrollViewer)
            {
                var itemsControl = FindItemsControl(scrollViewer);

                if (itemsControl?.ItemsSource is INotifyCollectionChanged collection)
                {
                    collection.CollectionChanged += (s, args) => ScrollToBottom(scrollViewer);
                }

                ScrollToBottom(scrollViewer);
            }
        }

        private ItemsControl FindItemsControl(DependencyObject parent)
        {
            for (int i = 0; i < System.Windows.Media.VisualTreeHelper.GetChildrenCount(parent); i++)
            {
                var child = System.Windows.Media.VisualTreeHelper.GetChild(parent, i);

                if (child is ItemsControl itemsControl)
                    return itemsControl;

                var result = FindItemsControl(child);
                if (result != null)
                    return result;
            }
            return null;
        }

        private void ScrollToBottom(ScrollViewer scrollViewer)
        {
            scrollViewer.Dispatcher.InvokeAsync(() =>
            {
                scrollViewer.ScrollToBottom();
            }, System.Windows.Threading.DispatcherPriority.Background);
        }
    }
}

Now with this code, every time I access inside a chat, I will always see the last messages in the bottom and I don't have to scroll manually all the way down, even with the Messages, as I send them the UI updates and show the last message.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.