diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index db6a750d..ec88dcba 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,8 +5,8 @@ true - 5.1.1 - 12.1.2 + 5.1.2 + 12.1.3 @@ -21,8 +21,8 @@ - - + + @@ -68,7 +68,7 @@ - + diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/Concurrency/DispatcherSequencer.cs b/src/ReactiveUI.Primitives.Wpf.Reactive/Concurrency/DispatcherSequencer.cs index f2b39374..3b6b4917 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/Concurrency/DispatcherSequencer.cs +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/Concurrency/DispatcherSequencer.cs @@ -15,6 +15,13 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency; [System.Diagnostics.DebuggerDisplay("DispatcherSequencer: Dispatcher = {Dispatcher}, Priority = {Priority}")] public sealed class DispatcherSequencer : LocalScheduler { + /// The shared main-thread scheduler, set once the application's dispatcher is available. + private static DispatcherSequencer? _main; + + /// The scheduler for the calling thread's dispatcher, cached per thread. + [ThreadStatic] + private static DispatcherSequencer? _current; + /// Optional callback for posting ready work. private readonly Func? _post; @@ -60,6 +67,23 @@ internal DispatcherSequencer( _dispatch = new(RunDrain, DefaultScheduler.Instance); } + /// Gets the shared scheduler for the WPF main (UI) thread. + /// No application exists yet and the calling thread has no dispatcher. + /// + /// Bound once to the running 's dispatcher, from any thread. Until an + /// application exists nothing is cached, and the calling thread's existing dispatcher is used instead. + /// + public static DispatcherSequencer Main => + Volatile.Read(ref _main) ?? BindMain(ref _main, System.Windows.Application.Current?.Dispatcher) ?? Current; + + /// Gets the scheduler for the calling thread's dispatcher. + /// The calling thread has no dispatcher. + /// + /// Cached per thread, for applications that run UI on more than one thread. Never creates a dispatcher: a thread + /// that has none cannot run the scheduled work. + /// + public static DispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher()); + /// Gets the dispatcher whose thread runs the scheduled work. public Dispatcher Dispatcher { get; } @@ -78,6 +102,29 @@ public override IDisposable Schedule(TState state, Func(TState state, TimeSpan dueTime, Func action) => _dispatch.Schedule(new DispatchHost(this), this, state, dueTime, action); + /// Caches the shared main-thread scheduler for the application's dispatcher, keeping the first one bound. + /// The field that holds the shared scheduler. + /// The application's dispatcher, or when no application exists. + /// The shared scheduler, or when is . + internal static DispatcherSequencer? BindMain(ref DispatcherSequencer? slot, Dispatcher? applicationDispatcher) + { + if (applicationDispatcher is null) + { + return null; + } + + DispatcherSequencer created = new(applicationDispatcher); + return Interlocked.CompareExchange(ref slot, created, null) ?? created; + } + + /// Returns the calling thread's existing dispatcher without creating one. + /// The calling thread's dispatcher. + /// The calling thread has no dispatcher. + internal static Dispatcher ResolveCurrentDispatcher() => + Dispatcher.FromThread(Thread.CurrentThread) + ?? throw new InvalidOperationException( + "The calling thread has no WPF dispatcher. Use DispatcherSequencer.Main or Current from a UI thread, or after the Application is created."); + /// Posts the drain callback, through the test hook when one was supplied. /// The drain callback. /// Whether the dispatcher accepted the callback. diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net10.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net10.0-windows/PublicAPI.txt index 980b0ebb..5b33c9ac 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net10.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net10.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net11.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net11.0-windows/PublicAPI.txt index 980b0ebb..5b33c9ac 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net11.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net11.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net462/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net462/PublicAPI.txt index 1cf2b989..01a52dd9 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net462/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net462/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net472/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net472/PublicAPI.txt index 1cf2b989..01a52dd9 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net472/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net472/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net48/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net48/PublicAPI.txt index 1cf2b989..01a52dd9 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net48/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net48/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net481/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net481/PublicAPI.txt index 1cf2b989..01a52dd9 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net481/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net481/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net8.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net8.0-windows/PublicAPI.txt index 980b0ebb..5b33c9ac 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net8.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net8.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net9.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net9.0-windows/PublicAPI.txt index 980b0ebb..5b33c9ac 100644 --- a/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net9.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf.Reactive/PublicAPI/net9.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : System.Reactive.Concurrency.LocalSched public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer Main { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public override System.IDisposable Schedule(TState state, System.Func action) { } diff --git a/src/ReactiveUI.Primitives.Wpf/Concurrency/DispatcherSequencer.cs b/src/ReactiveUI.Primitives.Wpf/Concurrency/DispatcherSequencer.cs index 47f49db5..f5a670b3 100644 --- a/src/ReactiveUI.Primitives.Wpf/Concurrency/DispatcherSequencer.cs +++ b/src/ReactiveUI.Primitives.Wpf/Concurrency/DispatcherSequencer.cs @@ -14,6 +14,13 @@ namespace ReactiveUI.Primitives.Concurrency; [System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] public sealed class DispatcherSequencer : ISequencer { + /// The shared main-thread sequencer, set once the application's dispatcher is available. + private static DispatcherSequencer? _main; + + /// The sequencer for the calling thread's dispatcher, cached per thread. + [ThreadStatic] + private static DispatcherSequencer? _current; + /// Coalescing dispatch engine. private DispatchSequencerState _state; @@ -51,6 +58,23 @@ internal DispatcherSequencer( _state = new(this, post ?? Post, RunDrain, scheduleDelayed ?? ScheduleDelayed); } + /// Gets the shared sequencer for the WPF main (UI) thread. + /// No application exists yet and the calling thread has no dispatcher. + /// + /// Bound once to the running 's dispatcher, from any thread. Until an + /// application exists nothing is cached, and the calling thread's existing dispatcher is used instead. + /// + public static DispatcherSequencer Main => + Volatile.Read(ref _main) ?? BindMain(ref _main, System.Windows.Application.Current?.Dispatcher) ?? Current; + + /// Gets the sequencer for the calling thread's dispatcher. + /// The calling thread has no dispatcher. + /// + /// Cached per thread, for applications that run UI on more than one thread. Never creates a dispatcher: a thread + /// that has none cannot run the scheduled work. + /// + public static DispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher()); + /// Gets the dispatcher whose thread runs the scheduled work. public Dispatcher Dispatcher { get; } @@ -75,6 +99,29 @@ internal DispatcherSequencer( [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Schedule(IWorkItem item, long dueTimestamp) => _state.Schedule(item, dueTimestamp); + /// Caches the shared main-thread sequencer for the application's dispatcher, keeping the first one bound. + /// The field that holds the shared sequencer. + /// The application's dispatcher, or when no application exists. + /// The shared sequencer, or when is . + internal static DispatcherSequencer? BindMain(ref DispatcherSequencer? slot, Dispatcher? applicationDispatcher) + { + if (applicationDispatcher is null) + { + return null; + } + + DispatcherSequencer created = new(applicationDispatcher); + return Interlocked.CompareExchange(ref slot, created, null) ?? created; + } + + /// Returns the calling thread's existing dispatcher without creating one. + /// The calling thread's dispatcher. + /// The calling thread has no dispatcher. + internal static Dispatcher ResolveCurrentDispatcher() => + Dispatcher.FromThread(Thread.CurrentThread) + ?? throw new InvalidOperationException( + "The calling thread has no WPF dispatcher. Use DispatcherSequencer.Main or Current from a UI thread, or after the Application is created."); + /// Marshals the cached drain callback onto the dispatcher. /// The drain callback. /// , since the dispatcher always accepts the work. diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net10.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net10.0-windows/PublicAPI.txt index 964e7afa..b3498857 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net10.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net10.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net11.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net11.0-windows/PublicAPI.txt index 964e7afa..b3498857 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net11.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net11.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net462/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net462/PublicAPI.txt index 178ccd13..38c3d4b4 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net462/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net462/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net472/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net472/PublicAPI.txt index 178ccd13..38c3d4b4 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net472/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net472/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net48/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net48/PublicAPI.txt index 178ccd13..38c3d4b4 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net48/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net48/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net481/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net481/PublicAPI.txt index 178ccd13..38c3d4b4 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net481/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net481/PublicAPI.txt @@ -7,6 +7,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net8.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net8.0-windows/PublicAPI.txt index 964e7afa..b3498857 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net8.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net8.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net9.0-windows/PublicAPI.txt b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net9.0-windows/PublicAPI.txt index 964e7afa..b3498857 100644 --- a/src/ReactiveUI.Primitives.Wpf/PublicAPI/net9.0-windows/PublicAPI.txt +++ b/src/ReactiveUI.Primitives.Wpf/PublicAPI/net9.0-windows/PublicAPI.txt @@ -8,6 +8,8 @@ public sealed class DispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISeq public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher) { } public DispatcherSequencer(System.Windows.Threading.Dispatcher dispatcher, System.Windows.Threading.DispatcherPriority priority) { } public System.Windows.Threading.Dispatcher Dispatcher { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Current { get; } + public static ReactiveUI.Primitives.Concurrency.DispatcherSequencer Main { get; } public System.DateTimeOffset Now { get; } public System.Windows.Threading.DispatcherPriority Priority { get; } public long Timestamp { get; } diff --git a/src/tests/ReactiveUI.Primitives.Wpf.Reactive.Tests/DispatcherSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Wpf.Reactive.Tests/DispatcherSequencerTests.cs index 052c1a6f..53d858c8 100644 --- a/src/tests/ReactiveUI.Primitives.Wpf.Reactive.Tests/DispatcherSequencerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Wpf.Reactive.Tests/DispatcherSequencerTests.cs @@ -38,6 +38,77 @@ public async Task ConstructorRetainsDispatcherAndPriority() await Assert.That(new DispatcherSequencer(dispatcher).Priority).IsEqualTo(DispatcherPriority.Normal); } + /// A thread without a dispatcher gets an error rather than a scheduler that could never run work. + /// The test operation. + [Test] + public async Task CurrentThrowsWhenTheThreadHasNoDispatcher() => + await Assert.That(static () => RunOnNewThread(static () => DispatcherSequencer.Current)) + .ThrowsExactly(); + + /// Each thread gets its own cached scheduler bound to that thread's dispatcher. + /// The test operation. + [Test] + public async Task CurrentIsCachedPerThreadAndBoundToThatThreadsDispatcher() + { + var first = await RunOnNewThread(CaptureCurrent); + var second = await RunOnNewThread(CaptureCurrent); + + await Assert.That(first.Repeat).IsSameReferenceAs(first.Sequencer); + await Assert.That(first.Sequencer.Dispatcher).IsSameReferenceAs(first.Dispatcher); + await Assert.That(first.Sequencer.Priority).IsEqualTo(DispatcherPriority.Normal); + await Assert.That(second.Sequencer).IsNotSameReferenceAs(first.Sequencer); + await Assert.That(second.Sequencer.Dispatcher).IsSameReferenceAs(second.Dispatcher); + } + + /// Before an application exists, Main uses the calling thread's dispatcher without caching it. + /// The test operation. + [Test] + public async Task MainFallsBackToCurrentBeforeAnApplicationExists() + { + if (System.Windows.Application.Current is not null) + { + return; + } + + var captured = await RunOnNewThread(static () => + { + _ = Dispatcher.CurrentDispatcher; + return (DispatcherSequencer.Main, DispatcherSequencer.Current); + }); + + await Assert.That(captured.Main).IsSameReferenceAs(captured.Current); + await Assert.That(static () => RunOnNewThread(static () => DispatcherSequencer.Main)) + .ThrowsExactly(); + } + + /// Without an application dispatcher there is nothing to bind, and nothing is cached. + /// The test operation. + [Test] + public async Task BindMainReturnsNullWithoutAnApplicationDispatcher() + { + DispatcherSequencer? slot = null; + await Assert.That(DispatcherSequencer.BindMain(ref slot, null)).IsNull(); + await Assert.That(slot).IsNull(); + } + + /// The first application dispatcher bound stays bound. + /// The test operation. + [Test] + public async Task BindMainKeepsTheFirstBinding() + { + var first = await RunOnNewThread(static () => Dispatcher.CurrentDispatcher); + var second = await RunOnNewThread(static () => Dispatcher.CurrentDispatcher); + DispatcherSequencer? slot = null; + + var bound = DispatcherSequencer.BindMain(ref slot, first); + var rebound = DispatcherSequencer.BindMain(ref slot, second); + + await Assert.That(bound).IsNotNull(); + await Assert.That(bound!.Dispatcher).IsSameReferenceAs(first); + await Assert.That(slot).IsSameReferenceAs(bound); + await Assert.That(rebound).IsSameReferenceAs(bound); + } + /// A posted batch preserves order and skips cancelled work. /// The test operation. [Test] @@ -131,4 +202,32 @@ public async Task DelayedScheduleHonorsCancellationBeforeDelivery(bool cancel) handle.Dispose(); } + + /// Gives the calling thread a dispatcher, then reads twice. + /// The thread's dispatcher and both reads. + private static (Dispatcher Dispatcher, DispatcherSequencer Sequencer, DispatcherSequencer Repeat) CaptureCurrent() => + (Dispatcher.CurrentDispatcher, DispatcherSequencer.Current, DispatcherSequencer.Current); + + /// Runs a function on a fresh STA thread and returns its result or exception. + /// The result type. + /// The function to run. + /// The function's result. + private static Task RunOnNewThread(Func func) + { + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + Thread thread = new(() => + { + try + { + completion.SetResult(func()); + } + catch (Exception ex) + { + completion.SetException(ex); + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + return completion.Task; + } } diff --git a/src/tests/ReactiveUI.Primitives.Wpf.Tests/DispatcherSequencerTests.cs b/src/tests/ReactiveUI.Primitives.Wpf.Tests/DispatcherSequencerTests.cs index ebd787c7..75f95a28 100644 --- a/src/tests/ReactiveUI.Primitives.Wpf.Tests/DispatcherSequencerTests.cs +++ b/src/tests/ReactiveUI.Primitives.Wpf.Tests/DispatcherSequencerTests.cs @@ -41,6 +41,77 @@ public async Task ConstructorRetainsDispatcherAndPriority() await Assert.That(sequencer.DebuggerDisplay).IsEqualTo(typeof(DispatcherSequencer).FullName); } + /// A thread without a dispatcher gets an error rather than a sequencer that could never run work. + /// The test operation. + [Test] + public async Task CurrentThrowsWhenTheThreadHasNoDispatcher() => + await Assert.That(static () => RunOnNewThread(static () => DispatcherSequencer.Current)) + .ThrowsExactly(); + + /// Each thread gets its own cached sequencer bound to that thread's dispatcher. + /// The test operation. + [Test] + public async Task CurrentIsCachedPerThreadAndBoundToThatThreadsDispatcher() + { + var first = await RunOnNewThread(CaptureCurrent); + var second = await RunOnNewThread(CaptureCurrent); + + await Assert.That(first.Repeat).IsSameReferenceAs(first.Sequencer); + await Assert.That(first.Sequencer.Dispatcher).IsSameReferenceAs(first.Dispatcher); + await Assert.That(first.Sequencer.Priority).IsEqualTo(DispatcherPriority.Normal); + await Assert.That(second.Sequencer).IsNotSameReferenceAs(first.Sequencer); + await Assert.That(second.Sequencer.Dispatcher).IsSameReferenceAs(second.Dispatcher); + } + + /// Before an application exists, Main uses the calling thread's dispatcher without caching it. + /// The test operation. + [Test] + public async Task MainFallsBackToCurrentBeforeAnApplicationExists() + { + if (System.Windows.Application.Current is not null) + { + return; + } + + var captured = await RunOnNewThread(static () => + { + _ = Dispatcher.CurrentDispatcher; + return (DispatcherSequencer.Main, DispatcherSequencer.Current); + }); + + await Assert.That(captured.Main).IsSameReferenceAs(captured.Current); + await Assert.That(static () => RunOnNewThread(static () => DispatcherSequencer.Main)) + .ThrowsExactly(); + } + + /// Without an application dispatcher there is nothing to bind, and nothing is cached. + /// The test operation. + [Test] + public async Task BindMainReturnsNullWithoutAnApplicationDispatcher() + { + DispatcherSequencer? slot = null; + await Assert.That(DispatcherSequencer.BindMain(ref slot, null)).IsNull(); + await Assert.That(slot).IsNull(); + } + + /// The first application dispatcher bound stays bound. + /// The test operation. + [Test] + public async Task BindMainKeepsTheFirstBinding() + { + var first = await RunOnNewThread(static () => Dispatcher.CurrentDispatcher); + var second = await RunOnNewThread(static () => Dispatcher.CurrentDispatcher); + DispatcherSequencer? slot = null; + + var bound = DispatcherSequencer.BindMain(ref slot, first); + var rebound = DispatcherSequencer.BindMain(ref slot, second); + + await Assert.That(bound).IsNotNull(); + await Assert.That(bound!.Dispatcher).IsSameReferenceAs(first); + await Assert.That(slot).IsSameReferenceAs(bound); + await Assert.That(rebound).IsSameReferenceAs(bound); + } + /// The dispatcher sequencer shares the monotonic timestamp scale used by scheduled work. /// A task representing the asynchronous test. [Test] @@ -138,6 +209,34 @@ public async Task DelayedScheduleWaitsForItsCallback(bool cancel) await Assert.That(calls).IsEqualTo(cancel ? 0 : 1); } + /// Gives the calling thread a dispatcher, then reads twice. + /// The thread's dispatcher and both reads. + private static (Dispatcher Dispatcher, DispatcherSequencer Sequencer, DispatcherSequencer Repeat) CaptureCurrent() => + (Dispatcher.CurrentDispatcher, DispatcherSequencer.Current, DispatcherSequencer.Current); + + /// Runs a function on a fresh STA thread and returns its result or exception. + /// The result type. + /// The function to run. + /// The function's result. + private static Task RunOnNewThread(Func func) + { + TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + Thread thread = new(() => + { + try + { + completion.SetResult(func()); + } + catch (Exception ex) + { + completion.SetException(ex); + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + return completion.Task; + } + /// Records execution and supports cancellation. /// The callback to run. private sealed class RecordingWorkItem(Action action) : IWorkItem, IsDisposed