diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
index 699186868a..bb1663eac0 100644
--- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
+++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
@@ -248,11 +248,11 @@ public partial class Dispatcher
/// An operation representing the queued delegate to be invoked.
///
///
- /// Note that the default priority is DispatcherPriority.Normal.
+ /// Note that the default priority is DispatcherPriority.Default.
///
public DispatcherOperation InvokeAsync(Action callback)
{
- return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None);
+ return InvokeAsync(callback, default, CancellationToken.None);
}
///
@@ -326,11 +326,11 @@ public partial class Dispatcher
/// An operation representing the queued delegate to be invoked.
///
///
- /// Note that the default priority is DispatcherPriority.Normal.
+ /// Note that the default priority is DispatcherPriority.Default.
///
public DispatcherOperation InvokeAsync(Func callback)
{
- return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None);
+ return InvokeAsync(callback, DispatcherPriority.Default, CancellationToken.None);
}
///
@@ -541,6 +541,18 @@ public partial class Dispatcher
InvokeAsyncImpl(new DispatcherOperation(this, priority, action, true), CancellationToken.None);
}
+ ///
+ /// Executes the specified Func<Task> asynchronously on the
+ /// thread that the Dispatcher was created on
+ ///
+ ///
+ /// A Func<Task> delegate to invoke through the dispatcher.
+ ///
+ ///
+ /// An task that completes after the task returned from callback finishes.
+ ///
+ public Task InvokeAsync(Func callback) => InvokeAsync(callback, DispatcherPriority.Default);
+
///
/// Executes the specified Func<Task> asynchronously on the
/// thread that the Dispatcher was created on
@@ -556,11 +568,29 @@ public partial class Dispatcher
///
/// An task that completes after the task returned from callback finishes
///
- public Task InvokeAsync(Func callback, DispatcherPriority priority = default)
+ public Task InvokeAsync(Func callback, DispatcherPriority priority)
{
_ = callback ?? throw new ArgumentNullException(nameof(callback));
return InvokeAsync(callback, priority).GetTask().Unwrap();
}
+
+ ///
+ /// Executes the specified Func<Task<TResult>> asynchronously on the
+ /// thread that the Dispatcher was created on
+ ///
+ ///
+ /// A Func<Task<TResult>> delegate to invoke through the dispatcher.
+ ///
+ ///
+ /// The priority that determines in what order the specified
+ /// callback is invoked relative to the other pending operations
+ /// in the Dispatcher.
+ ///
+ ///
+ /// An task that completes after the task returned from callback finishes
+ ///
+ public Task InvokeAsync(Func> action) =>
+ InvokeAsync(action, DispatcherPriority.Default);
///
/// Executes the specified Func<Task<TResult>> asynchronously on the
@@ -577,7 +607,7 @@ public partial class Dispatcher
///
/// An task that completes after the task returned from callback finishes
///
- public Task InvokeAsync(Func> action, DispatcherPriority priority = default)
+ public Task InvokeAsync(Func> action, DispatcherPriority priority)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
return InvokeAsync>(action, priority).GetTask().Unwrap();
diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs
index 1f8974dfa3..e826432475 100644
--- a/src/Avalonia.Base/Threading/DispatcherFrame.cs
+++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs
@@ -91,31 +91,44 @@ public class DispatcherFrame
internal void Run(IControlledDispatcherImpl impl)
{
- // Since the actual platform run loop is controlled by a Cancellation token, we are restarting
- // it if frame still needs to run
- while (Continue)
- RunCore(impl);
- }
-
- private void RunCore(IControlledDispatcherImpl impl)
- {
- if (_isRunning)
- throw new InvalidOperationException("This frame is already running");
- _isRunning = true;
- try
- {
- _cancellationTokenSource = new CancellationTokenSource();
- // Wake up the dispatcher in case it has pending jobs
- Dispatcher.RequestProcessing();
- impl.RunLoop(_cancellationTokenSource.Token);
- }
- finally
+ Dispatcher.VerifyAccess();
+
+ // Since the actual platform run loop is controlled by a Cancellation token, we have an
+ // outer loop that restarts the platform one in case Continue was set to true after being set to false
+ while (true)
{
- _isRunning = false;
- _cancellationTokenSource?.Cancel();
- _cancellationTokenSource = null;
+ // Take the instance lock since `Continue` is changed from one too
+ lock (Dispatcher.InstanceLock)
+ {
+ if (!Continue)
+ return;
+
+ if (_isRunning)
+ throw new InvalidOperationException("This frame is already running");
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _isRunning = true;
+ }
+
+ try
+ {
+ // Wake up the dispatcher in case it has pending jobs
+ Dispatcher.RequestProcessing();
+ impl.RunLoop(_cancellationTokenSource.Token);
+ }
+ finally
+ {
+ lock (Dispatcher.InstanceLock)
+ {
+ _isRunning = false;
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource?.Dispose();
+ _cancellationTokenSource = null;
+ }
+ }
}
}
+
internal void MaybeExitOnDispatcherRequest()
{
diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
index 20aa91c83e..fdc098777a 100644
--- a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
+++ b/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs
@@ -58,6 +58,10 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
public void RunLoop(CancellationToken token)
{
+ CancellationTokenRegistration registration = default;
+ if (token.CanBeCanceled)
+ registration = token.Register(() => _wakeup.Set());
+
while (!token.IsCancellationRequested)
{
bool signaled;
@@ -105,5 +109,7 @@ public class ManagedDispatcherImpl : IControlledDispatcherImpl
else
_wakeup.WaitOne();
}
+
+ registration.Dispose();
}
}
\ No newline at end of file
diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs
index 9ba3f3980d..7b401918ce 100644
--- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs
+++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
using Avalonia.Threading;
using Avalonia.Utilities;
using Xunit;
@@ -458,4 +460,46 @@ public class DispatcherTests
}
}
+ [Fact]
+ public void DispatcherInvokeAsyncUnwrapsTasks()
+ {
+ int asyncMethodStage = 0;
+
+ async Task AsyncMethod()
+ {
+ asyncMethodStage = 1;
+ await Task.Delay(200);
+ asyncMethodStage = 2;
+ }
+
+ async Task AsyncMethodWithResult()
+ {
+ await Task.Delay(100);
+ return 1;
+ }
+
+ async Task Test()
+ {
+ await Dispatcher.UIThread.InvokeAsync(AsyncMethod);
+ Assert.Equal(2, asyncMethodStage);
+ Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult));
+ asyncMethodStage = 0;
+
+ await Dispatcher.UIThread.InvokeAsync(AsyncMethod, DispatcherPriority.Default);
+ Assert.Equal(2, asyncMethodStage);
+ Assert.Equal(1, await Dispatcher.UIThread.InvokeAsync(AsyncMethodWithResult, DispatcherPriority.Default));
+
+ Dispatcher.UIThread.ExitAllFrames();
+ }
+
+ using (new DispatcherServices(new ManagedDispatcherImpl(null)))
+ {
+ var t = Test();
+ var cts = new CancellationTokenSource();
+ Task.Delay(3000).ContinueWith(_ => cts.Cancel());
+ Dispatcher.UIThread.MainLoop(cts.Token);
+ Assert.True(t.IsCompletedSuccessfully);
+ t.GetAwaiter().GetResult();
+ }
+ }
}
\ No newline at end of file