maui icon indicating copy to clipboard operation
maui copied to clipboard

Navigation messed up permanently if Shell.Current.GoToAsync() is called in the wrong thread

Open mavispuford opened this issue 3 years ago • 5 comments

Description

When calling Shell.Current.GoToAsync() outside of the main thread, we should expect to get an exception with no other negative effects. In some cases we do get an exception, like when awaiting a Task.Run() with a GoToAsync() call inside. Here's what happens afterwards:

Android - From that point forward, the navigation stack is messed up and new pages are blank/behavior is odd.
iOS - Future GoToAsync() calls result in a NullReferenceException and an app crash. Windows - A COMException is thrown and the page is still pushed. Afterwards, the behavior is fine. I wasn't able to test on the remaining platforms

Background

In our app, we borrowed the code from DispatcherExtensions (since it's internal) to try to be safe around threading when it comes to navigation. We use a navigation service to navigate from our view models, and those navigation calls could be happening outside of the main thread. Our navigation service was essentially doing something like this:

await _dispatcher.DispatchIfRequiredAsync(async () => await Shell.Current.GoToAsync(name, animated, parameters));

However, we were finding that in some cases Dispatcher.IsDispatchRequired was reporting false when it should have been true. This was causing our app to call GoToAsync() outside of the main thread. This is when we started noticing this weird behavior.

We've since switched from using DispatchIfRequiredAsync() to DispatchAsync() every time without checking Dispatch.IsDispatchRequired, so this isn't really a big issue for us anymore, but it is probably unintended behavior that you would like to fix.

Steps to Reproduce

  1. Clone my linked repo
  2. Tap the buttons on the main page (the first two should work fine UNTIL you tap either of the bottom two)
  3. Observe the app behavior and the console output (I tried to log the flow using Console.WriteLine())

Link to public reproduction project repository

https://github.com/mavispuford/MauiDispatcherDemo

Version with bug

7.0 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android, I was not able test on other platforms

Affected platform versions

Android 13, iOS 16.2, Windows 10.0.19044 Build 19044

Did you find any workaround?

Just use Dispatcher.Dispatch() and Dispatcher.DispatchAsync() every time to be safe.

Relevant log output

No response

mavispuford avatar Feb 24 '23 00:02 mavispuford

UI stuff should always be done on UI thread. Instead of relying to DispatchIfRequiredAsync just use https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/appmodel/main-thread?view=net-maui-7.0#run-code-on-the-ui-thread

Symbai avatar Feb 24 '23 11:02 Symbai

It's my understanding that Dispatcher is the preferred method in .NET MAUI. It seems like some better documentation might be needed, though. If I search through .NET MAUI docs, it isn't super clear what we should be using and why/when. However, I did find these great comments from @mattleibow and Rob Caplan that explain things in more detail. Those comments are then expanded upon in this StackOverflow post.

mavispuford avatar Feb 24 '23 17:02 mavispuford

The link above is from MAUI docs and explains what you should use in your scenario, why and when. And the things you were doing previously are not stated there.

Symbai avatar Feb 24 '23 20:02 Symbai

If you read Matt's comments, you'll see that MainThread isn't always the answer:

MainThread has no concept of the UI thread so picks the first one. In most cases, it is correct because most platforms only have 1 "main thread". Windows is an outlier to this because it supports windows on separate threads.

Rob Caplan's (also from Microsoft) comments:

InvokeOnMainThread was an oversimplification - not all apps have a main thread or a singular UI thread. Associating the Dispatcher to a UI object (which will in turn be tied to a thread) is more general and better supports multi-window applications.

The MAUI docs make no mention of this on that page, which is why it would be nice to have more clarification in their documentation.

Matt also talks about best practices:

So, what is the "correct" way to do things? Well, mostly I would say use dispatcher if you can - keep in mind that you can pass the IDispatcher around and can inject it via dependency injection because it is part of the DI system already. You can also pass the instance around since the object is valid for as long as the thread is valid.

mavispuford avatar Feb 24 '23 21:02 mavispuford

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost avatar Feb 28 '23 19:02 ghost

Visual Studio Enterprise 17.8.0 Preview 2.0. Can repro on android and ios platform with sample project. MauiDispatcherDemo-main.zip

Zhanglirong-Winnie avatar Oct 11 '23 07:10 Zhanglirong-Winnie

I almost can't believe this is still in the BackLog since February! This as a really sensitive issue! Anyway I'm running into this issue and can confirm it on Android when GoToAsync() is called from inside of a Task.Run(). The navigation is then ruined.

MauiUIui avatar Dec 11 '23 19:12 MauiUIui

Any update? Experiencing the same issue. Can replicate with using the IDispatcher.

SeanHogg avatar Jul 14 '24 16:07 SeanHogg

I have something quite similar.

In Net9 - Android have this code:

await Shell.Current.GoToAsync(shellNavigation, true, routeParameters);

I get this error:

'Exception has been thrown by the target of an invocation.'

And a deep stack:

0xAC in System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs	C#
0x9C in System.Reflection.MethodBaseInvoker.InvokeWithOneArg	C#
0x76 in System.Reflection.RuntimeMethodInfo.Invoke	C#
0x55 in System.Reflection.RuntimePropertyInfo.SetValue	C#
0x7 in System.Reflection.PropertyInfo.SetValue	C#
0x4 in System.Reflection.PropertyInfo.SetValue	C#
0x118 in Microsoft.Maui.Controls.ShellContent.ApplyQueryAttributes at /_/src/Controls/src/Core/Shell/ShellContent.cs:340,9	C#
0x51 in Microsoft.Maui.Controls.ShellContent.ApplyQueryAttributes at /_/src/Controls/src/Core/Shell/ShellContent.cs:315,5	C#
0xD in Microsoft.Maui.Controls.ShellContent.OnQueryAttributesPropertyChanged at /_/src/Controls/src/Core/Shell/ShellContent.cs:300,4	C#
0x14C in Microsoft.Maui.Controls.BindableObject.SetValueActual at /_/src/Controls/src/Core/BindableObject.cs:650,5	C#
0x168 in Microsoft.Maui.Controls.BindableObject.SetValueCore at /_/src/Controls/src/Core/BindableObject.cs:576,5	C#
0x5F in Microsoft.Maui.Controls.BindableObject.SetValue at /_/src/Controls/src/Core/BindableObject.cs:481,4	C#
0xE2 in Microsoft.Maui.Controls.ShellNavigationManager.ApplyQueryAttributes at /_/src/Controls/src/Core/Shell/ShellNavigationManager.cs:314,5	C#
0x4C in Microsoft.Maui.Controls.ShellSection.GetOrCreateFromRoute at /_/src/Controls/src/Core/Shell/ShellSection.cs:511,4	C#
0x260 in Microsoft.Maui.Controls.ShellSection.GoToAsync at /_/src/Controls/src/Core/Shell/ShellSection.cs:546,5	C#
0x2F in System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start<Microsoft.Maui.Controls.ShellSection.<GoToAsync>d__65>	C#
0x1 in System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Microsoft.Maui.Controls.ShellSection.<GoToAsync>d__65>	C#
0x4F in Microsoft.Maui.Controls.ShellSection.GoToAsync	C#
0xAF1 in Microsoft.Maui.Controls.ShellNavigationManager.GoToAsync at /_/src/Controls/src/Core/Shell/ShellNavigationManager.cs:196,5	C#
0x2F in System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start<Microsoft.Maui.Controls.ShellNavigationManager.<GoToAsync>d__14>	C#
0x1 in System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<Microsoft.Maui.Controls.ShellNavigationManager.<GoToAsync>d__14>	C#
0x35 in Microsoft.Maui.Controls.ShellNavigationManager.GoToAsync	C#
0x3 in Microsoft.Maui.Controls.ShellNavigationManager.GoToAsync at /_/src/Controls/src/Core/Shell/ShellNavigationManager.cs:45,4	C#
0x33 in Microsoft.Maui.Controls.ShellNavigationManager.GoToAsync at /_/src/Controls/src/Core/Shell/ShellNavigationManager.cs:33,4	C#
0x1E in Microsoft.Maui.Controls.Shell.GoToAsync at /_/src/Controls/src/Core/Shell/Shell.cs:990,4	C#

0x4B in ZoEazy.Net9.Admin.Services.NavigationService.NavigateToAsync at C:\Users\mdelg\source\repos\ZoEazy\ZoEazy.Net9.Admin\Services\NavigationService.cs:29,13 C#

As you can see, it happens in a deep level of interaction.

The error happens when Android passes a string parameter (an email address in this case)

For now, I just skip the parameters but this is due to became a nightmare...

On a personal note, I want to thank all the Maui teams for the gigantic effort.

mdelgadov avatar Aug 07 '24 21:08 mdelgadov