diff --git a/src/Avalonia.Base/Media/MediaContext.cs b/src/Avalonia.Base/Media/MediaContext.cs index d82144688d..943bf54149 100644 --- a/src/Avalonia.Base/Media/MediaContext.cs +++ b/src/Avalonia.Base/Media/MediaContext.cs @@ -91,7 +91,9 @@ internal partial class MediaContext : ICompositorScheduler if (_inputMarkerOp == null) { - _inputMarkerOp = _dispatcher.InvokeAsync(_inputMarkerHandler, DispatcherPriority.Input); + _inputMarkerOp = new DispatcherOperation(_dispatcher, DispatcherPriority.Input, _inputMarkerHandler, + throwOnUiThread: true, captureExecutionContext: false); + _dispatcher.InvokeAsyncImpl(_inputMarkerOp, CancellationToken.None); _inputMarkerAddedAt = _time.Elapsed; } else if (!now && (_time.Elapsed - _inputMarkerAddedAt).TotalSeconds > MaxSecondsWithoutInput) @@ -99,7 +101,7 @@ internal partial class MediaContext : ICompositorScheduler priority = DispatcherPriority.Input; } - var renderOp = new DispatcherOperation(_dispatcher, priority, _render, throwOnUiThread: true); + var renderOp = new DispatcherOperation(_dispatcher, priority, _render, throwOnUiThread: true, captureExecutionContext: false); _nextRenderOp = renderOp; _dispatcher.InvokeAsyncImpl(renderOp, CancellationToken.None); } diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index ea48fa31b0..1e19ccd196 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -44,18 +44,20 @@ public class DispatcherOperation private DispatcherPriority _priority; private readonly ExecutionContext? _executionContext; - internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread) : - this(dispatcher, priority, throwOnUiThread) + internal DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, Action callback, bool throwOnUiThread, + bool captureExecutionContext = true) : + this(dispatcher, priority, throwOnUiThread, captureExecutionContext) { Callback = callback; } - private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread) + private protected DispatcherOperation(Dispatcher dispatcher, DispatcherPriority priority, bool throwOnUiThread, + bool captureExecutionContext = true) { ThrowOnUiThread = throwOnUiThread; Priority = priority; Dispatcher = dispatcher; - _executionContext = ExecutionContext.Capture(); + _executionContext = captureExecutionContext ? ExecutionContext.Capture() : null; } internal string DebugDisplay diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index ffb94cc5d6..2efb648f4d 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Threading; using Avalonia.UnitTests; @@ -600,6 +601,19 @@ public partial class DispatcherTests public AsyncLocal AsyncLocalField { get; set; } = new AsyncLocal(); } + private sealed class AsyncLocalMeasureControl(Func getValue, Action setValue) : Control + { + public bool RecordMeasure { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + if (RecordMeasure) + setValue(getValue()); + + return new Size(1, 1); + } + } + [Fact] public async Task ExecutionContextIsPreservedInDispatcherInvokeAsync() { @@ -697,6 +711,76 @@ public partial class DispatcherTests Assert.Null(test); } + [Fact] + public void MediaContextRenderSchedulingDoesNotCaptureAmbientExecutionContext() + { + var impl = new SimpleDispatcherWithBackgroundProcessingImpl(); + using var services = new DispatcherServices(impl); + Dispatcher.InitializeUIThreadDispatcher(impl); + + var testObject = new AsyncLocalTestClass(); + string? test = "Not measured"; + var control = new AsyncLocalMeasureControl(() => testObject.AsyncLocalField.Value, value => test = value); + var root = new TestRoot { Child = control }; + + root.ExecuteInitialLayoutPass(); + control.RecordMeasure = true; + + Dispatcher.UIThread.Post(() => + { + testObject.AsyncLocalField.Value = "Initial Value"; + control.InvalidateMeasure(); + testObject.AsyncLocalField.Value = null; + }); + + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + + Assert.Null(test); + } + + [Fact] + public void MediaContextRenderSchedulingAllowsAlreadySuppressedExecutionContextFlow() + { + var impl = new SimpleDispatcherWithBackgroundProcessingImpl(); + using var services = new DispatcherServices(impl); + Dispatcher.InitializeUIThreadDispatcher(impl); + + var testObject = new AsyncLocalTestClass(); + string? test = "Not measured"; + Exception? schedulingException = null; + var control = new AsyncLocalMeasureControl(() => testObject.AsyncLocalField.Value, value => test = value); + var root = new TestRoot { Child = control }; + + root.ExecuteInitialLayoutPass(); + control.RecordMeasure = true; + + Dispatcher.UIThread.Post(() => + { + testObject.AsyncLocalField.Value = "Initial Value"; + + try + { + using (ExecutionContext.SuppressFlow()) + { + control.InvalidateMeasure(); + } + } + catch (Exception e) + { + schedulingException = e; + } + + testObject.AsyncLocalField.Value = null; + }); + + Assert.True(impl.AskedForSignal); + impl.ExecuteSignal(); + + Assert.Null(schedulingException); + Assert.Null(test); + } + [Fact] public async Task ExecutionContextCultureInfoIsPreservedInDispatcherInvokeAsync() {