Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
same release pipeline and always share a version. -->
<RoslynCommonAnalyzersVersion>5.1.1</RoslynCommonAnalyzersVersion>
<AvaloniaVersion>12.1.2</AvaloniaVersion>
<RoslynCommonAnalyzersVersion>5.1.2</RoslynCommonAnalyzersVersion>
<AvaloniaVersion>12.1.3</AvaloniaVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401"/>
Expand All @@ -21,8 +21,8 @@
</ItemGroup>
<!-- Benchmarks + tests -->
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.16.0-preview.1"/>
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.16.0-preview.1"/>
<PackageVersion Include="BenchmarkDotNet" Version="0.16.0-preview.2"/>
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.16.0-preview.2"/>
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.10.0"/>
<PackageVersion Include="System.Reactive" Version="7.0.0"/>
<PackageVersion Include="Microsoft.Reactive.Testing" Version="7.0.0"/>
Expand Down Expand Up @@ -68,7 +68,7 @@
<!-- BenchmarkDotNet needs Roslyn 5.3+; this also reaches the job projects it generates under the benchmark bin folders. -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" Condition="$(MSBuildProjectDirectory.Replace('\', '/').Contains('/benchmarks/'))"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.10.1"/>
<PackageVersion Include="TUnit" Version="1.68.4"/>
<PackageVersion Include="TUnit" Version="1.69.0"/>
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.11.2"/>
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)/directory.packages.support.props" Condition="Exists('$(MSBuildThisFileDirectory)/directory.packages.support.props')"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency;
[System.Diagnostics.DebuggerDisplay("DispatcherSequencer: Dispatcher = {Dispatcher}, Priority = {Priority}")]
public sealed class DispatcherSequencer : LocalScheduler
{
/// <summary>The shared main-thread scheduler, set once the application's dispatcher is available.</summary>
private static DispatcherSequencer? _main;

/// <summary>The scheduler for the calling thread's dispatcher, cached per thread.</summary>
[ThreadStatic]
private static DispatcherSequencer? _current;

/// <summary>Optional callback for posting ready work.</summary>
private readonly Func<Action, bool>? _post;

Expand Down Expand Up @@ -60,6 +67,23 @@ internal DispatcherSequencer(
_dispatch = new(RunDrain, DefaultScheduler.Instance);
}

/// <summary>Gets the shared scheduler for the WPF main (UI) thread.</summary>
/// <exception cref="InvalidOperationException">No application exists yet and the calling thread has no dispatcher.</exception>
/// <remarks>
/// Bound once to the running <see cref="System.Windows.Application"/>'s dispatcher, from any thread. Until an
/// application exists nothing is cached, and the calling thread's existing dispatcher is used instead.
/// </remarks>
public static DispatcherSequencer Main =>
Volatile.Read(ref _main) ?? BindMain(ref _main, System.Windows.Application.Current?.Dispatcher) ?? Current;

/// <summary>Gets the scheduler for the calling thread's dispatcher.</summary>
/// <exception cref="InvalidOperationException">The calling thread has no dispatcher.</exception>
/// <remarks>
/// 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.
/// </remarks>
public static DispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher());

/// <summary>Gets the dispatcher whose thread runs the scheduled work.</summary>
public Dispatcher Dispatcher { get; }

Expand All @@ -78,6 +102,29 @@ public override IDisposable Schedule<TState>(TState state, Func<IScheduler, TSta
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) =>
_dispatch.Schedule(new DispatchHost(this), this, state, dueTime, action);

/// <summary>Caches the shared main-thread scheduler for the application's dispatcher, keeping the first one bound.</summary>
/// <param name="slot">The field that holds the shared scheduler.</param>
/// <param name="applicationDispatcher">The application's dispatcher, or <see langword="null"/> when no application exists.</param>
/// <returns>The shared scheduler, or <see langword="null"/> when <paramref name="applicationDispatcher"/> is <see langword="null"/>.</returns>
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;
}

/// <summary>Returns the calling thread's existing dispatcher without creating one.</summary>
/// <returns>The calling thread's dispatcher.</returns>
/// <exception cref="InvalidOperationException">The calling thread has no dispatcher.</exception>
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.");

/// <summary>Posts the drain callback, through the test hook when one was supplied.</summary>
/// <param name="drain">The drain callback.</param>
/// <returns>Whether the dispatcher accepted the callback.</returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(TState state, System.Func<System.Reactive.Concurrency.IScheduler, TState, System.IDisposable> action) { }
Expand Down
47 changes: 47 additions & 0 deletions src/ReactiveUI.Primitives.Wpf/Concurrency/DispatcherSequencer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ namespace ReactiveUI.Primitives.Concurrency;
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class DispatcherSequencer : ISequencer
{
/// <summary>The shared main-thread sequencer, set once the application's dispatcher is available.</summary>
private static DispatcherSequencer? _main;

/// <summary>The sequencer for the calling thread's dispatcher, cached per thread.</summary>
[ThreadStatic]
private static DispatcherSequencer? _current;

/// <summary>Coalescing dispatch engine.</summary>
private DispatchSequencerState _state;

Expand Down Expand Up @@ -51,6 +58,23 @@ internal DispatcherSequencer(
_state = new(this, post ?? Post, RunDrain, scheduleDelayed ?? ScheduleDelayed);
}

/// <summary>Gets the shared sequencer for the WPF main (UI) thread.</summary>
/// <exception cref="InvalidOperationException">No application exists yet and the calling thread has no dispatcher.</exception>
/// <remarks>
/// Bound once to the running <see cref="System.Windows.Application"/>'s dispatcher, from any thread. Until an
/// application exists nothing is cached, and the calling thread's existing dispatcher is used instead.
/// </remarks>
public static DispatcherSequencer Main =>
Volatile.Read(ref _main) ?? BindMain(ref _main, System.Windows.Application.Current?.Dispatcher) ?? Current;

/// <summary>Gets the sequencer for the calling thread's dispatcher.</summary>
/// <exception cref="InvalidOperationException">The calling thread has no dispatcher.</exception>
/// <remarks>
/// 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.
/// </remarks>
public static DispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher());

/// <summary>Gets the dispatcher whose thread runs the scheduled work.</summary>
public Dispatcher Dispatcher { get; }

Expand All @@ -75,6 +99,29 @@ internal DispatcherSequencer(
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Schedule(IWorkItem item, long dueTimestamp) => _state.Schedule(item, dueTimestamp);

/// <summary>Caches the shared main-thread sequencer for the application's dispatcher, keeping the first one bound.</summary>
/// <param name="slot">The field that holds the shared sequencer.</param>
/// <param name="applicationDispatcher">The application's dispatcher, or <see langword="null"/> when no application exists.</param>
/// <returns>The shared sequencer, or <see langword="null"/> when <paramref name="applicationDispatcher"/> is <see langword="null"/>.</returns>
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;
}

/// <summary>Returns the calling thread's existing dispatcher without creating one.</summary>
/// <returns>The calling thread's dispatcher.</returns>
/// <exception cref="InvalidOperationException">The calling thread has no dispatcher.</exception>
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.");

/// <summary>Marshals the cached drain callback onto the dispatcher.</summary>
/// <param name="drain">The drain callback.</param>
/// <returns><see langword="true"/>, since the dispatcher always accepts the work.</returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
2 changes: 2 additions & 0 deletions src/ReactiveUI.Primitives.Wpf/PublicAPI/net462/PublicAPI.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading
Loading