From 12f7e96d72d90df5bdd5c31c40bf611dd60c6eb9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 2 Mar 2026 19:11:39 +0500 Subject: [PATCH] Introduce Dispatcher.CurrentDispatcher, Dispatcher.FromThread, AvaloniaObject.Dispatcher (#18686) * Introduce Dispatcher.CurrentDispatcher, FromThread, AvaloniaObject.Dispatcher, allow dispatcher usage before avalonia initialization, auto-create valid dispatchers for new threads. * Remove async context usages, avoid scope splits * Create InvalidTheme per dispatcher * Update src/Avalonia.Base/Threading/Dispatcher.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * netstandard isn't there anymore * Leftover code Removed dispatcher mock setup and test services initialization. * Remove dead code * Dispatcher provides its own time provider * Clean InputManager between tests * Update API suppressions --------- Co-authored-by: Julien Lebosquain Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api/Avalonia.nupkg.xml | 12 ++ .../Avalonia.Android/AndroidPlatform.cs | 2 +- src/Avalonia.Base/AvaloniaObject.cs | 13 +- src/Avalonia.Base/Input/InputManager.cs | 9 +- .../Platform/ManagedDispatcherImpl.cs | 0 .../Transport/BatchStreamArrayPool.cs | 5 +- src/Avalonia.Base/StyledElement.cs | 13 +- .../Threading/Dispatcher.Queue.cs | 10 +- .../Threading/Dispatcher.ThreadStorage.cs | 95 +++++++++++ .../Threading/Dispatcher.Timers.cs | 8 +- src/Avalonia.Base/Threading/Dispatcher.cs | 129 ++++++++++----- .../Threading/IDispatcherImpl.cs | 31 +--- .../Remote/PreviewerWindowingPlatform.cs | 1 - src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/CallbackBase.cs | 3 +- src/Avalonia.X11/X11Platform.cs | 11 +- src/Avalonia.X11/XEmbedPlug.cs | 14 +- .../Avalonia.Browser/WindowingPlatform.cs | 8 +- .../AvaloniaHeadlessPlatform.cs | 1 - .../LinuxFramebufferPlatform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 3 +- src/iOS/Avalonia.iOS/Platform.cs | 2 +- .../AvaloniaObjectTests_Binding.cs | 119 +++++--------- .../AvaloniaObjectTests_Direct.cs | 24 +-- .../AvaloniaObjectTests_Threading.cs | 88 +++------- .../DispatcherTests.Exception.cs | 155 +++++++++--------- .../DispatcherTests.cs | 35 ++-- .../Input/MouseDeviceTests.cs | 8 +- .../Input/TouchDeviceTests.cs | 2 +- .../DesktopStyleApplicationLifetimeTests.cs | 6 +- .../Primitives/UniformGridTests.cs | 3 +- .../Avalonia.LeakTests/AvaloniaObjectTests.cs | 2 +- .../Data/BindingTests_Delay.cs | 3 +- ...onObserverBuilderTests_AttachedProperty.cs | 7 +- ...onObserverBuilderTests_AvaloniaProperty.cs | 1 - .../Avalonia.Markup.Xaml.UnitTests.csproj | 10 ++ .../Avalonia.RenderTests/TestRenderHelper.cs | 34 +--- tests/Avalonia.UnitTests/TestServices.cs | 24 +-- tests/Avalonia.UnitTests/ThreadRunHelper.cs | 2 - .../Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 40 files changed, 474 insertions(+), 425 deletions(-) rename src/{Avalonia.Controls => Avalonia.Base}/Platform/ManagedDispatcherImpl.cs (100%) create mode 100644 src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index b729164b3b..fbf4000a79 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -307,6 +307,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll + current/Avalonia/lib/net10.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Primitives.ChromeOverlayLayer @@ -697,6 +703,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll + + CP0001 + T:Avalonia.Controls.Platform.ManagedDispatcherImpl + baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll + current/Avalonia/lib/net8.0/Avalonia.Controls.dll + CP0001 T:Avalonia.Controls.Primitives.ChromeOverlayLayer diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 5316a84570..7a3059cb65 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -81,12 +81,12 @@ namespace Avalonia.Android { Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); + Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl()); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(new AndroidDispatcherImpl()) .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToSingleton() diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 3303ed276e..3ad488b615 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -34,7 +34,6 @@ namespace Avalonia /// public AvaloniaObject() { - VerifyAccess(); _values = new ValueStore(this); } @@ -109,16 +108,22 @@ namespace Avalonia /// internal string DebugDisplay => GetDebugDisplay(true); + /// + /// Returns the that this + /// is associated with. + /// + public Dispatcher Dispatcher { get; } = Dispatcher.CurrentDispatcher; + /// /// Returns a value indicating whether the current thread is the UI thread. /// /// true if the current thread is the UI thread; otherwise false. - public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); - + public bool CheckAccess() => Dispatcher.CheckAccess(); + /// /// Checks that the current thread is the UI thread and throws if not. /// - public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); + public void VerifyAccess() => Dispatcher.VerifyAccess(); /// /// Clears a 's local value. diff --git a/src/Avalonia.Base/Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs index c9b1751b2a..7f5b5f82e7 100644 --- a/src/Avalonia.Base/Input/InputManager.cs +++ b/src/Avalonia.Base/Input/InputManager.cs @@ -8,7 +8,7 @@ namespace Avalonia.Input /// Receives input from the windowing subsystem and dispatches it to interested parties /// for processing. /// - internal class InputManager : IInputManager + internal class InputManager : IInputManager, IDisposable { private readonly LightweightSubject _preProcess = new(); private readonly LightweightSubject _process = new(); @@ -36,5 +36,12 @@ namespace Avalonia.Input _process.OnNext(e); _postProcess.OnNext(e); } + + public void Dispose() + { + _preProcess.OnCompleted(); + _process.OnCompleted(); + _postProcess.OnCompleted(); + } } } diff --git a/src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs b/src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs similarity index 100% rename from src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs rename to src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs index 6aabb4d168..7e1c9e711f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Avalonia.Platform; using Avalonia.Threading; @@ -30,9 +31,7 @@ internal abstract class BatchStreamPoolBase : IDisposable var updateRef = new WeakReference>(this); if ( reclaimImmediately - || ( - AvaloniaLocator.Current.GetService() == null - && AvaloniaLocator.Current.GetService() == null)) + || Dispatcher.FromThread(Thread.CurrentThread) == null) _reclaimImmediately = true; else StartUpdateTimer(startTimer, updateRef); diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d1b960390c..f511d1de2a 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -76,7 +76,7 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); + [ThreadStatic] private static ControlTheme? s_invalidTheme; private int _initCount; private string? _name; private Classes? _classes; @@ -332,6 +332,9 @@ namespace Avalonia /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + internal static ControlTheme InvalidTheme + => s_invalidTheme ??= new(); + /// public virtual void BeginInit() { @@ -666,10 +669,10 @@ namespace Avalonia if (this.TryFindResource(key, out var value) && value is ControlTheme t) _implicitTheme = t; else - _implicitTheme = s_invalidTheme; + _implicitTheme = InvalidTheme; } - if (_implicitTheme != s_invalidTheme) + if (_implicitTheme != InvalidTheme) return _implicitTheme; return null; @@ -828,11 +831,11 @@ namespace Avalonia return; // Refetch the implicit theme. - var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var oldImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; _implicitTheme = null; GetEffectiveTheme(); - var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + var newImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme; // If the implicit theme has changed, detach the existing theme. if (newImplicitTheme != oldImplicitTheme) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 954183ffcc..09dd9f27ec 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -12,7 +12,7 @@ public partial class Dispatcher private bool _explicitBackgroundProcessingRequested; private const int MaximumInputStarvationTimeInFallbackMode = 50; private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; - private readonly int _maximumInputStarvationTime; + private int _maximumInputStarvationTime; void RequestBackgroundProcessing() { @@ -101,9 +101,9 @@ public partial class Dispatcher internal static void ResetBeforeUnitTests() { - s_uiThread = null; + ResetGlobalState(); } - + internal static void ResetForUnitTests() { if (s_uiThread == null) @@ -122,14 +122,14 @@ public partial class Dispatcher if (job == null || job.Priority <= DispatcherPriority.Inactive) { s_uiThread.ShutdownImpl(); - s_uiThread = null; + ResetGlobalState(); return; } s_uiThread.ExecuteJob(job); } - } + private void ExecuteJob(DispatcherOperation job) { diff --git a/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs new file mode 100644 index 0000000000..7d9d0b39cf --- /dev/null +++ b/src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Threading; + +public partial class Dispatcher +{ + [ThreadStatic] + private static DispatcherReferenceStorage? s_currentThreadDispatcher; + private static readonly object s_globalLock = new(); + private static readonly ConditionalWeakTable s_dispatchers = new(); + + private static Dispatcher? s_uiThread; + + // This class is needed PURELY for ResetForUnitTests, so we can reset s_currentThreadDispatcher for all threads + class DispatcherReferenceStorage + { + public WeakReference Reference = new(null!); + } + + public static Dispatcher CurrentDispatcher + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (s_currentThreadDispatcher?.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + + return new Dispatcher(null); + } + } + + public static Dispatcher? FromThread(Thread thread) + { + lock (s_globalLock) + { + if (s_dispatchers.TryGetValue(thread, out var reference) && reference.Reference.TryGetTarget(out var dispatcher) == true) + return dispatcher; + return null; + } + } + + public static Dispatcher UIThread + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + static Dispatcher GetUIThreadDispatcherSlow() + { + lock (s_globalLock) + { + return s_uiThread ?? CurrentDispatcher; + } + } + return s_uiThread ?? GetUIThreadDispatcherSlow(); + } + } + + internal static Dispatcher? TryGetUIThread() + { + lock (s_globalLock) + return s_uiThread; + } + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IPlatformThreadingInterface impl) => + InitializeUIThreadDispatcher(new LegacyDispatcherImpl(impl)); + + [PrivateApi] + public static void InitializeUIThreadDispatcher(IDispatcherImpl impl) + { + UIThread.VerifyAccess(); + if (UIThread._initialized) + throw new InvalidOperationException("UI thread dispatcher is already initialized"); + UIThread.ReplaceImplementation(impl); + } + + private static void ResetGlobalState() + { + lock (s_globalLock) + { + foreach (var store in s_dispatchers) + store.Value.Reference = new(null!); + s_dispatchers.Clear(); + + s_currentThreadDispatcher = null; + s_uiThread = null; + } + } +} diff --git a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs index ce16820286..9966a156d2 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Timers.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Timers.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Avalonia.Threading; @@ -15,7 +16,8 @@ public partial class Dispatcher private long? _dueTimeForBackgroundProcessing; private long? _osTimerSetTo; - internal long Now => _impl.Now; + private readonly Func _timeProvider; + internal long Now => _timeProvider(); private void UpdateOSTimer() { @@ -26,6 +28,7 @@ public partial class Dispatcher _dueTimeForTimers ?? _dueTimeForBackgroundProcessing; if (_osTimerSetTo == nextDueTime) return; + _impl.UpdateTimer(_osTimerSetTo = nextDueTime); } @@ -114,7 +117,8 @@ public partial class Dispatcher bool needToProcessQueue = false; lock (InstanceLock) { - _impl.UpdateTimer(_osTimerSetTo = null); + _impl.UpdateTimer(null); + _osTimerSetTo = null; needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; if (needToPromoteTimers) _dueTimeForTimers = null; diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 8253c2fed2..07582ac3f4 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -3,7 +3,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Threading; @@ -17,63 +20,60 @@ namespace Avalonia.Threading; public partial class Dispatcher : IDispatcher { private IDispatcherImpl _impl; + private bool _initialized; internal object InstanceLock { get; } = new(); private IControlledDispatcherImpl? _controlledImpl; - private static Dispatcher? s_uiThread; private IDispatcherImplWithPendingInput? _pendingInputImpl; - private readonly IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; + private readonly Thread _thread; private readonly AvaloniaSynchronizationContext?[] _priorityContexts = new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1]; - internal Dispatcher(IDispatcherImpl impl) + internal Dispatcher(IDispatcherImpl? impl) { - _impl = impl; - impl.Timer += OnOSTimer; - impl.Signaled += Signaled; - _controlledImpl = _impl as IControlledDispatcherImpl; - _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; - _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; - _maximumInputStarvationTime = _backgroundProcessingImpl == null ? - MaximumInputStarvationTimeInFallbackMode : - MaximumInputStarvationTimeInExplicitProcessingExplicitMode; - if (_backgroundProcessingImpl != null) - _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; - - _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); - _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); - } - - public static Dispatcher UIThread - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get +#if DEBUG + if (AvaloniaLocator.Current.GetService() != null + || AvaloniaLocator.Current.GetService() != null) + throw new InvalidOperationException( + "Registering IDispatcherImpl or IPlatformThreadingInterface via locator is no longer valid"); +#endif + lock (s_globalLock) { - return s_uiThread ??= CreateUIThreadDispatcher(); - } - } + _thread = Thread.CurrentThread; + if (FromThread(_thread) != null) + throw new InvalidOperationException("The current thread already has a dispatcher"); - public bool SupportsRunLoops => _controlledImpl != null; + // The first created dispatcher becomes "UI thread one" + s_uiThread ??= this; - [MethodImpl(MethodImplOptions.NoInlining)] - private static Dispatcher CreateUIThreadDispatcher() - { - var impl = AvaloniaLocator.Current.GetService(); - if (impl == null) + s_dispatchers.Remove(Thread.CurrentThread); + s_dispatchers.Add(Thread.CurrentThread, + s_currentThreadDispatcher = new() { Reference = new WeakReference(this) }); + } + + if (impl is null) { - var platformThreading = AvaloniaLocator.Current.GetService(); - if (platformThreading != null) - impl = new LegacyDispatcherImpl(platformThreading); - else - impl = new NullDispatcherImpl(); + var st = Stopwatch.StartNew(); + _timeProvider = () => st.ElapsedMilliseconds; } - return new Dispatcher(impl); + else + _timeProvider = () => impl.Now; + + _impl = null!; // Set by ReplaceImplementation + ReplaceImplementation(impl); + + + _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this); + _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this); } + public bool SupportsRunLoops => _controlledImpl != null; + /// /// Checks that the current thread is the UI thread. /// - public bool CheckAccess() => _impl.CurrentThreadIsLoopThread; + public bool CheckAccess() => Thread.CurrentThread == _thread; /// /// Checks that the current thread is the UI thread and throws if not. @@ -89,15 +89,64 @@ public partial class Dispatcher : IDispatcher [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] static void ThrowVerifyAccess() - => throw new InvalidOperationException("Call from invalid thread"); + => throw new InvalidOperationException("The calling thread cannot access this object because a different thread owns it."); ThrowVerifyAccess(); } } + public Thread Thread => _thread; + internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority) { DispatcherPriority.Validate(priority, nameof(priority)); var index = priority - DispatcherPriority.MinValue; return _priorityContexts[index] ??= new(this, priority); } + + [PrivateApi] + public IDispatcherImpl PlatformImpl => _impl; + + private void ReplaceImplementation(IDispatcherImpl? impl) + { + // TODO: Consider moving the helper out of Avalonia.Win32 so + // it's usable earlier + using var _ = NonPumpingLockHelper.Use(); + + + if (impl?.CurrentThreadIsLoopThread == false) + throw new InvalidOperationException("IDispatcherImpl belongs to a different thread"); + + if (_impl != null!) // Null in ctor + { + _impl.Timer -= OnOSTimer; + _impl.Signaled -= Signaled; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing -= OnReadyForExplicitBackgroundProcessing; + _impl = null!; + _controlledImpl = null; + _pendingInputImpl = null; + _backgroundProcessingImpl = null; + } + + if (impl != null) + _initialized = true; + else + impl = new ManagedDispatcherImpl(null); + _impl = impl; + + impl.Timer += OnOSTimer; + impl.Signaled += Signaled; + _controlledImpl = _impl as IControlledDispatcherImpl; + _pendingInputImpl = _impl as IDispatcherImplWithPendingInput; + _backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; + _maximumInputStarvationTime = _backgroundProcessingImpl == null ? + MaximumInputStarvationTimeInFallbackMode : + MaximumInputStarvationTimeInExplicitProcessingExplicitMode; + if (_backgroundProcessingImpl != null) + _backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing; + if (_signaled) + _impl.Signal(); + _osTimerSetTo = null; + UpdateOSTimer(); + } } diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index dd438b176e..f8d5cb8947 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -80,33 +80,4 @@ internal class LegacyDispatcherImpl : IDispatcherImpl _timer = null; Timer?.Invoke(); } -} - -internal sealed class NullDispatcherImpl : IDispatcherImpl -{ - public bool CurrentThreadIsLoopThread => true; - - public void Signal() - { - - } - - public event Action? Signaled - { - add { } - remove { } - } - - public event Action? Timer - { - add { } - remove { } - } - - public long Now => 0; - - public void UpdateTimer(long? dueTimeInMs) - { - - } -} +} \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index dcc24482c0..43eddb010d 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -55,7 +55,6 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToSingleton() - .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToConstant(new UiThreadRenderTimer(60)) .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 88d809e47d..40bc2ca71e 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -114,8 +114,8 @@ namespace Avalonia.Native var clipboardImpl = new ClipboardImpl(_factory.CreateClipboard()); var clipboard = new Clipboard(clipboardImpl); + Dispatcher.InitializeUIThreadDispatcher(new DispatcherImpl(_factory.CreatePlatformThreadingInterface())); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface())) .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToConstant(new ScreenImpl(_factory.CreateScreens)) .Bind().ToSingleton() diff --git a/src/Avalonia.Native/CallbackBase.cs b/src/Avalonia.Native/CallbackBase.cs index c5978e2a0d..04ca37b4b9 100644 --- a/src/Avalonia.Native/CallbackBase.cs +++ b/src/Avalonia.Native/CallbackBase.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.ExceptionServices; +using System.Threading; using Avalonia.MicroCom; using Avalonia.Platform; using Avalonia.Threading; @@ -11,7 +12,7 @@ namespace Avalonia.Native { public void RaiseException(Exception e) { - if (AvaloniaLocator.Current.GetService() is DispatcherImpl dispatcherImpl) + if(Dispatcher.FromThread(Thread.CurrentThread) is { PlatformImpl: DispatcherImpl dispatcherImpl }) { dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e)); } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e685ff4afb..ec8a915ea0 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -42,6 +42,7 @@ namespace Avalonia.X11 public X11Globals Globals { get; private set; } = null!; public XResources Resources { get; private set; } = null!; public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new(); + public IX11PlatformDispatcher DispatcherImpl { get; private set; } = null!; public void Initialize(X11PlatformOptions options) { @@ -79,10 +80,12 @@ namespace Avalonia.X11 var clipboard = new Input.Platform.Clipboard(clipboardImpl); AvaloniaLocator.CurrentMutable.BindToSelf(this) - .Bind().ToConstant(this) - .Bind().ToConstant(options.UseGLibMainLoop - ? new GlibDispatcherImpl(this) - : new X11PlatformThreading(this)) + .Bind().ToConstant(this); + DispatcherImpl = options.UseGLibMainLoop + ? new GlibDispatcherImpl(this) + : new X11PlatformThreading(this); + Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl); + AvaloniaLocator.CurrentMutable .Bind().ToConstant(timer) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) diff --git a/src/Avalonia.X11/XEmbedPlug.cs b/src/Avalonia.X11/XEmbedPlug.cs index f0e3df688e..3ea53f2ab9 100644 --- a/src/Avalonia.X11/XEmbedPlug.cs +++ b/src/Avalonia.X11/XEmbedPlug.cs @@ -13,19 +13,20 @@ public class XEmbedPlug : IDisposable private Color _backgroundColor; private readonly X11Info _x11; private readonly X11Window.XEmbedClientWindowMode _mode; + private readonly AvaloniaX11Platform _platform; private XEmbedPlug(IntPtr? parentXid) { - var platform = AvaloniaLocator.Current.GetRequiredService(); + _platform = AvaloniaLocator.Current.GetRequiredService(); _mode = new X11Window.XEmbedClientWindowMode(); - _root = new EmbeddableControlRoot(new X11Window(platform, null, _mode)); + _root = new EmbeddableControlRoot(new X11Window(_platform, null, _mode)); _root.Prepare(); - _x11 = platform.Info; + _x11 = _platform.Info; if (parentXid.HasValue) - XLib.XReparentWindow(platform.Display, Handle, parentXid.Value, 0, 0); + XLib.XReparentWindow(_platform.Display, Handle, parentXid.Value, 0, 0); // Make sure that the newly created XID is visible for other clients - XLib.XSync(platform.Display, false); + XLib.XSync(_platform.Display, false); } private EmbeddableControlRoot Root @@ -60,8 +61,7 @@ public class XEmbedPlug : IDisposable public void ProcessInteractiveResize(PixelSize size) { - - var events = (IX11PlatformDispatcher)AvaloniaLocator.Current.GetRequiredService(); + var events = _platform.DispatcherImpl; events.EventDispatcher.DispatchX11Events(CancellationToken.None); _mode.ProcessInteractiveResize(size); Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender); diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 33b9521ae2..016c598716 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -97,15 +97,17 @@ internal class BrowserWindowingPlatform : IWindowingPlatform .Bind().ToSingleton() .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToSingleton(); + if (IsManagedDispatcherEnabled) { EventGrouperDispatchQueue = new(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant( - new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); + Dispatcher.InitializeUIThreadDispatcher( + new ManagedDispatcherImpl( + new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); } else { - AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + Dispatcher.InitializeUIThreadDispatcher(new BrowserDispatcherImpl()); } // GC thread is the same as the main one when MT is disabled diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 48c5f5d84e..b56e686d4b 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -79,7 +79,6 @@ namespace Avalonia.Headless var clipboard = new Clipboard(clipboardImpl); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new ManagedDispatcherImpl(null)) .Bind().ToConstant(clipboardImpl) .Bind().ToConstant(clipboard) .Bind().ToSingleton() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 0d7d74e01c..ee8b85919e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -62,8 +62,8 @@ namespace Avalonia.LinuxFramebuffer ? new UiThreadRenderTimer(opts.Fps) : new DefaultRenderTimer(opts.Fps); + Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))) .Bind().ToConstant(timer) .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 41f273bc5e..f158d539ff 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -85,6 +85,8 @@ namespace Avalonia.Win32 SetDpiAwareness(); + Dispatcher.InitializeUIThreadDispatcher(s_instance._dispatcher); + var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); var clipboardImpl = new ClipboardImpl(); var clipboard = new Clipboard(clipboardImpl); @@ -96,7 +98,6 @@ namespace Avalonia.Win32 .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToSingleton() .Bind().ToSingleton() - .Bind().ToConstant(s_instance._dispatcher) .Bind().ToConstant(renderTimer) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 67152029af..29633a8609 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -77,6 +77,7 @@ namespace Avalonia.iOS Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); + Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl.Instance); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Graphics) .Bind().ToConstant(new CursorFactoryStub()) @@ -93,7 +94,6 @@ namespace Avalonia.iOS { Key.Up , "↑" } }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) .Bind().ToConstant(Timer) - .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); if (appDelegate is not null) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index e47dca2391..fa1919feb4 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading; @@ -19,7 +20,7 @@ using Xunit; namespace Avalonia.Base.UnitTests { - public class AvaloniaObjectTests_Binding + public class AvaloniaObjectTests_Binding : ScopedTestBase { [Fact] public void Bind_Sets_Current_Value() @@ -858,37 +859,28 @@ namespace Avalonia.Base.UnitTests [InlineData(BindingPriority.Style)] public void Typed_Bind_Executes_On_UIThread(BindingPriority priority) { - AsyncContext.Run(async () => + using (UnitTestApplication.Start()) { var target = new Class1(); var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var dispatcherMock = new Mock(); - dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - - var services = new TestServices( - dispatcherImpl: dispatcherMock.Object); - target.PropertyChanged += (s, e) => { Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); ++raised; }; - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source, priority); - await Task.Run(() => source.OnNext("foobar")); - Dispatcher.UIThread.RunJobs(); + target.Bind(Class1.FooProperty, source, priority); - Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); - Assert.Equal(1, raised); - } - }); + ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } } [Theory] @@ -896,37 +888,28 @@ namespace Avalonia.Base.UnitTests [InlineData(BindingPriority.Style)] public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority) { - AsyncContext.Run(async () => + using (UnitTestApplication.Start()) { var target = new Class1(); var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var dispatcherMock = new Mock(); - dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - - var services = new TestServices( - dispatcherImpl: dispatcherMock.Object); - target.PropertyChanged += (s, e) => { Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); ++raised; }; - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source, priority); - await Task.Run(() => source.OnNext("foobar")); - Dispatcher.UIThread.RunJobs(); + target.Bind(Class1.FooProperty, source, priority); - Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); - Assert.Equal(1, raised); - } - }); + ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); + } } [Theory] @@ -934,59 +917,41 @@ namespace Avalonia.Base.UnitTests [InlineData(BindingPriority.Style)] public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority) { - AsyncContext.Run(async () => - { - var target = new Class1(); - var source = new Subject>(); - var currentThreadId = Thread.CurrentThread.ManagedThreadId; - var raised = 0; - - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - - var services = new TestServices( - dispatcherImpl: threadingInterfaceMock.Object); + using var _ = UnitTestApplication.Start(); + var target = new Class1(); + var source = new Subject>(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + var raised = 0; - target.PropertyChanged += (s, e) => - { - Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); - ++raised; - }; + target.PropertyChanged += (s, e) => + { + Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); + ++raised; + }; - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source, priority); + target.Bind(Class1.FooProperty, source, priority); - await Task.Run(() => source.OnNext("foobar")); - Dispatcher.UIThread.RunJobs(); + ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); - Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); - Assert.Equal(1, raised); - } - }); + Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); + Assert.Equal(1, raised); } [Fact] - public async Task Bind_With_Scheduler_Executes_On_UI_Thread() + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Explicit threading test")] + public void Bind_With_Scheduler_Executes_On_UI_Thread() { + using var _ = UnitTestApplication.Start(); + var target = new Class1(); var source = new Subject(); - var currentThreadId = Thread.CurrentThread.ManagedThreadId; - - var threadingInterfaceMock = new Mock(); - threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - - var services = new TestServices( - dispatcherImpl: threadingInterfaceMock.Object); - - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.QuxProperty, source); + target.Bind(Class1.QuxProperty, source); - await Task.Run(() => source.OnNext(6.7), TestContext.Current.CancellationToken); - } + ThreadRunHelper.RunOnDedicatedThread(() => source.OnNext(6.7)).GetAwaiter().GetResult(); + Assert.NotEqual(6.7, target.GetValue(Class1.QuxProperty)); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index baa44c6770..02c34f886b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -492,19 +492,13 @@ namespace Avalonia.Base.UnitTests [Fact] public void Bind_Executes_On_UIThread() { - AsyncContext.Run(async () => + using(UnitTestApplication.Start()) { var target = new Class1(); var source = new Subject(); var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0; - var dispatcherMock = new Mock(); - dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) - .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); - - var services = new TestServices( - dispatcherImpl: dispatcherMock.Object); target.PropertyChanged += (s, e) => { @@ -512,17 +506,15 @@ namespace Avalonia.Base.UnitTests ++raised; }; - using (UnitTestApplication.Start(services)) - { - target.Bind(Class1.FooProperty, source); + + target.Bind(Class1.FooProperty, source); - await Task.Run(() => source.OnNext("foobar")); - Dispatcher.UIThread.RunJobs(); + ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar")); + Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); - Assert.Equal("foobar", target.Foo); - Assert.Equal(1, raised); - } - }); + Assert.Equal("foobar", target.Foo); + Assert.Equal(1, raised); + } } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs index 5c8dd2d476..b8070ea104 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Subjects; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Platform; @@ -9,49 +10,43 @@ using Xunit; namespace Avalonia.Base.UnitTests { - public class AvaloniaObjectTests_Threading + public class AvaloniaObjectTests_Threading : ScopedTestBase { - private TestDipatcherImpl _threading = new(true); - - [Fact] - public void AvaloniaObject_Constructor_Should_Throw() + void AssertThrowsOnDifferentThread(Action cb) { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new TestDipatcherImpl()))) - { - Assert.Throws(() => new Class1()); - } + Assert.Throws(() => + ThreadRunHelper.RunOnDedicatedThread(cb).GetAwaiter().GetResult()); } [Fact] public void StyledProperty_GetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.GetValue(Class1.StyledProperty)); + target.GetValue(Class1.StyledProperty); + + AssertThrowsOnDifferentThread(() => target.GetValue(Class1.StyledProperty)); } } [Fact] public void StyledProperty_SetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.SetValue(Class1.StyledProperty, "foo")); + AssertThrowsOnDifferentThread(() => target.SetValue(Class1.StyledProperty, "foo")); } } [Fact] public void Setting_StyledProperty_Binding_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => + AssertThrowsOnDifferentThread(() => target.Bind( Class1.StyledProperty, new BehaviorSubject("foo"))); @@ -61,55 +56,50 @@ namespace Avalonia.Base.UnitTests [Fact] public void StyledProperty_ClearValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.ClearValue(Class1.StyledProperty)); + AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.StyledProperty)); } } [Fact] public void StyledProperty_IsSet_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.IsSet(Class1.StyledProperty)); + AssertThrowsOnDifferentThread(() => target.IsSet(Class1.StyledProperty)); } } [Fact] public void DirectProperty_GetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.GetValue(Class1.DirectProperty)); + AssertThrowsOnDifferentThread(() => target.GetValue(Class1.DirectProperty)); } } [Fact] public void DirectProperty_SetValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.SetValue(Class1.DirectProperty, "foo")); + AssertThrowsOnDifferentThread(() => target.SetValue(Class1.DirectProperty, "foo")); } } [Fact] public void Setting_DirectProperty_Binding_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => + AssertThrowsOnDifferentThread(() => target.Bind( Class1.DirectProperty, new BehaviorSubject("foo"))); @@ -119,22 +109,20 @@ namespace Avalonia.Base.UnitTests [Fact] public void DirectProperty_ClearValue_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.ClearValue(Class1.DirectProperty)); + AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.DirectProperty)); } } [Fact] public void DirectProperty_IsSet_Should_Throw() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) + using (UnitTestApplication.Start()) { var target = new Class1(); - _threading.CurrentThreadIsLoopThread = false; - Assert.Throws(() => target.IsSet(Class1.DirectProperty)); + AssertThrowsOnDifferentThread(() => target.IsSet(Class1.DirectProperty)); } } @@ -146,27 +134,5 @@ namespace Avalonia.Base.UnitTests public static readonly DirectProperty DirectProperty = AvaloniaProperty.RegisterDirect("Qux", _ => null, (o, v) => { }); } - - private class TestDipatcherImpl : IDispatcherImpl - { - - public TestDipatcherImpl(bool isLoopThread = false) - { - CurrentThreadIsLoopThread = isLoopThread; - } - - public bool CurrentThreadIsLoopThread { get; set; } - - public event Action? Signaled { add { } remove { } } - public event Action? Timer { add { } remove { } } - public long Now => 0; - public void UpdateTimer(long? dueTimeInMs) - { - throw new NotImplementedException(); - } - public void Signal() => throw new NotImplementedException(); - - - } } } diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs index 5cde720bc2..e9a306509d 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs @@ -1,43 +1,76 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Threading; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests; // Some of these exceptions are based from https://github.com/dotnet/wpf-test/blob/05797008bb4975ceeb71be36c47f01688f535d53/src/Test/ElementServices/FeatureTests/Untrusted/Dispatcher/UnhandledExceptionTest.cs#L30 -public partial class DispatcherTests +public partial class DispatcherTests : ScopedTestBase { private const string ExpectedExceptionText = "Exception thrown inside Dispatcher.Invoke / Dispatcher.BeginInvoke."; private int _numberOfHandlerOnUnhandledEventInvoked; private int _numberOfHandlerOnUnhandledEventFilterInvoked; + private Dispatcher _uiThread; public DispatcherTests() { _numberOfHandlerOnUnhandledEventInvoked = 0; _numberOfHandlerOnUnhandledEventFilterInvoked = 0; + + VerifyDispatcherSanity(); + _uiThread = Dispatcher.CurrentDispatcher; + } + + void VerifyDispatcherSanity() + { + // Verify that we are in a clear-ish state. Do this for every test to ensure that our reset procedure is working + Assert.Null(Dispatcher.FromThread(Thread.CurrentThread)); + Assert.Null(Dispatcher.TryGetUIThread()); + + // The first (this) dispatcher becomes UI thread one + Assert.NotNull(Dispatcher.CurrentDispatcher); + Assert.Equal(Dispatcher.TryGetUIThread(), Dispatcher.CurrentDispatcher); + Assert.Equal(Dispatcher.UIThread, Dispatcher.CurrentDispatcher); + + // Dispatcher.FromThread works + Assert.Equal(Dispatcher.CurrentDispatcher, Dispatcher.FromThread(Thread.CurrentThread)); + Assert.Equal(Dispatcher.UIThread, Dispatcher.FromThread(Thread.CurrentThread)); } [Fact] - public void DispatcherHandlesExceptionWithPost() + [SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Tests the dispatcher itself")] + public void Different_Threads_Auto_Spawn_Dispatchers() { - var impl = new ManagedDispatcherImpl(null); - var disp = new Dispatcher(impl); + var dispatcher = Dispatcher.CurrentDispatcher; + ThreadRunHelper.RunOnDedicatedThread(() => + { + Assert.Null(Dispatcher.FromThread(Thread.CurrentThread)); + Assert.NotNull(Dispatcher.CurrentDispatcher); + Assert.NotEqual(dispatcher, Dispatcher.CurrentDispatcher); + Assert.Equal(Dispatcher.CurrentDispatcher, Dispatcher.FromThread(Thread.CurrentThread)); + }).GetAwaiter().GetResult(); + } + [Fact] + public void DispatcherHandlesExceptionWithPost() + { var handled = false; var executed = false; - disp.UnhandledException += (sender, args) => + _uiThread.UnhandledException += (sender, args) => { handled = true; args.Handled = true; }; - disp.Post(() => ThrowAnException()); - disp.Post(() => executed = true); + _uiThread.Post(() => ThrowAnException()); + _uiThread.Post(() => executed = true); - disp.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); Assert.True(handled); Assert.True(executed); @@ -46,14 +79,11 @@ public partial class DispatcherTests [Fact] public void SyncContextExceptionCanBeHandledWithPost() { - var impl = new ManagedDispatcherImpl(null); - var disp = new Dispatcher(impl); - - var syncContext = disp.GetContextWithPriority(DispatcherPriority.Background); + var syncContext = _uiThread.GetContextWithPriority(DispatcherPriority.Background); var handled = false; var executed = false; - disp.UnhandledException += (sender, args) => + _uiThread.UnhandledException += (sender, args) => { handled = true; args.Handled = true; @@ -62,7 +92,7 @@ public partial class DispatcherTests syncContext.Post(_ => ThrowAnException(), null); syncContext.Post(_ => executed = true, null); - disp.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); Assert.True(handled); Assert.True(executed); @@ -71,24 +101,22 @@ public partial class DispatcherTests [Fact] public void CanRemoveDispatcherExceptionHandler() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); var caughtCorrectException = false; - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionNotHandled; - dispatcher.UnhandledExceptionFilter -= + _uiThread.UnhandledExceptionFilter -= HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException -= + _uiThread.UnhandledException -= HandlerOnUnhandledExceptionNotHandled; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { @@ -103,19 +131,16 @@ public partial class DispatcherTests [Fact] public void CanHandleExceptionWithUnhandledException() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandled; var caughtCorrectException = true; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception) { @@ -131,20 +156,17 @@ public partial class DispatcherTests [Fact] public void InvokeMethodDoesntTriggerUnhandledException() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandled; var caughtCorrectException = false; try { // Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. - dispatcher.Invoke(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Invoke(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { @@ -160,21 +182,18 @@ public partial class DispatcherTests [Fact] public void InvokeAsyncMethodDoesntTriggerUnhandledException() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandled; var caughtCorrectException = false; try { // Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. - var op = dispatcher.InvokeAsync(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken); + var op = _uiThread.InvokeAsync(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken); op.Wait(); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { @@ -190,19 +209,16 @@ public partial class DispatcherTests [Fact] public void CanRethrowExceptionWithUnhandledException() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionNotHandled; var caughtCorrectException = false; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { @@ -217,23 +233,20 @@ public partial class DispatcherTests [Fact] public void MultipleUnhandledExceptionFilterCannotResetRequestCatchFlag() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterNotRequestCatch; - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionNotHandled; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandled; var caughtCorrectException = false; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { @@ -248,22 +261,19 @@ public partial class DispatcherTests [Fact] public void MultipleUnhandledExceptionCannotResetHandleFlag() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterRequestCatch; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandled; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionNotHandled; var caughtCorrectException = true; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception) { @@ -279,19 +289,16 @@ public partial class DispatcherTests [Fact] public void CanPushFrameAndShutdownDispatcherFromUnhandledException() { - var impl = new ManagedDispatcherImpl(null); - var dispatcher = new Dispatcher(impl); - - dispatcher.UnhandledExceptionFilter += + _uiThread.UnhandledExceptionFilter += HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame; - dispatcher.UnhandledException += + _uiThread.UnhandledException += HandlerOnUnhandledExceptionHandledPushFrame; var caughtCorrectException = false; try { - dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); - dispatcher.RunJobs(null, TestContext.Current.CancellationToken); + _uiThread.Post(ThrowAnException, DispatcherPriority.Normal); + _uiThread.RunJobs(null, TestContext.Current.CancellationToken); } catch (Exception e) { diff --git a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs index d6e5243abd..5596761b7c 100644 --- a/tests/Avalonia.Base.UnitTests/DispatcherTests.cs +++ b/tests/Avalonia.Base.UnitTests/DispatcherTests.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Threading; +using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests; @@ -121,11 +122,11 @@ public partial class DispatcherTests public void DispatcherExecutesJobsAccordingToPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl); + Dispatcher.InitializeUIThreadDispatcher(impl); var actions = new List(); - disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); - disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); - disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input); + _uiThread.Post(()=>actions.Add("Background"), DispatcherPriority.Background); + _uiThread.Post(()=>actions.Add("Render"), DispatcherPriority.Render); + _uiThread.Post(()=>actions.Add("Input"), DispatcherPriority.Input); Assert.True(impl.AskedForSignal); impl.ExecuteSignal(); Assert.Equal(new[] { "Render", "Input", "Background" }, actions); @@ -135,12 +136,11 @@ public partial class DispatcherTests public void DispatcherPreservesOrderWhenChangingPriority() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl); + Dispatcher.InitializeUIThreadDispatcher(impl); var actions = new List(); - var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background, TestContext.Current.CancellationToken); - var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input, TestContext.Current.CancellationToken); - disp.Post(() => actions.Add("Render"), DispatcherPriority.Render); - + var toPromote = _uiThread.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background, TestContext.Current.CancellationToken); + var toPromote2 = _uiThread.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input, TestContext.Current.CancellationToken); + _uiThread.Post(() => actions.Add("Render"), DispatcherPriority.Render); toPromote.Priority = DispatcherPriority.Render; toPromote2.Priority = DispatcherPriority.Render; @@ -154,12 +154,13 @@ public partial class DispatcherTests public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() { var impl = new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl); + Dispatcher.ResetForUnitTests(); + _uiThread = new Dispatcher(impl); var actions = new List(); for (var c = 0; c < 10; c++) { var itemId = c; - disp.Post(() => + _uiThread.Post(() => { actions.Add(itemId); impl.Now += 20; @@ -195,14 +196,17 @@ public partial class DispatcherTests [Fact] public void DispatcherStopsItemProcessingWhenInputIsPending() { + Dispatcher.ResetForUnitTests(); + var impl = new SimpleDispatcherImpl(); impl.TestInputPending = true; - var disp = new Dispatcher(impl); + _uiThread = new Dispatcher(impl); + var actions = new List(); for (var c = 0; c < 10; c++) { var itemId = c; - disp.Post(() => + _uiThread.Post(() => { actions.Add(itemId); if (itemId == 0 || itemId == 3 || itemId == 7) @@ -249,10 +253,10 @@ public partial class DispatcherTests public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground) { var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl(); - var disp = new Dispatcher(impl); + Dispatcher.InitializeUIThreadDispatcher(impl); bool finished = false; - disp.InvokeAsync(() => finished = true, + _uiThread.InvokeAsync(() => finished = true, foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait(); Assert.True(finished); @@ -268,7 +272,6 @@ public partial class DispatcherTests public DispatcherServices(IDispatcherImpl impl) { _scope = AvaloniaLocator.EnterScope(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(impl); Dispatcher.ResetForUnitTests(); SynchronizationContext.SetSynchronizationContext(null); } diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index b81c8aefd6..6380f71935 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -18,17 +18,13 @@ namespace Avalonia.Base.UnitTests.Input { using var scope = AvaloniaLocator.EnterScope(); var settingsMock = new Mock(); - var dispatcherMock = new Mock(); - - dispatcherMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true); - + AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(settingsMock.Object); using var app = UnitTestApplication.Start( new TestServices( - inputManager: new InputManager(), - dispatcherImpl: dispatcherMock.Object)); + inputManager: new InputManager())); var renderer = new Mock(); var device = new MouseDevice(); diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs index a83122099e..97f83b1a69 100644 --- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs @@ -213,7 +213,7 @@ namespace Avalonia.Input.UnitTests private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) { var unitTestApp = UnitTestApplication.Start( - new TestServices(inputManager: new InputManager(), dispatcherImpl: Mock.Of(x => x.CurrentThreadIsLoopThread == true))); + new TestServices(inputManager: new InputManager())); var iSettingsMock = new Mock(); iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny())).Returns(doubleClickTime); iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny())).Returns(new Size(16, 16)); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index aa213d996c..9ae80dbacb 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Set_ExitCode_After_Shutdown() { - using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new ManagedDispatcherImpl(null)))) + using (UnitTestApplication.Start(new TestServices())) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.SetupCore(Array.Empty()); @@ -326,7 +326,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() { - using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: new ManagedDispatcherImpl(null)))) + using (UnitTestApplication.Start(TestServices.StyledWindow.With())) using (var lifetime = new ClassicDesktopStyleApplicationLifetime()) { var lifetimeEvents = new Mock(); @@ -475,7 +475,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Shutdown_NotCancellable_By_Preventing_Window_Close() { - using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop()))) + using (UnitTestApplication.Start(TestServices.StyledWindow.With())) using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) { lifetime.SetupCore(Array.Empty()); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs index 1b87a3ff2f..c82c3e4416 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs @@ -1,7 +1,6 @@ -using System; +using System; using Avalonia.Controls.Primitives; using Avalonia.UnitTests; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives diff --git a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs index 6df8eb9fe1..e5e2255b50 100644 --- a/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs +++ b/tests/Avalonia.LeakTests/AvaloniaObjectTests.cs @@ -149,7 +149,7 @@ namespace Avalonia.LeakTests } var weakTarget = SetupBinding(); - + CollectGarbage(); Assert.False(weakTarget.IsAlive); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs index da649bc184..8b4b868a6d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs @@ -24,8 +24,9 @@ public class BindingTests_Delay : ScopedTestBase, IDisposable public BindingTests_Delay() { + _app = UnitTestApplication.Start(new(keyboardDevice: () => new KeyboardDevice())); _dispatcher = new ManualTimerDispatcher(); - _app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, keyboardDevice: () => new KeyboardDevice())); + _ = new Dispatcher(_dispatcher); _source = new BindingTests.Source { Foo = InitialFooValue }; _target = new TextBox { DataContext = _source }; diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs index 82d5268b73..33c1439655 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs @@ -4,11 +4,10 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Diagnostics; using Avalonia.Data.Core; -using Xunit; -using Avalonia.Utilities; -using Avalonia.Data.Core.ExpressionNodes; -using Avalonia.UnitTests; using Avalonia.Data.Core.Parsers; +using Avalonia.UnitTests; +using Avalonia.Utilities; +using Xunit; namespace Avalonia.Markup.UnitTests.Parsers { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs index 63916e03e5..0c061805e1 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs @@ -6,7 +6,6 @@ using Avalonia.Diagnostics; using Avalonia.Data.Core; using Xunit; using Avalonia.Utilities; -using Avalonia.Data.Core.ExpressionNodes; using Avalonia.UnitTests; using Avalonia.Data.Core.Parsers; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 60fd896c0d..def3c7cef3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -41,5 +41,15 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs index 48a63bc716..4112edacbf 100644 --- a/tests/Avalonia.RenderTests/TestRenderHelper.cs +++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs @@ -28,16 +28,9 @@ namespace Avalonia.Skia.RenderTests; static class TestRenderHelper { - private static readonly TestDispatcherImpl s_dispatcherImpl = - new TestDispatcherImpl(); - static TestRenderHelper() { SkiaPlatform.Initialize(); - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(s_dispatcherImpl); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new StandardAssetLoader()); AvaloniaLocator.CurrentMutable.Bind().ToConstant(new HarfBuzzTextShaper()); } @@ -48,7 +41,7 @@ static class TestRenderHelper var dir = Path.GetDirectoryName(path); Assert.NotNull(dir); - if (!Directory.Exists(dir)) + if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var factory = AvaloniaLocator.Current.GetRequiredService(); @@ -110,13 +103,14 @@ static class TestRenderHelper public static void BeginTest() { - s_dispatcherImpl.MainThread = Thread.CurrentThread; + Dispatcher.ResetBeforeUnitTests(); } public static void EndTest() { if (Dispatcher.UIThread.CheckAccess()) Dispatcher.UIThread.RunJobs(); + Dispatcher.ResetForUnitTests(); } public static string GetTestsDirectory() @@ -131,28 +125,6 @@ static class TestRenderHelper Assert.NotNull(path); return path; } - - private class TestDispatcherImpl : IDispatcherImpl - { - public bool CurrentThreadIsLoopThread => MainThread?.ManagedThreadId == Thread.CurrentThread.ManagedThreadId; - - public Thread? MainThread { get; set; } - - public event Action? Signaled { add { } remove { } } - public event Action? Timer { add { } remove { } } - - public void Signal() - { - // No-op - } - - public long Now => 0; - - public void UpdateTimer(long? dueTimeInMs) - { - // No-op - } - } public static void AssertCompareImages(string actualPath, string expectedPath) { diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index baf86a538f..5c8cd6d776 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -15,34 +15,32 @@ namespace Avalonia.UnitTests { public class TestServices { - public static readonly TestServices StyledWindow = new TestServices( + public static TestServices StyledWindow => new TestServices( assetLoader: new StandardAssetLoader(), platform: new StandardRuntimePlatform(), renderInterface: new HeadlessPlatformRenderInterface(), standardCursorFactory: new HeadlessCursorFactoryStub(), theme: () => CreateSimpleTheme(), - dispatcherImpl: new NullDispatcherImpl(), fontManagerImpl: new TestFontManager(), textShaperImpl: new HarfBuzzTextShaper(), windowingPlatform: new MockWindowingPlatform()); - public static readonly TestServices MockPlatformRenderInterface = new TestServices( + public static TestServices MockPlatformRenderInterface => new TestServices( assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new TestFontManager(), textShaperImpl: new HarfBuzzTextShaper()); - public static readonly TestServices MockPlatformWrapper = new TestServices( + public static TestServices MockPlatformWrapper => new TestServices( platform: Mock.Of()); - public static readonly TestServices MockThreadingInterface = new TestServices( - dispatcherImpl: new NullDispatcherImpl(), + public static TestServices MockThreadingInterface => new TestServices( assetLoader: new StandardAssetLoader()); - public static readonly TestServices MockWindowingPlatform = new TestServices( + public static TestServices MockWindowingPlatform => new TestServices( windowingPlatform: new MockWindowingPlatform()); - public static readonly TestServices RealFocus = new TestServices( + public static TestServices RealFocus => new TestServices( keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), @@ -51,7 +49,7 @@ namespace Avalonia.UnitTests fontManagerImpl: new TestFontManager(), textShaperImpl: new HarfBuzzTextShaper()); - public static readonly TestServices FocusableWindow = new TestServices( + public static TestServices FocusableWindow => new TestServices( keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: () => new KeyboardNavigationHandler(), inputManager: new InputManager(), @@ -60,12 +58,11 @@ namespace Avalonia.UnitTests renderInterface: new HeadlessPlatformRenderInterface(), standardCursorFactory: new HeadlessCursorFactoryStub(), theme: () => CreateSimpleTheme(), - dispatcherImpl: new NullDispatcherImpl(), fontManagerImpl: new TestFontManager(), textShaperImpl: new HarfBuzzTextShaper(), windowingPlatform: new MockWindowingPlatform()); - public static readonly TestServices TextServices = new TestServices( + public static TestServices TextServices => new TestServices( assetLoader: new StandardAssetLoader(), renderInterface: new HeadlessPlatformRenderInterface(), fontManagerImpl: new TestFontManager(), @@ -82,7 +79,6 @@ namespace Avalonia.UnitTests IPlatformRenderInterface? renderInterface = null, ICursorFactory? standardCursorFactory = null, Func? theme = null, - IDispatcherImpl? dispatcherImpl = null, IFontManagerImpl? fontManagerImpl = null, ITextShaperImpl? textShaperImpl = null, IWindowImpl? windowImpl = null, @@ -102,7 +98,6 @@ namespace Avalonia.UnitTests TextShaperImpl = textShaperImpl; StandardCursorFactory = standardCursorFactory; Theme = theme; - DispatcherImpl = dispatcherImpl; WindowImpl = windowImpl; WindowingPlatform = windowingPlatform; } @@ -120,7 +115,6 @@ namespace Avalonia.UnitTests public ITextShaperImpl? TextShaperImpl { get; } public ICursorFactory? StandardCursorFactory { get; } public Func? Theme { get; } - public IDispatcherImpl? DispatcherImpl { get; } public IWindowImpl? WindowImpl { get; } public IWindowingPlatform? WindowingPlatform { get; } @@ -138,7 +132,6 @@ namespace Avalonia.UnitTests IScheduler? scheduler = null, ICursorFactory? standardCursorFactory = null, Func? theme = null, - IDispatcherImpl? dispatcherImpl = null, IFontManagerImpl? fontManagerImpl = null, ITextShaperImpl? textShaperImpl = null, IWindowImpl? windowImpl = null, @@ -158,7 +151,6 @@ namespace Avalonia.UnitTests textShaperImpl: textShaperImpl ?? TextShaperImpl, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, theme: theme ?? Theme, - dispatcherImpl: dispatcherImpl ?? DispatcherImpl, windowingPlatform: windowingPlatform ?? WindowingPlatform, windowImpl: windowImpl ?? WindowImpl); } diff --git a/tests/Avalonia.UnitTests/ThreadRunHelper.cs b/tests/Avalonia.UnitTests/ThreadRunHelper.cs index ba88c1c9e6..73c8e8f545 100644 --- a/tests/Avalonia.UnitTests/ThreadRunHelper.cs +++ b/tests/Avalonia.UnitTests/ThreadRunHelper.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Threading; using System.Threading.Tasks; diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 4d266aa809..e2bfc9e86c 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -52,6 +52,7 @@ namespace Avalonia.UnitTests (AvaloniaLocator.Current.GetService() as ToolTipService)?.Dispose(); (AvaloniaLocator.Current.GetService() as IDisposable)?.Dispose(); + (AvaloniaLocator.Current.GetService() as IDisposable)?.Dispose(); Dispatcher.ResetForUnitTests(); scope.Dispose(); @@ -84,7 +85,6 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.RenderInterface) .Bind().ToConstant(Services.FontManagerImpl) .Bind().ToConstant(Services.TextShaperImpl) - .Bind().ToConstant(Services.DispatcherImpl) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton()