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
79 changes: 50 additions & 29 deletions README.md

Large diffs are not rendered by default.

47 changes: 32 additions & 15 deletions Skill.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<LibraryTargetFrameworks>$(NetTargetFrameworks);$(NetFrameworkTargetFrameworks)</LibraryTargetFrameworks>
<WindowsNetTargetFrameworks>net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows</WindowsNetTargetFrameworks>
<WindowsLibraryTargetFrameworks>$(WindowsNetTargetFrameworks);$(NetFrameworkTargetFrameworks)</WindowsLibraryTargetFrameworks>
<AvaloniaTargetFrameworks>$(NetTargetFrameworks)</AvaloniaTargetFrameworks>
<BlazorTargetFrameworks>$(NetTargetFrameworks)</BlazorTargetFrameworks>
<!-- MAUI 9 left support May 2026; MAUI 10 is the only supported release until 11 GAs. -->
<MauiTargetFrameworks>net10.0;net11.0</MauiTargetFrameworks>
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<!-- StyleSharp.Analyzers, PerformanceSharp.Analyzers and SecuritySharp.Analyzers ship from the
same release pipeline and always share a version. -->
<RoslynCommonAnalyzersVersion>3.38.1</RoslynCommonAnalyzersVersion>
<AvaloniaVersion>12.1.0</AvaloniaVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301"/>
Expand All @@ -31,6 +32,8 @@
<PackageVersion Include="ReactiveUI.Extensions" Version="4.0.0"/>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)"/>
<PackageVersion Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.29" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.18" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.10" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net11.0'))"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Reactive.Disposables;
using Avalonia.Threading;

namespace ReactiveUI.Primitives.Reactive.Concurrency;

/// <summary>Avalonia UI-thread scheduler that coalesces scheduled work onto a dispatcher drain.</summary>
/// <seealso cref="System.Reactive.Concurrency.IScheduler" />
public sealed class AvaloniaScheduler : CoalescingDispatchScheduler
{
/// <summary>Gets the shared scheduler for <see cref="Dispatcher.UIThread"/>.</summary>
public static readonly AvaloniaScheduler Instance =
new(Dispatcher.UIThread, DispatcherPriority.Background);

/// <summary>Initializes a new instance of the <see cref="AvaloniaScheduler"/> class.</summary>
/// <param name="dispatcher">The dispatcher used to marshal work to the UI thread.</param>
/// <exception cref="ArgumentNullException"><paramref name="dispatcher"/> is <see langword="null"/>.</exception>
public AvaloniaScheduler(Dispatcher dispatcher)
: this(dispatcher, DispatcherPriority.Background)
{
}

/// <summary>Initializes a new instance of the <see cref="AvaloniaScheduler"/> class.</summary>
/// <param name="dispatcher">The dispatcher used to marshal work to the UI thread.</param>
/// <param name="priority">Dispatcher priority used for posted drains and delayed work.</param>
/// <exception cref="ArgumentNullException"><paramref name="dispatcher"/> is <see langword="null"/>.</exception>
public AvaloniaScheduler(Dispatcher dispatcher, DispatcherPriority priority)
{
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Priority = priority;
}

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

/// <summary>Gets the dispatcher priority used for posted drains and delayed work.</summary>
public DispatcherPriority Priority { get; }

/// <inheritdoc/>
protected override bool Post(Action drain)
{
Dispatcher.Post(drain, Priority);
return true;
}

/// <inheritdoc/>
protected override IDisposable ScheduleOnDispatcher(Action work, TimeSpan dueTime)
{
DispatcherTimer timer = new(Priority, Dispatcher) { Interval = dueTime };
timer.Tick += (_, _) =>
{
timer.Stop();
work();
};
timer.Start();
return Disposable.Create(timer, static value => value.Stop());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void
abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.TimeSpan dueTime, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler!
virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void
abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.TimeSpan dueTime, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler!
virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void
abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.TimeSpan dueTime, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler!
virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void
ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void
abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule<TState>(TState state, System.TimeSpan dueTime, System.Func<System.Reactive.Concurrency.IScheduler!, TState, System.IDisposable!>! action) -> System.IDisposable!
static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler!
virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- System.Reactive-flavoured Avalonia leaf: a coalescing IScheduler over the Avalonia dispatcher.
REACTIVE_SHIM + the System.Reactive dependency come from Directory.Build.props via the name suffix. -->
<TargetFrameworks>$(AvaloniaTargetFrameworks)</TargetFrameworks>
<PackageDescription>Avalonia UI-thread scheduler for ReactiveUI.Primitives, recompiled against System.Reactive's IScheduler.</PackageDescription>
<PackageTags>system.reactive;rx;reactive;primitives;avalonia;dispatcher;scheduler</PackageTags>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Primitives.Platform.Reactive.Shared\**\*.cs" Link="Primitives.Platform.Reactive.Shared\%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia"/>
<ProjectReference Include="..\ReactiveUI.Primitives.Reactive\ReactiveUI.Primitives.Reactive.csproj"/>
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Avalonia.Threading;
using ReactiveUI.Primitives.Advanced;

namespace ReactiveUI.Primitives.Concurrency;

/// <summary>Avalonia UI-thread scheduler that coalesces scheduled work onto a dispatcher drain.</summary>
/// <seealso cref="ISequencer" />
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class AvaloniaScheduler : ISequencer
{
/// <summary>Gets the shared scheduler for <see cref="Dispatcher.UIThread"/>.</summary>
public static readonly AvaloniaScheduler Instance =
new(Dispatcher.UIThread, DispatcherPriority.Background);

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

/// <summary>Initializes a new instance of the <see cref="AvaloniaScheduler"/> class.</summary>
/// <param name="dispatcher">The dispatcher used to marshal work to the UI thread.</param>
/// <exception cref="ArgumentNullException"><paramref name="dispatcher"/> is <see langword="null"/>.</exception>
public AvaloniaScheduler(Dispatcher dispatcher)
: this(dispatcher, DispatcherPriority.Background)
{
}

/// <summary>Initializes a new instance of the <see cref="AvaloniaScheduler"/> class.</summary>
/// <param name="dispatcher">The dispatcher used to marshal work to the UI thread.</param>
/// <param name="priority">Dispatcher priority used for posted drains and delayed work.</param>
/// <exception cref="ArgumentNullException"><paramref name="dispatcher"/> is <see langword="null"/>.</exception>
public AvaloniaScheduler(Dispatcher dispatcher, DispatcherPriority priority)
{
Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
Priority = priority;
_state = new(this, Post, RunDrain, ScheduleDelayed);
}

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

/// <summary>Gets the dispatcher priority used for posted drains and delayed work.</summary>
public DispatcherPriority Priority { get; }

/// <inheritdoc/>
public DateTimeOffset Now => DispatchSequencerState.Now;

/// <inheritdoc/>
public long Timestamp => DispatchSequencerState.Timestamp;

/// <summary>Gets the debugger display text.</summary>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
private string DebuggerDisplay => ToString() ?? string.Empty;

/// <inheritdoc/>
public void Schedule(IWorkItem item) => _state.Schedule(item);

/// <inheritdoc/>
public void Schedule(IWorkItem item, long dueTimestamp) => _state.Schedule(item, dueTimestamp);

/// <summary>Marshals the cached drain callback onto the dispatcher.</summary>
/// <param name="drain">The drain callback.</param>
/// <returns><see langword="true"/>, since the dispatcher accepts the work.</returns>
private bool Post(Action drain)
{
Dispatcher.Post(drain, Priority);
return true;
}

/// <summary>Runs delayed work on a dispatcher timer bound to the selected dispatcher.</summary>
/// <param name="item">Work item to execute at the due time.</param>
/// <param name="dueTimestamp">Absolute monotonic timestamp at which to execute the item.</param>
private void ScheduleDelayed(IWorkItem item, long dueTimestamp)
{
DispatcherTimer timer =
new(Priority, Dispatcher) { Interval = DispatchSequencerState.DelayUntil(dueTimestamp) };
timer.Tick += (_, _) =>
{
timer.Stop();
DispatchSequencerState.RunIfActive(item);
};
timer.Start();
}

/// <summary>Forwards the cached drain callback to the engine.</summary>
private void RunDrain() => _state.RunDrain();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long
static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher!
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void
ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long
static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Loading
Loading