Populating ObservableCollection for CollectionView using MainThread in .NET MAUI app

I thought I understood when to use the MainThread in a .NET MAUI app but maybe not fully so I’d appreciate some pointers on this. In one particular page, I’m getting the following error message:

Objective-C exception thrown. Name: NSInternalInconsistencyException
Reason: Modifications to the layout engine must not be performed from
a background thread after it has been accessed from the main thread.

In my page, I have a CollectionView:

<CollectionView
   ItemsSource="{Binding Feed}"
   RemainingItemsThreshold="0"
   RemainingItemsThresholdReachedCommand="{Binding GetMoreFeedItemsCommand}"
   IsVisible="{Binding IsFeedVisible}">
   ... Omitted for brevity
<CollectionView>

And here’s my view model code:

private int LastFeedId { get; set; }
private DateTime LastDataFetchTimeStamp { get; set; }

public ObservableCollection<FeedItem> Feed { get; } = new();

[RelayCommand]
async Task GetMoreFeedItems()
{
   if(LastDataFetchTimeStamp.AddSeconds(30) >= DateTime.UtcNow)
      return;

   IsBusy = true;

   // Fetch new data
   await GetFeedDataAsync();

   IsBusy = false;
}

private async Task GetFeedDataAsync()
{
   var data = await MyService.GetFeedDataAsync(LastFeedId);

   LastDataFetchTimeStamp = DateTime.UtcNow;

   if(data != null && data.Count > 0)
   {
      LastFeedId = data.Max(x => Id);
      foreach(var item in data)
         Feed.Add(item);
   }

   IsFeedVisible = true;
}

internal async Task InitAsync()
{
   // InitAsync() gets called OnAppearing()
   if(LastDataFetchTimeStamp.AddSeconds(30) < DateTime.UtcNow)
      await GetFeedDataAsync();
}

The part that I want to understand is that when I call an async function, it gets handled in a background thread. So calling GetFeedDataAsync() method should be handled by a background thread.

With that said, I need to populate my ObservableCollection<FeedItem> which provides the data to the CollectionView.

Do I need to handle the foreach loop that adds data to the ObservableCollection in MainThread like:

...
MainThread.BeginInvokeOnMainThread(() =>
{
   foreach(var item in data)
      Feed.Add(item);
});
...

In other pages with a similar situation, I actually add data to an ObservableCollection without invoking the MainThread and I get no errors.

However on this page, I am getting the error mentioned above. I wonder if that’s because the GetFeedDataAsync() gets called both in the very beginning when the page appears and also to fetch more items after the user reaches the bottom of his feed.

The interesting verbiage in the error message is:

… after it has been accessed from the main thread

I’m trying to understand why I don’t get errors on other pages where the data fetch method gets called only once as opposed to this page that produces this error.

Should I always use MainThread for my foreach loops that add to data to ObservableCollections that provide data to CollectionViews?

  • Yes you need to handle the foreach at mainthread, where the UI lives.

    – 

  • You may need to implement it as a notifying property and then replace the entire List with another one that contains the new Feed. And the issue seems it is a new exception in iOS 16.4, see:github.com/dotnet/maui/issues/10163#issuecomment-1558960487

    – 

  • It’s a long story, but afaik no, you shouldn’t need to call MainThread.BeginInvokeOnMainThread every time you have to update UI. That’s because of how asynchronous programming is handled in MAUI. When you await a Task, the default behavior should be to execute the code after await line in the main thread. However, this could be not true if you use a Task.Run(), that will force MAUI to use a new thread, or if you specify ConfigureAwait(false) somewhere. Does one of these cases apply to you?

    – 




  • @RiccardoMinato I’m not using Task.Run() or ConfigureAwait() and I agree with your point about MAUI handling what comes after a line with await in MainThread because as I mentioned in original post, I don’t get any errors in any other page.

    – 




  • @Sam I would really be curios to see a minimal repro. I always felt that this topic was a grey zone, but I was never able to prove it. You could also check the current thread id when that instruction is executed, so you can understand if the non-main thread is used in the first call or in the following calls and maybe you could check the thread id in the code executed before to find out where the new thread is spawned. Please keep me updated about this, I’m very interested. Thank you.

    – 




Leave a Comment