diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 347e6b3b08..8d64cb2a82 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -109,6 +109,42 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action) + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0002 + M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0002 M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect}) @@ -187,6 +223,30 @@ baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll + + CP0007 + T:Avalonia.Threading.DispatcherPriorityAwaitable + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0007 + T:Avalonia.Threading.DispatcherPriorityAwaitable`1 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Threading.DispatcherPriorityAwaitable + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + + + CP0008 + T:Avalonia.Threading.DispatcherPriorityAwaitable`1 + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0009 T:Avalonia.Diagnostics.StyleDiagnostics diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index 324a50e4b4..afee481252 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -672,4 +672,70 @@ public partial class Dispatcher /// public DispatcherPriorityAwaitable AwaitWithPriority(Task task, DispatcherPriority priority) => new(this, task, priority); + + /// + /// Creates an awaitable object that asynchronously resumes execution on the dispatcher. + /// + /// + /// An awaitable object that asynchronously resumes execution on the dispatcher. + /// + /// + /// This method is equivalent to calling the method + /// and passing in . + /// + public DispatcherPriorityAwaitable Resume() => + Resume(DispatcherPriority.Background); + + /// `` + /// Creates an awaitable object that asynchronously resumes execution on the dispatcher. The work that occurs + /// when control returns to the code awaiting the result of this method is scheduled with the specified priority. + /// + /// The priority at which to schedule the continuation. + /// + /// An awaitable object that asynchronously resumes execution on the dispatcher. + /// + public DispatcherPriorityAwaitable Resume(DispatcherPriority priority) + { + DispatcherPriority.Validate(priority, nameof(priority)); + return new(this, null, priority); + } + + /// + /// Creates an awaitable object that asynchronously yields control back to the current dispatcher + /// and provides an opportunity for the dispatcher to process other events. + /// + /// + /// An awaitable object that asynchronously yields control back to the current dispatcher + /// and provides an opportunity for the dispatcher to process other events. + /// + /// + /// This method is equivalent to calling the method + /// and passing in . + /// + /// + /// The current thread is not the UI thread. + /// + public static DispatcherPriorityAwaitable Yield() => + Yield(DispatcherPriority.Background); + + /// + /// Creates an cawaitable object that asynchronously yields control back to the current dispatcher + /// and provides an opportunity for the dispatcher to process other events. The work that occurs when + /// control returns to the code awaiting the result of this method is scheduled with the specified priority. + /// + /// The priority at which to schedule the continuation. + /// + /// An awaitable object that asynchronously yields control back to the current dispatcher + /// and provides an opportunity for the dispatcher to process other events. + /// + /// + /// The current thread is not the UI thread. + /// + public static DispatcherPriorityAwaitable Yield(DispatcherPriority priority) + { + // TODO12: Update to use Dispatcher.CurrentDispatcher once multi-dispatcher support is merged + var current = UIThread; + current.VerifyAccess(); + return UIThread.Resume(priority); + } } diff --git a/src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs b/src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs index 456e2d7551..ab4fb38b5a 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs @@ -1,40 +1,130 @@ using System; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace Avalonia.Threading; -public class DispatcherPriorityAwaitable : INotifyCompletion +/// +/// A simple awaitable type that will return a DispatcherPriorityAwaiter. +/// +public struct DispatcherPriorityAwaitable { private readonly Dispatcher _dispatcher; - private protected readonly Task Task; + private readonly Task? _task; private readonly DispatcherPriority _priority; - internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority) + internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task? task, DispatcherPriority priority) { _dispatcher = dispatcher; - Task = task; + _task = task; _priority = priority; } - - public void OnCompleted(Action continuation) => - Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority)); - public bool IsCompleted => Task.IsCompleted; + public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority); +} + +/// +/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority. +/// +/// +/// This is returned from DispatcherPriorityAwaitable.GetAwaiter() +/// +public struct DispatcherPriorityAwaiter : INotifyCompletion +{ + private readonly Dispatcher _dispatcher; + private readonly Task? _task; + private readonly DispatcherPriority _priority; + + internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task? task, DispatcherPriority priority) + { + _dispatcher = dispatcher; + _task = task; + _priority = priority; + } + + public void OnCompleted(Action continuation) + { + if(_task == null || _task.IsCompleted) + _dispatcher.Post(continuation, _priority); + else + { + var self = this; + _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => + { + self._dispatcher.Post(continuation, self._priority); + }); + } + } + + /// + /// This always returns false since continuation is requested to be queued to a dispatcher queue + /// + public bool IsCompleted => false; + + public void GetResult() + { + if (_task != null) + _task.GetAwaiter().GetResult(); + } +} + +/// +/// A simple awaitable type that will return a DispatcherPriorityAwaiter<T>. +/// +public struct DispatcherPriorityAwaitable +{ + private readonly Dispatcher _dispatcher; + private readonly Task _task; + private readonly DispatcherPriority _priority; - public void GetResult() => Task.GetAwaiter().GetResult(); + internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority) + { + _dispatcher = dispatcher; + _task = task; + _priority = priority; + } - public DispatcherPriorityAwaitable GetAwaiter() => this; + public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority); } -public sealed class DispatcherPriorityAwaitable : DispatcherPriorityAwaitable +/// +/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority. +/// +/// +/// This is returned from DispatcherPriorityAwaitable<T>.GetAwaiter() +/// +public struct DispatcherPriorityAwaiter : INotifyCompletion { - internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority) : base( - dispatcher, task, priority) + private readonly Dispatcher _dispatcher; + private readonly Task _task; + private readonly DispatcherPriority _priority; + + internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task task, DispatcherPriority priority) { + _dispatcher = dispatcher; + _task = task; + _priority = priority; } - public new T GetResult() => ((Task)Task).GetAwaiter().GetResult(); + public void OnCompleted(Action continuation) + { + if(_task.IsCompleted) + _dispatcher.Post(continuation, _priority); + else + { + var self = this; + _task.ConfigureAwait(false).GetAwaiter().OnCompleted(() => + { + self._dispatcher.Post(continuation, self._priority); + }); + } + } + + /// + /// This always returns false since continuation is requested to be queued to a dispatcher queue + /// + public bool IsCompleted => false; - public new DispatcherPriorityAwaitable GetAwaiter() => this; -} + public void GetResult() => _task.GetAwaiter().GetResult(); +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index a667057708..1884a1ab65 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -505,4 +505,104 @@ public partial class DispatcherTests t.GetAwaiter().GetResult(); } } + + + [Fact] + public async Task DispatcherResumeContinuesOnUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + + await Dispatcher.UIThread.Resume(); + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task DispatcherYieldContinuesOnUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Dispatcher.Yield(); + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task DispatcherYieldThrowsOnNonUIThread() + { + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + await Assert.ThrowsAsync(async () => await Dispatcher.Yield()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } + + [Fact] + public async Task AwaitWithPriorityRunsOnUIThread() + { + static async Task Workload() + { + await Task.Delay(1).ConfigureAwait(false); + Assert.False(Dispatcher.UIThread.CheckAccess()); + + return Thread.CurrentThread.ManagedThreadId; + } + + using var services = new DispatcherServices(new SimpleControlledDispatcherImpl()); + + var tokenSource = new CancellationTokenSource(); + var workload = Dispatcher.UIThread.InvokeAsync( + async () => + { + Assert.True(Dispatcher.UIThread.CheckAccess()); + Task taskWithoutResult = Workload(); + + await Dispatcher.UIThread.AwaitWithPriority(taskWithoutResult, DispatcherPriority.Default); + + Assert.True(Dispatcher.UIThread.CheckAccess()); + Task taskWithResult = Workload(); + + await Dispatcher.UIThread.AwaitWithPriority(taskWithResult, DispatcherPriority.Default); + + Assert.True(Dispatcher.UIThread.CheckAccess()); + + tokenSource.Cancel(); + }); + + Dispatcher.UIThread.MainLoop(tokenSource.Token); + } }