Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Replace default MainThreadScheduler on UAP #2100

Merged
merged 3 commits into from
Jul 1, 2019
Merged

fix: Replace default MainThreadScheduler on UAP #2100

merged 3 commits into from
Jul 1, 2019

Conversation

hinterlandcreative
Copy link
Contributor

@hinterlandcreative hinterlandcreative commented Jun 29, 2019

What kind of change does this PR introduce?
This replaces the usage of WaitForDispatcherScheduler and CoreDispatcherScheduler with a scheduler SingleWindowDispatcherScheduler that explicitly dispatches to the main app window regardless of how many windows are created by the application.

What is the current behavior?
See #2092, #2034 for more info. Should also resolve or at least improve the situation in #1001. Every window created by the CoreApplication gets it's own shiny new UI thread and CoreDispatcher. Example of this would be applications which use casting, ShareTarget, ProjectionManager or are just snazzy enough to have an app with multiple windows.

Because WaitForDispatcherScheduler uses a factory to get an instance of CoreDispatcherScheduler and CoreDispatcherScheduler uses Window.Current to obtain a CoreDispatcher there are multiple ways for MainThreadScheduler to be hijacked by a second window. For instance, if the thread being used by that window is returned to the thread pool (e.g. - by that window being closed or launching your main window from a ShareTargetActivated) then forever that [ThreadStatic] instance of MainThreadScheduler will attempt to dispatch to an non-existent window. This will surface in thread marshaling exceptions which will very likely crash the user's application (or cause nasty UI issues like controls which no longer update).

Of course, in these situations most devs will simply inject a scheduler into their view models for anything that used on a separate window. But this still won't block the hijacking because if code from the main window is ever executed on the thread separate window and then attempts to get an instance of MainThreadScheduler you could still have this problem. An example of this is code executing from the UI thread of window 2 updates an observable which is subscribed to on a view model or service being used in window 1. If in that code now in window 1 attempts to ObserveOn(RxApp.MainThreadScheduler) and MainThreadScheduler does not have an instance for that thread (which it likely wouldn't since we aren't using it on our other window in this example) it will grab the wrong dispatcher because for that thread Window.Current.Dispatcher is not in fact null, but is the dispatcher for window 2.

Since that is a very likely scenario in Rx, the problem should be solved in ReactiveUI.

What is the new behavior?

SingleWindowDispatcherScheduler does not use CoreDispatcherScheduler as its underlying scheduler, but does use code and patterns from that implementation.

It also does not use Window.Current to find the correct dispatcher. Instead it uses CoreApplication.Views to find the dispatcher. This has several advantages: 1) It is always available and you can get the correct dispatcher from any thread, 2) It is not possible to get the wrong dispatcher because your app's main window will always exist at CoreApplication.Views[0]

What might this PR break?
If for some reason a user was expressly relying on RxApp.MainThreadScheduler being of a specific type they will be discouraged from doing that ever again.

Please check if the PR fulfills these requirements

  • [] Tests for the changes have been added (for bug fixes / features)
  • [] Docs have been added / updated (for bug fixes / features)

Other information:
Unfortunately this code is not testable in a unit test, but then again neither are other platform schedulers (see NSRunloopScheduler). Since most of the actual dispatching code is taken from CoreDispatcherScheduler I think we are ok.

A separate PR will be added to the website documentation to make it VERY clear that ReactiveUI supports one window UWP environments only out of the box and suggest appropriate workarounds for multiple windows.

Also tested that this will not cause a regression on fixes from c5b9462 (original issue: #1221)

See #2092, #2032 for more info. This replaces the usage of
`WaitForDispatcherScheduler` and `CoreDispatcherScheduler` with a
scheduler that explicitly dispatches to the main app window regardless
of how many windows are created by the application.
@hinterlandcreative hinterlandcreative requested a review from a team June 29, 2019 22:40
@dnfclas
Copy link

dnfclas commented Jun 29, 2019

CLA assistant check
All CLA requirements met.

@hinterlandcreative
Copy link
Contributor Author

hinterlandcreative commented Jun 29, 2019

@glennawatson I did give a multiple window scheduler the good ole' college try but after about a week of testing I do not think it's possible. See an attempt here: https://gist.github.com/hinterlandsupplyco/c8e0396f6d837fdc9cd2a5b00688b451

Gist
This is an attempt to build a scheduler for `ReactiveUI` which can handle multiple windows. It doesn't work. - MultipleWindowDispatcherScheduler.cs

@glennawatson
Copy link
Contributor

I'm going to get multiple reviewers on this one who have some uwp experience. So hang in there.

Copy link
Contributor

@worldbeater worldbeater left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tried the new SingleWindowDispatcherScheduler in several Universal Windows Platform applications, so far I confirm it works good without breaking anything, although those applications are all single window apps. This change seems reasonable to me, so I approve – at least this fixes the scheduler hijacking issue. Probably we should wait a bit longer to gather more opinions from UWP reviewers.

@glennawatson glennawatson merged commit 2fc4e6e into reactiveui:master Jul 1, 2019
@hinterlandcreative hinterlandcreative deleted the uwp-multiple-windows-bugs branch July 1, 2019 22:43
@glennawatson
Copy link
Contributor

Thanks for this one @hinterlandsupplyco sure it will assist others.

@hinterlandcreative
Copy link
Contributor Author

@glennawatson happy to contribute! :-)

madmonkey pushed a commit to madmonkey/ReactiveUI that referenced this pull request Jul 12, 2019
See reactiveui#2092, reactiveui#2032 for more info. This replaces the usage of
`WaitForDispatcherScheduler` and `CoreDispatcherScheduler` with a
scheduler that explicitly dispatches to the main app window regardless
of how many windows are created by the application.
@jasonwurzel
Copy link
Contributor

this fix does not work in the constellation WPF + WindowsXamlHost + UWP content.
CoreApplication.Views.Count is > 0, yet accessing CoreApplication.Views[0].Dispatcher throws an Exception. I'm looking into it, maybe I can implement a fallback for XamlIslands...

@glennawatson
Copy link
Contributor

glennawatson commented Sep 3, 2019

Hey @jasonwurzel if you can make a new issue even linking back to this one that would be great.

@KAS1990
Copy link
Contributor

KAS1990 commented Sep 28, 2019

Hello. I have tested new SingleWindowDispatcherScheduler and found bug. One of my view models has such code
Observable.Interval(TimeSpan.FromSeconds(.5)).Select(_ => State)
.DistinctUntilChanged()
.SubscribeOn(RxApp.MainThreadScheduler)
.Subscribe(_ => this.RaisePropertyChanged(nameof(State)))
.DisposeWith(DisposableOnDestroy);
My app allows user to create new view by ProjectionManager.StartProjectingAsync. Often when my app executes
await CoreApplication.CreateNewView().Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {...});
COMException "Element not found" is fired, and execution is broken on this.RaisePropertyChanged(nameof(State) or outside my code (somewhere in System.Private.CoreLib.dll).
In first situation stack trace leads to
SingleWindowDispatcherScheduler.ScheduleOnDispatcherNow(TState state, Func<IScheduler, TState, IDisposable> action) line 152, where scheduler tries to get CoreApplication.Views[0].Dispatcher, but it cannot do it. I do not know why?
In second situation i cannot get stack trace but i think the problem is the same.
But often such exceptions are fired during normal program execution, not when RunAsync is called.
Can you help me?

@lock lock bot locked and limited conversation to collaborators Dec 27, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants