Skip to content

Commit 5687935

Browse files
hinterlandcreativemadmonkey
authored andcommitted
fix: Replace default MainThreadScheduler on UAP (reactiveui#2100)
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.
1 parent 5e68838 commit 5687935

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed

src/ReactiveUI/Platforms/uap/PlatformRegistrations.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void Register(Action<Func<object>, Type> registerFunction)
2828
registerFunction(() => new BooleanToVisibilityTypeConverter(), typeof(IBindingTypeConverter));
2929
registerFunction(() => new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
3030
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
31-
RxApp.MainThreadScheduler = new WaitForDispatcherScheduler(() => CoreDispatcherScheduler.Current);
31+
RxApp.MainThreadScheduler = new SingleWindowDispatcherScheduler();
3232
registerFunction(() => new WinRTAppDataDriver(), typeof(ISuspensionDriver));
3333
}
3434
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright (c) 2019 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System;
7+
using System.Reactive.Concurrency;
8+
using System.Reactive.Disposables;
9+
using System.Reactive.PlatformServices;
10+
using System.Runtime.ExceptionServices;
11+
using System.Threading;
12+
using Windows.ApplicationModel.Core;
13+
using Windows.System.Threading;
14+
using Windows.UI.Core;
15+
using Windows.UI.Xaml;
16+
17+
namespace ReactiveUI
18+
{
19+
/// <summary>
20+
/// This scheduler forces all dispatching to go to the first window of the <see cref="CoreApplication.Views"/> enumeration.
21+
/// This makes the intended behavior of only supporting single window apps on UWP explicit.
22+
/// If your app creates multiple windows, you should explicitly supply a scheduler which marshals
23+
/// back to that window's <see cref="CoreDispatcher"/>.
24+
/// </summary>
25+
/// <remarks>
26+
/// This follows patterns set out in <see cref="CoreDispatcherScheduler"/> with some minor tweaks
27+
/// for thread-safety and performance.
28+
/// </remarks>
29+
/// <seealso cref="System.Reactive.Concurrency.IScheduler" />
30+
public class SingleWindowDispatcherScheduler : IScheduler
31+
{
32+
private static CoreDispatcher _dispatcher;
33+
private readonly CoreDispatcherPriority _priority;
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="SingleWindowDispatcherScheduler"/> class.
37+
/// </summary>
38+
public SingleWindowDispatcherScheduler()
39+
{
40+
if (CoreApplication.Views.Count > 0)
41+
{
42+
Interlocked.CompareExchange(ref _dispatcher, CoreApplication.Views[0].Dispatcher, null);
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Initializes a new instance of the <see cref="SingleWindowDispatcherScheduler"/> class with an explicit dispatcher.
48+
/// </summary>
49+
/// <param name="dispatcher">
50+
/// The explicit <see cref="CoreDispatcher"/> to use. If you supply a dispatcher here then all instances of
51+
/// <see cref="SingleWindowDispatcherScheduler"/> will dispatch to that dispatcher from instantiation on.
52+
/// </param>
53+
/// <exception cref="System.ArgumentNullException">
54+
/// dispatcher - To override the scheduler you must supply a non-null instance of CoreDispatcher.
55+
/// </exception>
56+
public SingleWindowDispatcherScheduler(CoreDispatcher dispatcher)
57+
{
58+
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher), "To override the scheduler you must supply a non-null instance of CoreDispatcher.");
59+
}
60+
61+
/// <inheritdoc/>
62+
public DateTimeOffset Now => SystemClock.UtcNow;
63+
64+
/// <inheritdoc/>
65+
public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
66+
{
67+
if (action is null)
68+
{
69+
throw new ArgumentNullException(nameof(action));
70+
}
71+
72+
if (CoreApplication.Views.Count == 0)
73+
{
74+
return CurrentThreadScheduler.Instance.Schedule(state, action);
75+
}
76+
77+
return ScheduleOnDispatcherNow(state, action);
78+
}
79+
80+
/// <inheritdoc/>
81+
public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
82+
{
83+
if (action == null)
84+
{
85+
throw new ArgumentNullException(nameof(action));
86+
}
87+
88+
if (CoreApplication.Views.Count == 0)
89+
{
90+
return CurrentThreadScheduler.Instance.Schedule(state, dueTime, action);
91+
}
92+
93+
var dt = Scheduler.Normalize(dueTime);
94+
if (dt.Ticks == 0)
95+
{
96+
return ScheduleOnDispatcherNow(state, action);
97+
}
98+
99+
return ScheduleSlow(state, dt, action);
100+
}
101+
102+
/// <inheritdoc/>
103+
public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
104+
{
105+
if (action is null)
106+
{
107+
throw new ArgumentNullException(nameof(action));
108+
}
109+
110+
if (CoreApplication.Views.Count == 0)
111+
{
112+
return CurrentThreadScheduler.Instance.Schedule(state, dueTime, action);
113+
}
114+
115+
var dt = Scheduler.Normalize(dueTime - DateTimeOffset.Now);
116+
if (dt.Ticks == 0)
117+
{
118+
return ScheduleOnDispatcherNow(state, action);
119+
}
120+
121+
return ScheduleSlow(state, dt, action);
122+
}
123+
124+
/// <summary>
125+
/// Work-around for the behavior of throwing from "async void" or an <see cref="IAsyncResult"/> not propagating
126+
/// the exception to the <see cref="Application.UnhandledException" /> event as users have come to expect from
127+
/// previous XAML stacks using Rx.
128+
/// </summary>
129+
/// <param name="ex">The exception.</param>
130+
private void RaiseUnhandledException(Exception ex)
131+
{
132+
var timer = new DispatcherTimer();
133+
timer.Interval = TimeSpan.Zero;
134+
timer.Tick += RaiseToDispatcher;
135+
136+
timer.Start();
137+
void RaiseToDispatcher(object sender, object e)
138+
{
139+
timer.Stop();
140+
timer.Tick -= RaiseToDispatcher;
141+
timer = null;
142+
143+
ExceptionDispatchInfo.Capture(ex).Throw();
144+
}
145+
}
146+
147+
private IDisposable ScheduleOnDispatcherNow<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
148+
{
149+
Interlocked.CompareExchange(ref _dispatcher, CoreApplication.Views[0].Dispatcher, null);
150+
151+
if (_dispatcher.HasThreadAccess)
152+
{
153+
return action(this, state);
154+
}
155+
156+
var d = new SingleAssignmentDisposable();
157+
158+
var dispatchResult = _dispatcher.RunAsync(
159+
_priority,
160+
() =>
161+
{
162+
if (!d.IsDisposed)
163+
{
164+
try
165+
{
166+
d.Disposable = action(this, state);
167+
}
168+
catch (Exception ex)
169+
{
170+
RaiseUnhandledException(ex);
171+
}
172+
}
173+
});
174+
175+
return StableCompositeDisposable.Create(
176+
d,
177+
Disposable.Create(() => dispatchResult.Cancel()));
178+
}
179+
180+
private IDisposable ScheduleSlow<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
181+
{
182+
var d = new MultipleAssignmentDisposable();
183+
184+
// Why ThreadPoolTimer?
185+
// --
186+
// Because, we can't guarantee that DispatcherTimer will dispatch to the correct CoreDispatcher if there are multiple
187+
// so we dispatch explicitly from our own method.
188+
var timer = ThreadPoolTimer.CreateTimer(_ => d.Disposable = ScheduleOnDispatcherNow(state, action), dueTime);
189+
190+
d.Disposable = Disposable.Create(() =>
191+
{
192+
var t = Interlocked.Exchange(ref timer, null);
193+
if (t != null)
194+
{
195+
t.Cancel();
196+
action = (_, __) => Disposable.Empty;
197+
}
198+
});
199+
200+
return d;
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)