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