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 ObservableCollection
s that provide data to CollectionView
s?
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 youawait
aTask
, the default behavior should be to execute the code afterawait
line in the main thread. However, this could be not true if you use aTask.Run()
, that will force MAUI to use a new thread, or if you specifyConfigureAwait(false)
somewhere. Does one of these cases apply to you?@RiccardoMinato I’m not using
Task.Run()
orConfigureAwait()
and I agree with your point about MAUI handling what comes after a line withawait
inMainThread
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.
Show 1 more comment