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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using Microsoft.Maui;
using Microsoft.Maui.Dispatching;

namespace ReactiveUI.Primitives.Reactive.Concurrency;
Expand All @@ -15,6 +16,13 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency;
[System.Diagnostics.DebuggerDisplay("MauiDispatcherSequencer: Dispatcher = {Dispatcher}")]
public sealed class MauiDispatcherSequencer : LocalScheduler
{
/// <summary>The shared main-thread scheduler, set once the application's dispatcher is available.</summary>
private static MauiDispatcherSequencer? _main;

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

/// <summary>Queues work and coalesces dispatcher drains.</summary>
private CoalescingDispatchState _dispatch;

Expand All @@ -27,6 +35,23 @@ public MauiDispatcherSequencer(IDispatcher dispatcher)
_dispatch = new(RunDrain, DefaultScheduler.Instance);
}

/// <summary>Gets the shared scheduler for the MAUI 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 application's dispatcher, from any thread. Until an application exists nothing is
/// cached, and the calling thread's existing dispatcher is used instead. Never falls back to the thread pool.
/// </remarks>
public static MauiDispatcherSequencer Main =>
Volatile.Read(ref _main) ?? BindMain(ref _main, ResolveApplicationDispatcher(IPlatformApplication.Current)) ?? 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 MauiDispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher());

/// <summary>Gets the dispatcher used to marshal work to the UI thread.</summary>
public IDispatcher Dispatcher { get; }

Expand All @@ -42,6 +67,35 @@ 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 MauiDispatcherSequencer? BindMain(ref MauiDispatcherSequencer? slot, IDispatcher? applicationDispatcher)
{
if (applicationDispatcher is null)
{
return null;
}

MauiDispatcherSequencer created = new(applicationDispatcher);
return Interlocked.CompareExchange(ref slot, created, null) ?? created;
}

/// <summary>Returns the application's dispatcher, which MAUI resolves on the UI thread while it starts the application.</summary>
/// <param name="application">The running platform application, or <see langword="null"/> before it exists.</param>
/// <returns>The application's dispatcher, or <see langword="null"/> when no application or dispatcher exists yet.</returns>
internal static IDispatcher? ResolveApplicationDispatcher(IPlatformApplication? application) =>
application?.Services?.GetService(typeof(IDispatcher)) as IDispatcher;

/// <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 IDispatcher ResolveCurrentDispatcher() =>
Microsoft.Maui.Dispatching.Dispatcher.GetForCurrentThread()
?? throw new InvalidOperationException(
"The calling thread has no MAUI dispatcher. Use MauiDispatcherSequencer.Main or Current from a UI thread, or after the application is created.");

/// <summary>Schedules delayed work through the dispatcher.</summary>
/// <param name="work">The callback to run.</param>
/// <param name="dueTime">The requested delay.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public sealed class MauiDispatcherSequencer : System.Reactive.Concurrency.LocalS
{
public MauiDispatcherSequencer(Microsoft.Maui.Dispatching.IDispatcher dispatcher) { }
public Microsoft.Maui.Dispatching.IDispatcher Dispatcher { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.MauiDispatcherSequencer Current { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.MauiDispatcherSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public sealed class MauiDispatcherSequencer : System.Reactive.Concurrency.LocalS
{
public MauiDispatcherSequencer(Microsoft.Maui.Dispatching.IDispatcher dispatcher) { }
public Microsoft.Maui.Dispatching.IDispatcher Dispatcher { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.MauiDispatcherSequencer Current { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.MauiDispatcherSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for full license information.

using System.Runtime.CompilerServices;
using Microsoft.Maui;
using Microsoft.Maui.Dispatching;
using ReactiveUI.Primitives.Advanced;

Expand All @@ -17,6 +18,13 @@ namespace ReactiveUI.Primitives.Concurrency;
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class MauiDispatcherSequencer : ISequencer
{
/// <summary>The shared main-thread sequencer, set once the application's dispatcher is available.</summary>
private static MauiDispatcherSequencer? _main;

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

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

Expand All @@ -29,6 +37,23 @@ public MauiDispatcherSequencer(IDispatcher dispatcher)
_state = new(this, Post, RunDrain, ScheduleDelayed);
}

/// <summary>Gets the shared sequencer for the MAUI 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 application's dispatcher, from any thread. Until an application exists nothing is
/// cached, and the calling thread's existing dispatcher is used instead. Never falls back to the thread pool.
/// </remarks>
public static MauiDispatcherSequencer Main =>
Volatile.Read(ref _main) ?? BindMain(ref _main, ResolveApplicationDispatcher(IPlatformApplication.Current)) ?? 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 MauiDispatcherSequencer Current => _current ??= new(ResolveCurrentDispatcher());

/// <summary>Gets the dispatcher used to marshal work to the UI thread.</summary>
public IDispatcher Dispatcher { get; }

Expand All @@ -51,6 +76,35 @@ public MauiDispatcherSequencer(IDispatcher dispatcher)
[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 MauiDispatcherSequencer? BindMain(ref MauiDispatcherSequencer? slot, IDispatcher? applicationDispatcher)
{
if (applicationDispatcher is null)
{
return null;
}

MauiDispatcherSequencer created = new(applicationDispatcher);
return Interlocked.CompareExchange(ref slot, created, null) ?? created;
}

/// <summary>Returns the application's dispatcher, which MAUI resolves on the UI thread while it starts the application.</summary>
/// <param name="application">The running platform application, or <see langword="null"/> before it exists.</param>
/// <returns>The application's dispatcher, or <see langword="null"/> when no application or dispatcher exists yet.</returns>
internal static IDispatcher? ResolveApplicationDispatcher(IPlatformApplication? application) =>
application?.Services?.GetService(typeof(IDispatcher)) as IDispatcher;

/// <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 IDispatcher ResolveCurrentDispatcher() =>
Microsoft.Maui.Dispatching.Dispatcher.GetForCurrentThread()
?? throw new InvalidOperationException(
"The calling thread has no MAUI dispatcher. Use MauiDispatcherSequencer.Main or Current from a UI thread, or after the application is created.");

/// <summary>Marshals the cached drain callback through the dispatcher.</summary>
/// <param name="drain">The drain callback.</param>
/// <returns><see langword="true"/> when the dispatcher accepted the work.</returns>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Primitives.Maui.Tests")]
namespace ReactiveUI.Primitives.Concurrency;

[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class MauiDispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISequencer
{
public MauiDispatcherSequencer(Microsoft.Maui.Dispatching.IDispatcher dispatcher) { }
public Microsoft.Maui.Dispatching.IDispatcher Dispatcher { get; }
public static ReactiveUI.Primitives.Concurrency.MauiDispatcherSequencer Current { get; }
public static ReactiveUI.Primitives.Concurrency.MauiDispatcherSequencer Main { get; }
public System.DateTimeOffset Now { get; }
public long Timestamp { get; }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Primitives.Maui.Tests")]
namespace ReactiveUI.Primitives.Concurrency;

[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class MauiDispatcherSequencer : ReactiveUI.Primitives.Concurrency.ISequencer
{
public MauiDispatcherSequencer(Microsoft.Maui.Dispatching.IDispatcher dispatcher) { }
public Microsoft.Maui.Dispatching.IDispatcher Dispatcher { get; }
public static ReactiveUI.Primitives.Concurrency.MauiDispatcherSequencer Current { get; }
public static ReactiveUI.Primitives.Concurrency.MauiDispatcherSequencer Main { get; }
public System.DateTimeOffset Now { get; }
public long Timestamp { get; }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@
<PackageReference Include="Microsoft.Extensions.Logging"/>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ReactiveUI.Primitives.Maui.Tests"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace ReactiveUI.Primitives.Reactive.Concurrency;
[System.Diagnostics.DebuggerDisplay("ControlSequencer: Control = {Control}")]
public sealed class ControlSequencer : LocalScheduler
{
/// <summary>The shared main-thread scheduler, set once a UI thread first reads it.</summary>
private static ControlSequencer? _main;

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

Expand Down Expand Up @@ -40,6 +43,17 @@ internal ControlSequencer(Control control, Func<Action, bool>? post, IScheduler?
Control.HandleCreated += OnHandleCreated;
}

/// <summary>Gets the shared scheduler for the Windows Forms main (UI) thread.</summary>
/// <exception cref="InvalidOperationException">Main is not bound yet and the calling thread is not an STA thread.</exception>
/// <remarks>
/// The first read binds Main to a hidden control whose handle belongs to the calling thread. Windows Forms UI
/// threads are always STA and thread-pool threads never are, so a read from any other thread throws and nothing is
/// cached. Read it first on the thread that runs the message loop, such as in <c>Main</c> before
/// <see cref="Application.Run()"/>.
/// </remarks>
public static ControlSequencer Main =>
Volatile.Read(ref _main) ?? BindMain(ref _main, Thread.CurrentThread.GetApartmentState());

/// <summary>Gets the control used to marshal work to the UI thread.</summary>
public Control Control { get; }

Expand All @@ -57,6 +71,32 @@ 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 a hidden control on the calling thread, keeping the first one bound.</summary>
/// <param name="slot">The field that holds the shared scheduler.</param>
/// <param name="apartment">The calling thread's apartment state.</param>
/// <returns>The shared scheduler.</returns>
/// <exception cref="InvalidOperationException"><paramref name="apartment"/> is not <see cref="ApartmentState.STA"/>.</exception>
internal static ControlSequencer BindMain(ref ControlSequencer? slot, ApartmentState apartment)
{
if (apartment != ApartmentState.STA)
{
throw new InvalidOperationException(
"The calling thread is not a Windows Forms UI thread. Read ControlSequencer.Main first on the STA thread that runs the message loop.");
}

Control control = new();
_ = control.Handle;
ControlSequencer created = new(control);
var bound = Interlocked.CompareExchange(ref slot, created, null);
if (bound is null)
{
return created;
}

control.Dispose();
return bound;
}

/// <summary>Posts work when the control is live and its handle is available.</summary>
/// <param name="control">The control whose disposal state is checked.</param>
/// <param name="handleCreated">Whether the control handle is available.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public sealed class ControlSequencer : System.Reactive.Concurrency.LocalSchedule
{
public ControlSequencer(System.Windows.Forms.Control control) { }
public System.Windows.Forms.Control Control { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.ControlSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public sealed class ControlSequencer : System.Reactive.Concurrency.LocalSchedule
{
public ControlSequencer(System.Windows.Forms.Control control) { }
public System.Windows.Forms.Control Control { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.ControlSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public sealed class ControlSequencer : System.Reactive.Concurrency.LocalSchedule
{
public ControlSequencer(System.Windows.Forms.Control control) { }
public System.Windows.Forms.Control Control { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.ControlSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public sealed class ControlSequencer : System.Reactive.Concurrency.LocalSchedule
{
public ControlSequencer(System.Windows.Forms.Control control) { }
public System.Windows.Forms.Control Control { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.ControlSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public sealed class ControlSequencer : System.Reactive.Concurrency.LocalSchedule
{
public ControlSequencer(System.Windows.Forms.Control control) { }
public System.Windows.Forms.Control Control { get; }
public static ReactiveUI.Primitives.Reactive.Concurrency.ControlSequencer Main { 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) { }
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
Expand Down
Loading
Loading