Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.Sign up
fix: assign VM from correct thread in RoutedViewHost #1281
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
On iOS specifically a "UIKitThreadAccessException" is thrown from
I'm pretty sure the async await from the previous Select causes the Do to execute on a thread that's not the UI Thread which then causes a thread access exception.
What is the current behavior? (You can also link to an open issue here)
Project to recreate issue here
It's a bit of a rare issue because usually all the Views on the Navigation stack have the VMs set but in a case where the stack is deserialized that won't be the case.. Also this only seems to cause issues on iOS.
What is the new behavior (if this is a feature change)?
Does this PR introduce a breaking change?
Please check if the PR fulfills these requirements
Thanks @PureWeen. Can you check to make sure you're not modifying the navigation stack from a non-UI thread. That's the scenario that I think would explain your issue. I suspect there's a better way to solve this problem if that's the case.
Basically, the expectation is that things with UI thread affinity should be done on th UI thread. But we should be validating that with some kind of assertion rather than failing with an unhelpful exception. An
I'm pretty sure everything I'm doing up until that point isn't too crazy. The code I'm calling for the POP is just this
I was also curious at what point it "jumped ship"
If I break point the async SelectMany block above the DO then it's on the UI THREAD
But then it hops over to the Thread Pool
In my very non-scientific experience code that follows "async" blocks in Observables don't seem to hop back on the UI Thread.... For example in most of my internal Navigation code I always ObserveOn the MainScheduler afterwards because of this
That being said instead of doing an ObserveOn just moving the setter up into the async block also achieves the same result and probably without the added performance cost
Yea I mean it's still on the UI Thread after the awaits run within that block of work. In my last screen shot you'll see if I just assign the VM after the awaits instead of inside the Do then it all works fine. It's when it leaves the async block inside the SelectMany and then the work inside the Do gets scheduled on a Thread Pool thread.
Here's a screen shot though of adding .ConfigureAWait(true)'s
You'll notice the DO still runs on a ThreadPool
If I just use that commented out code in the finally block then it all works great so I'm thinking that's the way to go
If I were to venture a guess as to why it's like this.
My guess would be that it's due to the fact that inside Rx the continuation of the task is managed via ContinueWith opposed to an await so it's not going to save state and pick up where it left off. The work after the task is just going to get scheduled on I'm guessing the DefaultScheduler.
What I'm finding particularly confusing about all this is that I'm using
Another theory: could you try setting a breakpoint on the lambda inside
I think it's the difference with how "await" works vs "ContinueWith" . Inside the SelectMany it's using "await" which does a bunch of stuff to save the state and make sure that it comes back on the same thread. Whereas the surrounding implementation from Rx.NET uses ContinueWith on the Task to signal to the observer that it has completed. It's going to execute that ContinueWith on the Thread Pool because no scheduler was supplied to the ContinueWith meaning that Do is going to execute from the Thread Pool. At least that's my working theory at this point.
Here's a quick example of what I mean
As far as why you're not seeing the issue. If I were to guess I would say that maybe it's because you're never quite triggering the effect? This will only occur if setting the ViewModel from a Pop causes the property to change. Which will only really happen if the ViewModel is null. In my case I only came across this when I started the application on the second page and the VM hadn't been assigned yet to the first page. If that first page already has the VM set then setting the ViewModel to the same value does nothing so no exception or anything of interest occurs.
if you check out this sample here
I insert two pages on the stack and if you click the back button you'll notice on iOS the ViewModel fails to get set on the main page.
The Do afterwards
Also it might be even more rare that it comes up because I've noticed that the exception only happens when there's a binding that will trigger the exception
For example in my recreation if I comment this line out
And leave this line in
But if I bring that Label binding back then it no longer works. Which if what I'm saying is true about it setting on the ThreadPool makes sense but I'm just bringing it up because that might be another reason why you haven't observed it. Because there seems to only be a few types of cases that will throw an exception. Whereas other cases are resilient to the VM being set from the ThreadPool
Makes me wonder why we're using
Sorry to have to back and forth like this, but it's difficult when I don't have a repro. To be honest, I'm at work so don't have the time to try right now.
Ah, excellent! So the explanation here must be that
One more thing and I'll be happy to merge this: please change from
changed the title
bug: ensure vm on xam forms RoutedViewHost gets assigned from MainThr…
Feb 21, 2017
Sigh :-( Actually I'm pretty sure I did this wrong....... In my haste I forgot to actually return the Observable's from that block so they don't actually become part of that chain. This is why now it's staying on the UIThread because the DO is just running on the Immediate Scheduler... Looking at the Rx source code any non completed task will have it's result scheduled inside a ContinueWith which will be on the Thread Pool
I've been running a few permutations of Obs => Task interactions and can't really find a great combination that will cause the continuing Observable to stay on the UI Thread unless the Task for some reason has already completed.
I was messing with ToObservable in my own project and wasn't getting the same result of having the later code running on the UI Thread which is why I came across this
Something like this works though it's not that pretty
At this point though I feel like the best option would be to just roll back to the awaits and assign the VM in the finally block.