We're experiencing persistent issues with the scrolling functionality in our MAUI chat application using SfListView. Specifically, the ListView fails to reliably scroll to the latest message when new messages are added.
Environment Details
- Platform: .NET MAUI
- OS: Android and iOS (tested on latest versions of both)
- Syncfusion Package Version: 25.1.35
- Visual Studio Version: 17.9.4
Issue Details
When new messages are added to our chat interface built with SfListView, the view does not automatically scroll down to show the latest messages. We're calling the ScrollTo method on the SfListView, but it's inconsistent in its behavior:
- The ScrollToLatest method is properly called, but the list doesn't scroll to the end
- The issue is more prominent when new messages are added and the keyboard is visible
- We've verified that our Messages collection is properly updating
Code Examples
We're using SfListView with the following configuration:
ChatView = new SfListView();
ChatView.ItemSpacing = 5;
ChatView.ItemTemplate = messageTemplateSelector;
ChatView.SelectionBackground = Brush.Transparent;
ChatView.AutoFitMode = AutoFitMode.DynamicHeight;
ChatView.ScrollBarVisibility = ScrollBarVisibility.Never;
ChatView.SelectionMode = Syncfusion.Maui.ListView.SelectionMode.None;
ChatView.SetBinding(SfListView.ItemsSourceProperty, nameof(ChatGPTModel.Messages));
Our ScrollToLatest method looks like this:
public void ScrollToLatest()
{
if (ChatGPTModel?.Messages == null || ChatGPTModel.Messages.Count == 0)
return;
var scrollView = ChatView.GetScrollView();
if (scrollView == null)
return;
var lastItem = ChatGPTModel.Messages[ChatGPTModel.Messages.Count - 1];
ChatView.ScrollTo(lastItem, ScrollToPosition.End, false);
}
We call this method from our page's OnAppearing:
protected override void OnAppearing()
{
try
{
base.OnAppearing();
#if ANDROID
Platform.CurrentActivity?.Window?.SetSoftInputMode(Android.Views.SoftInput.AdjustResize);
#endif
if (TabView?.SelectedIndex >= 0 && TabView.Items.Count > TabView.SelectedIndex)
{
if (TabView.Items[(int)TabView.SelectedIndex] is FCustomTabItem selectedTabItem &&
selectedTabItem.Page is FChatGPT chatPage)
{
try
{
chatPage.ScrollToLatest();
chatPage.FocusEntry();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to scroll: {ex.Message}");
}
}
}
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine($"Exception in OnAppearing: {e.Message}");
}
}
We also call it after adding messages to our collection:
public async Task SendMessage(string message)
{
// Add user message to collection
Messages.Add(new ChatMessage
{
Text = message,
IsUserMessage = true
});
// Try to scroll to show the message
ConversationView?.ScrollToLatest();
// API call and response handling...
// ...
// Add bot response to collection
Messages.Add(new ChatMessage
{
Text = responseText,
IsUserMessage = false
});
// Try to scroll again after response
ConversationView?.ScrollToLatest();
}
We've tried:
- Using both
ScrollTo method and direct scrollView.ScrollToAsync calls
- Adding delays with
await Task.Delay()
- Ensuring calls are on the UI thread
- Various combinations of the
animated parameter (true/false)
But the scroll behavior remains unreliable.
Steps to Reproduce
- Add a SfListView to a MAUI page
- Populate it with chat message items
- Set up ScrollTo method to scroll to the latest item
- Call this method after adding new items to the collection
- Observe that the view doesn't reliably scroll to the latest item
Here's a minimal reproduction sample:
// In a page class
public partial class ChatPage : ContentPage
{
private SfListView chatListView;
private ObservableCollection<MessageItem> messages;
private Entry messageEntry;
public ChatPage()
{
messages = new ObservableCollection<MessageItem>();
// Set up the list view
chatListView = new SfListView
{
ItemsSource = messages,
ItemSize = 200,
ItemSpacing = 5,
AutoFitMode = AutoFitMode.DynamicHeight,
SelectionMode = SelectionMode.None,
ItemTemplate = new DataTemplate(() => new MessageViewCell())
};
// Set up the input field
messageEntry = new Entry { Placeholder = "Type a message" };
Button sendButton = new Button { Text = "Send" };
sendButton.Clicked += SendMessage;
// Layout
Grid inputGrid = new Grid
{
ColumnDefinitions =
{
new ColumnDefinition { Width = GridLength.Star },
new ColumnDefinition { Width = GridLength.Auto }
}
};
inputGrid.Add(messageEntry, 0, 0);
inputGrid.Add(sendButton, 1, 0);
Content = new Grid
{
RowDefinitions =
{
new RowDefinition { Height = GridLength.Star },
new RowDefinition { Height = GridLength.Auto }
},
Children =
{
{ chatListView, 0, 0 },
{ inputGrid, 0, 1 }
}
};
}
private void SendMessage(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(messageEntry.Text))
return;
// Add user message
messages.Add(new MessageItem
{
Text = messageEntry.Text,
IsUser = true
});
// Attempt to scroll to the last item
ScrollToLatest();
// Simulate bot response after 1 second
string userMessage = messageEntry.Text;
messageEntry.Text = "";
Task.Delay(1000).ContinueWith(t =>
{
// Add bot message
MainThread.BeginInvokeOnMainThread(() =>
{
messages.Add(new MessageItem
{
Text = $"You said: {userMessage}",
IsUser = false
});
// Attempt to scroll again - this often fails
ScrollToLatest();
});
});
}
public void ScrollToLatest()
{
if (messages.Count == 0) return;
var lastItem = messages[messages.Count - 1];
chatListView.ScrollTo(lastItem, ScrollToPosition.End, false);
// We've also tried:
// var scrollView = chatListView.GetScrollView();
// if (scrollView != null)
// {
// scrollView.ScrollToAsync(0, scrollView.ContentSize.Height, true);
// }
}
}
public class MessageItem
{
public string Text { get; set; }
public bool IsUser { get; set; }
}
public class MessageViewCell : ViewCell
{
public MessageViewCell()
{
var label = new Label
{
Padding = new Thickness(10),
TextColor = Colors.White
};
label.SetBinding(Label.TextProperty, "Text");
var frame = new Frame
{
CornerRadius = 10,
Content = label
};
var grid = new Grid();
grid.Children.Add(frame);
// Position based on sender
var isUserBinding = new Binding("IsUser");
frame.SetBinding(Frame.BackgroundColorProperty,
new Binding("IsUser", converter: new BoolToColorConverter()));
frame.SetBinding(Frame.HorizontalOptionsProperty,
new Binding("IsUser", converter: new BoolToOptionsConverter()));
View = grid;
}
}
// Converters
public class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Colors.Blue : Colors.Gray;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToOptionsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? LayoutOptions.End : LayoutOptions.Start;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}