ObservableRangeCollection
AddRange and ReplaceRange raise one CollectionChanged Reset. Do not Add in a loop for mid or large lists. Large lists must also virtualize in CollectionView. After a page is visible, prefer Add / Insert over ReplaceRange on Android BindableLayout.
var items = new ObservableRangeCollection<Product>();
items.AddRange(page);
items.ReplaceRange(next);Pagination, snapshots, and search
PagedCollection<T> / DelegatePagedCollection<T> own load-more, refresh, and retry — not a live chat inbox. SnapshotCollection<T> loads once in InitializeAsync; later LoadAsync calls are no-ops unless force is true. After appear, mutate with AddLocal / Insert. Do not pair DelegatePagedCollection with CollectionView RemainingItemsThreshold when the fetch is sync. SearchQuery.Text binds to Entry; filter from CommittedText after debounce. Do not two-way bind SearchQuery to Android SearchBar.
public sealed class InboxViewModel : ViewModel
{
public SnapshotCollection<Conversation> Inbox { get; }
public SearchQuery Search { get; } = new();
public InboxViewModel(IInbox catalog)
{
Inbox = new SnapshotCollection<Conversation>(
ct => catalog.ListAsync(Search.CommittedText, ct));
}
protected override Task InitializeAsync(CancellationToken cancellationToken) =>
Inbox.LoadAsync(cancellationToken: cancellationToken);
}
// catalog paging (not a live inbox)
await Products.RefreshAsync(ct);
await Products.LoadMoreAsync(ct);