Browse Source

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 <julien@lebosquain.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
pull/14481/merge
Nikita Tsukanov 3 weeks ago
committed by GitHub
parent
commit
12f7e96d72
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  3. 13
      src/Avalonia.Base/AvaloniaObject.cs
  4. 9
      src/Avalonia.Base/Input/InputManager.cs
  5. 0
      src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs
  6. 5
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs
  7. 13
      src/Avalonia.Base/StyledElement.cs
  8. 10
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  9. 95
      src/Avalonia.Base/Threading/Dispatcher.ThreadStorage.cs
  10. 8
      src/Avalonia.Base/Threading/Dispatcher.Timers.cs
  11. 129
      src/Avalonia.Base/Threading/Dispatcher.cs
  12. 31
      src/Avalonia.Base/Threading/IDispatcherImpl.cs
  13. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  14. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  15. 3
      src/Avalonia.Native/CallbackBase.cs
  16. 11
      src/Avalonia.X11/X11Platform.cs
  17. 14
      src/Avalonia.X11/XEmbedPlug.cs
  18. 8
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  19. 1
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  20. 2
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  21. 3
      src/Windows/Avalonia.Win32/Win32Platform.cs
  22. 2
      src/iOS/Avalonia.iOS/Platform.cs
  23. 119
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  24. 24
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  25. 88
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs
  26. 155
      tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs
  27. 35
      tests/Avalonia.Base.UnitTests/DispatcherTests.cs
  28. 8
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  29. 2
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
  30. 6
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  31. 3
      tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs
  32. 2
      tests/Avalonia.LeakTests/AvaloniaObjectTests.cs
  33. 3
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs
  34. 7
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  35. 1
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  36. 10
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  37. 34
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  38. 24
      tests/Avalonia.UnitTests/TestServices.cs
  39. 2
      tests/Avalonia.UnitTests/ThreadRunHelper.cs
  40. 2
      tests/Avalonia.UnitTests/UnitTestApplication.cs

12
api/Avalonia.nupkg.xml

@ -307,6 +307,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left> <Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right> <Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression> </Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Platform.ManagedDispatcherImpl</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression> <Suppression>
<DiagnosticId>CP0001</DiagnosticId> <DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.ChromeOverlayLayer</Target> <Target>T:Avalonia.Controls.Primitives.ChromeOverlayLayer</Target>
@ -697,6 +703,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left> <Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right> <Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression> </Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Platform.ManagedDispatcherImpl</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression> <Suppression>
<DiagnosticId>CP0001</DiagnosticId> <DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.ChromeOverlayLayer</Target> <Target>T:Avalonia.Controls.Primitives.ChromeOverlayLayer</Target>

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -81,12 +81,12 @@ namespace Avalonia.Android
{ {
Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions(); Options = AvaloniaLocator.Current.GetService<AndroidPlatformOptions>() ?? new AndroidPlatformOptions();
Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl());
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<ICursorFactory>().ToTransient<CursorFactory>() .Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub()) .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>() .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new AndroidDispatcherImpl())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer()) .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

13
src/Avalonia.Base/AvaloniaObject.cs

@ -34,7 +34,6 @@ namespace Avalonia
/// </summary> /// </summary>
public AvaloniaObject() public AvaloniaObject()
{ {
VerifyAccess();
_values = new ValueStore(this); _values = new ValueStore(this);
} }
@ -109,16 +108,22 @@ namespace Avalonia
/// </summary> /// </summary>
internal string DebugDisplay => GetDebugDisplay(true); internal string DebugDisplay => GetDebugDisplay(true);
/// <summary>
/// Returns the <see cref="Dispatcher"/> that this
/// <see cref="AvaloniaObject"/> is associated with.
/// </summary>
public Dispatcher Dispatcher { get; } = Dispatcher.CurrentDispatcher;
/// <summary> /// <summary>
/// Returns a value indicating whether the current thread is the UI thread. /// Returns a value indicating whether the current thread is the UI thread.
/// </summary> /// </summary>
/// <returns>true if the current thread is the UI thread; otherwise false.</returns> /// <returns>true if the current thread is the UI thread; otherwise false.</returns>
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public bool CheckAccess() => Dispatcher.CheckAccess();
/// <summary> /// <summary>
/// Checks that the current thread is the UI thread and throws if not. /// Checks that the current thread is the UI thread and throws if not.
/// </summary> /// </summary>
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); public void VerifyAccess() => Dispatcher.VerifyAccess();
/// <summary> /// <summary>
/// Clears a <see cref="AvaloniaProperty"/>'s local value. /// Clears a <see cref="AvaloniaProperty"/>'s local value.

9
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 /// Receives input from the windowing subsystem and dispatches it to interested parties
/// for processing. /// for processing.
/// </summary> /// </summary>
internal class InputManager : IInputManager internal class InputManager : IInputManager, IDisposable
{ {
private readonly LightweightSubject<RawInputEventArgs> _preProcess = new(); private readonly LightweightSubject<RawInputEventArgs> _preProcess = new();
private readonly LightweightSubject<RawInputEventArgs> _process = new(); private readonly LightweightSubject<RawInputEventArgs> _process = new();
@ -36,5 +36,12 @@ namespace Avalonia.Input
_process.OnNext(e); _process.OnNext(e);
_postProcess.OnNext(e); _postProcess.OnNext(e);
} }
public void Dispose()
{
_preProcess.OnCompleted();
_process.OnCompleted();
_postProcess.OnCompleted();
}
} }
} }

0
src/Avalonia.Controls/Platform/ManagedDispatcherImpl.cs → src/Avalonia.Base/Platform/ManagedDispatcherImpl.cs

5
src/Avalonia.Base/Rendering/Composition/Transport/BatchStreamArrayPool.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
@ -30,9 +31,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this); var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
if ( if (
reclaimImmediately reclaimImmediately
|| ( || Dispatcher.FromThread(Thread.CurrentThread) == null)
AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null
&& AvaloniaLocator.Current.GetService<IDispatcherImpl>() == null))
_reclaimImmediately = true; _reclaimImmediately = true;
else else
StartUpdateTimer(startTimer, updateRef); StartUpdateTimer(startTimer, updateRef);

13
src/Avalonia.Base/StyledElement.cs

@ -76,7 +76,7 @@ namespace Avalonia
public static readonly StyledProperty<ControlTheme?> ThemeProperty = public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme)); AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme));
private static readonly ControlTheme s_invalidTheme = new ControlTheme(); [ThreadStatic] private static ControlTheme? s_invalidTheme;
private int _initCount; private int _initCount;
private string? _name; private string? _name;
private Classes? _classes; private Classes? _classes;
@ -332,6 +332,9 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;
internal static ControlTheme InvalidTheme
=> s_invalidTheme ??= new();
/// <inheritdoc/> /// <inheritdoc/>
public virtual void BeginInit() public virtual void BeginInit()
{ {
@ -666,10 +669,10 @@ namespace Avalonia
if (this.TryFindResource(key, out var value) && value is ControlTheme t) if (this.TryFindResource(key, out var value) && value is ControlTheme t)
_implicitTheme = t; _implicitTheme = t;
else else
_implicitTheme = s_invalidTheme; _implicitTheme = InvalidTheme;
} }
if (_implicitTheme != s_invalidTheme) if (_implicitTheme != InvalidTheme)
return _implicitTheme; return _implicitTheme;
return null; return null;
@ -828,11 +831,11 @@ namespace Avalonia
return; return;
// Refetch the implicit theme. // Refetch the implicit theme.
var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; var oldImplicitTheme = _implicitTheme == InvalidTheme ? null : _implicitTheme;
_implicitTheme = null; _implicitTheme = null;
GetEffectiveTheme(); 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 the implicit theme has changed, detach the existing theme.
if (newImplicitTheme != oldImplicitTheme) if (newImplicitTheme != oldImplicitTheme)

10
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -12,7 +12,7 @@ public partial class Dispatcher
private bool _explicitBackgroundProcessingRequested; private bool _explicitBackgroundProcessingRequested;
private const int MaximumInputStarvationTimeInFallbackMode = 50; private const int MaximumInputStarvationTimeInFallbackMode = 50;
private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50; private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50;
private readonly int _maximumInputStarvationTime; private int _maximumInputStarvationTime;
void RequestBackgroundProcessing() void RequestBackgroundProcessing()
{ {
@ -101,9 +101,9 @@ public partial class Dispatcher
internal static void ResetBeforeUnitTests() internal static void ResetBeforeUnitTests()
{ {
s_uiThread = null; ResetGlobalState();
} }
internal static void ResetForUnitTests() internal static void ResetForUnitTests()
{ {
if (s_uiThread == null) if (s_uiThread == null)
@ -122,14 +122,14 @@ public partial class Dispatcher
if (job == null || job.Priority <= DispatcherPriority.Inactive) if (job == null || job.Priority <= DispatcherPriority.Inactive)
{ {
s_uiThread.ShutdownImpl(); s_uiThread.ShutdownImpl();
s_uiThread = null; ResetGlobalState();
return; return;
} }
s_uiThread.ExecuteJob(job); s_uiThread.ExecuteJob(job);
} }
} }
private void ExecuteJob(DispatcherOperation job) private void ExecuteJob(DispatcherOperation job)
{ {

95
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<Thread, DispatcherReferenceStorage> 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<Dispatcher> 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;
}
}
}

8
src/Avalonia.Base/Threading/Dispatcher.Timers.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
namespace Avalonia.Threading; namespace Avalonia.Threading;
@ -15,7 +16,8 @@ public partial class Dispatcher
private long? _dueTimeForBackgroundProcessing; private long? _dueTimeForBackgroundProcessing;
private long? _osTimerSetTo; private long? _osTimerSetTo;
internal long Now => _impl.Now; private readonly Func<long> _timeProvider;
internal long Now => _timeProvider();
private void UpdateOSTimer() private void UpdateOSTimer()
{ {
@ -26,6 +28,7 @@ public partial class Dispatcher
_dueTimeForTimers ?? _dueTimeForBackgroundProcessing; _dueTimeForTimers ?? _dueTimeForBackgroundProcessing;
if (_osTimerSetTo == nextDueTime) if (_osTimerSetTo == nextDueTime)
return; return;
_impl.UpdateTimer(_osTimerSetTo = nextDueTime); _impl.UpdateTimer(_osTimerSetTo = nextDueTime);
} }
@ -114,7 +117,8 @@ public partial class Dispatcher
bool needToProcessQueue = false; bool needToProcessQueue = false;
lock (InstanceLock) lock (InstanceLock)
{ {
_impl.UpdateTimer(_osTimerSetTo = null); _impl.UpdateTimer(null);
_osTimerSetTo = null;
needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now; needToPromoteTimers = _dueTimeForTimers.HasValue && _dueTimeForTimers.Value <= Now;
if (needToPromoteTimers) if (needToPromoteTimers)
_dueTimeForTimers = null; _dueTimeForTimers = null;

129
src/Avalonia.Base/Threading/Dispatcher.cs

@ -3,7 +3,10 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Controls.Platform;
using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Threading; namespace Avalonia.Threading;
@ -17,63 +20,60 @@ namespace Avalonia.Threading;
public partial class Dispatcher : IDispatcher public partial class Dispatcher : IDispatcher
{ {
private IDispatcherImpl _impl; private IDispatcherImpl _impl;
private bool _initialized;
internal object InstanceLock { get; } = new(); internal object InstanceLock { get; } = new();
private IControlledDispatcherImpl? _controlledImpl; private IControlledDispatcherImpl? _controlledImpl;
private static Dispatcher? s_uiThread;
private IDispatcherImplWithPendingInput? _pendingInputImpl; private IDispatcherImplWithPendingInput? _pendingInputImpl;
private readonly IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl; private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl;
private readonly Thread _thread;
private readonly AvaloniaSynchronizationContext?[] _priorityContexts = private readonly AvaloniaSynchronizationContext?[] _priorityContexts =
new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1]; new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1];
internal Dispatcher(IDispatcherImpl impl) internal Dispatcher(IDispatcherImpl? impl)
{ {
_impl = impl; #if DEBUG
impl.Timer += OnOSTimer; if (AvaloniaLocator.Current.GetService<IDispatcherImpl>() != null
impl.Signaled += Signaled; || AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() != null)
_controlledImpl = _impl as IControlledDispatcherImpl; throw new InvalidOperationException(
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput; "Registering IDispatcherImpl or IPlatformThreadingInterface via locator is no longer valid");
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing; #endif
_maximumInputStarvationTime = _backgroundProcessingImpl == null ? lock (s_globalLock)
MaximumInputStarvationTimeInFallbackMode :
MaximumInputStarvationTimeInExplicitProcessingExplicitMode;
if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
_unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this);
_exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);
}
public static Dispatcher UIThread
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{ {
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)] s_dispatchers.Remove(Thread.CurrentThread);
private static Dispatcher CreateUIThreadDispatcher() s_dispatchers.Add(Thread.CurrentThread,
{ s_currentThreadDispatcher = new() { Reference = new WeakReference<Dispatcher>(this) });
var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>(); }
if (impl == null)
if (impl is null)
{ {
var platformThreading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); var st = Stopwatch.StartNew();
if (platformThreading != null) _timeProvider = () => st.ElapsedMilliseconds;
impl = new LegacyDispatcherImpl(platformThreading);
else
impl = new NullDispatcherImpl();
} }
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;
/// <summary> /// <summary>
/// Checks that the current thread is the UI thread. /// Checks that the current thread is the UI thread.
/// </summary> /// </summary>
public bool CheckAccess() => _impl.CurrentThreadIsLoopThread; public bool CheckAccess() => Thread.CurrentThread == _thread;
/// <summary> /// <summary>
/// Checks that the current thread is the UI thread and throws if not. /// Checks that the current thread is the UI thread and throws if not.
@ -89,15 +89,64 @@ public partial class Dispatcher : IDispatcher
[DoesNotReturn] [DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
static void ThrowVerifyAccess() 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(); ThrowVerifyAccess();
} }
} }
public Thread Thread => _thread;
internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority) internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority)
{ {
DispatcherPriority.Validate(priority, nameof(priority)); DispatcherPriority.Validate(priority, nameof(priority));
var index = priority - DispatcherPriority.MinValue; var index = priority - DispatcherPriority.MinValue;
return _priorityContexts[index] ??= new(this, priority); 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();
}
} }

31
src/Avalonia.Base/Threading/IDispatcherImpl.cs

@ -80,33 +80,4 @@ internal class LegacyDispatcherImpl : IDispatcherImpl
_timer = null; _timer = null;
Timer?.Invoke(); 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)
{
}
}

1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -55,7 +55,6 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>() .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(Keyboard) .Bind<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(instance) .Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -114,8 +114,8 @@ namespace Avalonia.Native
var clipboardImpl = new ClipboardImpl(_factory.CreateClipboard()); var clipboardImpl = new ClipboardImpl(_factory.CreateClipboard());
var clipboard = new Clipboard(clipboardImpl); var clipboard = new Clipboard(clipboardImpl);
Dispatcher.InitializeUIThreadDispatcher(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()));
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
.Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind<IScreenImpl>().ToConstant(new ScreenImpl(_factory.CreateScreens)) .Bind<IScreenImpl>().ToConstant(new ScreenImpl(_factory.CreateScreens))
.Bind<IPlatformIconLoader>().ToSingleton<IconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()

3
src/Avalonia.Native/CallbackBase.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading;
using Avalonia.MicroCom; using Avalonia.MicroCom;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
@ -11,7 +12,7 @@ namespace Avalonia.Native
{ {
public void RaiseException(Exception e) public void RaiseException(Exception e)
{ {
if (AvaloniaLocator.Current.GetService<IDispatcherImpl>() is DispatcherImpl dispatcherImpl) if(Dispatcher.FromThread(Thread.CurrentThread) is { PlatformImpl: DispatcherImpl dispatcherImpl })
{ {
dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e)); dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e));
} }

11
src/Avalonia.X11/X11Platform.cs

@ -42,6 +42,7 @@ namespace Avalonia.X11
public X11Globals Globals { get; private set; } = null!; public X11Globals Globals { get; private set; } = null!;
public XResources Resources { get; private set; } = null!; public XResources Resources { get; private set; } = null!;
public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new(); public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new();
public IX11PlatformDispatcher DispatcherImpl { get; private set; } = null!;
public void Initialize(X11PlatformOptions options) public void Initialize(X11PlatformOptions options)
{ {
@ -79,10 +80,12 @@ namespace Avalonia.X11
var clipboard = new Input.Platform.Clipboard(clipboardImpl); var clipboard = new Input.Platform.Clipboard(clipboardImpl);
AvaloniaLocator.CurrentMutable.BindToSelf(this) AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IWindowingPlatform>().ToConstant(this) .Bind<IWindowingPlatform>().ToConstant(this);
.Bind<IDispatcherImpl>().ToConstant<IDispatcherImpl>(options.UseGLibMainLoop DispatcherImpl = options.UseGLibMainLoop
? new GlibDispatcherImpl(this) ? new GlibDispatcherImpl(this)
: new X11PlatformThreading(this)) : new X11PlatformThreading(this);
Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl);
AvaloniaLocator.CurrentMutable
.Bind<IRenderTimer>().ToConstant(timer) .Bind<IRenderTimer>().ToConstant(timer)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super")) .Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))

14
src/Avalonia.X11/XEmbedPlug.cs

@ -13,19 +13,20 @@ public class XEmbedPlug : IDisposable
private Color _backgroundColor; private Color _backgroundColor;
private readonly X11Info _x11; private readonly X11Info _x11;
private readonly X11Window.XEmbedClientWindowMode _mode; private readonly X11Window.XEmbedClientWindowMode _mode;
private readonly AvaloniaX11Platform _platform;
private XEmbedPlug(IntPtr? parentXid) private XEmbedPlug(IntPtr? parentXid)
{ {
var platform = AvaloniaLocator.Current.GetRequiredService<AvaloniaX11Platform>(); _platform = AvaloniaLocator.Current.GetRequiredService<AvaloniaX11Platform>();
_mode = new X11Window.XEmbedClientWindowMode(); _mode = new X11Window.XEmbedClientWindowMode();
_root = new EmbeddableControlRoot(new X11Window(platform, null, _mode)); _root = new EmbeddableControlRoot(new X11Window(_platform, null, _mode));
_root.Prepare(); _root.Prepare();
_x11 = platform.Info; _x11 = _platform.Info;
if (parentXid.HasValue) 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 // 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 private EmbeddableControlRoot Root
@ -60,8 +61,7 @@ public class XEmbedPlug : IDisposable
public void ProcessInteractiveResize(PixelSize size) public void ProcessInteractiveResize(PixelSize size)
{ {
var events = _platform.DispatcherImpl;
var events = (IX11PlatformDispatcher)AvaloniaLocator.Current.GetRequiredService<IDispatcherImpl>();
events.EventDispatcher.DispatchX11Events(CancellationToken.None); events.EventDispatcher.DispatchX11Events(CancellationToken.None);
_mode.ProcessInteractiveResize(size); _mode.ProcessInteractiveResize(size);
Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender); Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender);

8
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -97,15 +97,17 @@ internal class BrowserWindowingPlatform : IWindowingPlatform
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { })) .Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>(); .Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
if (IsManagedDispatcherEnabled) if (IsManagedDispatcherEnabled)
{ {
EventGrouperDispatchQueue = new(); EventGrouperDispatchQueue = new();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant( Dispatcher.InitializeUIThreadDispatcher(
new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue))); new ManagedDispatcherImpl(
new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
} }
else else
{ {
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>(); Dispatcher.InitializeUIThreadDispatcher(new BrowserDispatcherImpl());
} }
// GC thread is the same as the main one when MT is disabled // GC thread is the same as the main one when MT is disabled

1
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -79,7 +79,6 @@ namespace Avalonia.Headless
var clipboard = new Clipboard(clipboardImpl); var clipboard = new Clipboard(clipboardImpl);
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IClipboardImpl>().ToConstant(clipboardImpl) .Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard) .Bind<IClipboard>().ToConstant(clipboard)
.Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>() .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()

2
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -62,8 +62,8 @@ namespace Avalonia.LinuxFramebuffer
? new UiThreadRenderTimer(opts.Fps) ? new UiThreadRenderTimer(opts.Fps)
: new DefaultRenderTimer(opts.Fps); : new DefaultRenderTimer(opts.Fps);
Dispatcher.InitializeUIThreadDispatcher(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new EpollDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)))
.Bind<IRenderTimer>().ToConstant(timer) .Bind<IRenderTimer>().ToConstant(timer)
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>() .Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice()) .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())

3
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -85,6 +85,8 @@ namespace Avalonia.Win32
SetDpiAwareness(); SetDpiAwareness();
Dispatcher.InitializeUIThreadDispatcher(s_instance._dispatcher);
var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60);
var clipboardImpl = new ClipboardImpl(); var clipboardImpl = new ClipboardImpl();
var clipboard = new Clipboard(clipboardImpl); var clipboard = new Clipboard(clipboardImpl);
@ -96,7 +98,6 @@ namespace Avalonia.Win32
.Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance) .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>() .Bind<IPlatformSettings>().ToSingleton<Win32PlatformSettings>()
.Bind<IScreenImpl>().ToSingleton<ScreenImpl>() .Bind<IScreenImpl>().ToSingleton<ScreenImpl>()
.Bind<IDispatcherImpl>().ToConstant(s_instance._dispatcher)
.Bind<IRenderTimer>().ToConstant(renderTimer) .Bind<IRenderTimer>().ToConstant(renderTimer)
.Bind<IWindowingPlatform>().ToConstant(s_instance) .Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)

2
src/iOS/Avalonia.iOS/Platform.cs

@ -77,6 +77,7 @@ namespace Avalonia.iOS
Timer ??= new DisplayLinkTimer(); Timer ??= new DisplayLinkTimer();
var keyboard = new KeyboardDevice(); var keyboard = new KeyboardDevice();
Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl.Instance);
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IPlatformGraphics>().ToConstant(Graphics) .Bind<IPlatformGraphics>().ToConstant(Graphics)
.Bind<ICursorFactory>().ToConstant(new CursorFactoryStub()) .Bind<ICursorFactory>().ToConstant(new CursorFactoryStub())
@ -93,7 +94,6 @@ namespace Avalonia.iOS
{ Key.Up , "↑" } { Key.Up , "↑" }
}, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥")) }, ctrl: "⌃", meta: "⌘", shift: "⇧", alt: "⌥"))
.Bind<IRenderTimer>().ToConstant(Timer) .Bind<IRenderTimer>().ToConstant(Timer)
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard); .Bind<IKeyboardDevice>().ToConstant(keyboard);
if (appDelegate is not null) if (appDelegate is not null)

119
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Threading; using System.Threading;
@ -19,7 +20,7 @@ using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
{ {
public class AvaloniaObjectTests_Binding public class AvaloniaObjectTests_Binding : ScopedTestBase
{ {
[Fact] [Fact]
public void Bind_Sets_Current_Value() public void Bind_Sets_Current_Value()
@ -858,37 +859,28 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)] [InlineData(BindingPriority.Style)]
public void Typed_Bind_Executes_On_UIThread(BindingPriority priority) public void Typed_Bind_Executes_On_UIThread(BindingPriority priority)
{ {
AsyncContext.Run(async () => using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
var source = new Subject<string>(); var source = new Subject<string>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId; var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0; var raised = 0;
var dispatcherMock = new Mock<IDispatcherImpl>();
dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
dispatcherImpl: dispatcherMock.Object);
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised; ++raised;
}; };
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar")); target.Bind(Class1.FooProperty, source, priority);
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Assert.Equal(1, raised); Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
}
}); Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
} }
[Theory] [Theory]
@ -896,37 +888,28 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)] [InlineData(BindingPriority.Style)]
public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority) public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority)
{ {
AsyncContext.Run(async () => using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
var source = new Subject<object>(); var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId; var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0; var raised = 0;
var dispatcherMock = new Mock<IDispatcherImpl>();
dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
dispatcherImpl: dispatcherMock.Object);
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised; ++raised;
}; };
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar")); target.Bind(Class1.FooProperty, source, priority);
Dispatcher.UIThread.RunJobs();
Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Assert.Equal(1, raised); Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
}
}); Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
} }
[Theory] [Theory]
@ -934,59 +917,41 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)] [InlineData(BindingPriority.Style)]
public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority) public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority)
{ {
AsyncContext.Run(async () => using var _ = UnitTestApplication.Start();
{ var target = new Class1();
var target = new Class1(); var source = new Subject<BindingValue<string>>();
var source = new Subject<BindingValue<string>>(); var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var currentThreadId = Thread.CurrentThread.ManagedThreadId; var raised = 0;
var raised = 0;
var threadingInterfaceMock = new Mock<IDispatcherImpl>();
threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
dispatcherImpl: threadingInterfaceMock.Object);
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId); Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised; ++raised;
}; };
using (UnitTestApplication.Start(services)) target.Bind(Class1.FooProperty, source, priority);
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar")); ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.GetValue(Class1.FooProperty)); Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised); Assert.Equal(1, raised);
}
});
} }
[Fact] [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 target = new Class1();
var source = new Subject<double>(); var source = new Subject<double>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId; target.Bind(Class1.QuxProperty, source);
var threadingInterfaceMock = new Mock<IDispatcherImpl>();
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);
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] [Fact]

24
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -492,19 +492,13 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void Bind_Executes_On_UIThread() public void Bind_Executes_On_UIThread()
{ {
AsyncContext.Run(async () => using(UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
var source = new Subject<object>(); var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId; var currentThreadId = Thread.CurrentThread.ManagedThreadId;
var raised = 0; var raised = 0;
var dispatcherMock = new Mock<IDispatcherImpl>();
dispatcherMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
.Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
var services = new TestServices(
dispatcherImpl: dispatcherMock.Object);
target.PropertyChanged += (s, e) => target.PropertyChanged += (s, e) =>
{ {
@ -512,17 +506,15 @@ namespace Avalonia.Base.UnitTests
++raised; ++raised;
}; };
using (UnitTestApplication.Start(services))
{ target.Bind(Class1.FooProperty, source);
target.Bind(Class1.FooProperty, source);
await Task.Run(() => source.OnNext("foobar")); ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.Foo); Assert.Equal("foobar", target.Foo);
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
});
} }
[Fact] [Fact]

88
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Runtime.ExceptionServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
@ -9,49 +10,43 @@ using Xunit;
namespace Avalonia.Base.UnitTests namespace Avalonia.Base.UnitTests
{ {
public class AvaloniaObjectTests_Threading public class AvaloniaObjectTests_Threading : ScopedTestBase
{ {
private TestDipatcherImpl _threading = new(true); void AssertThrowsOnDifferentThread(Action cb)
[Fact]
public void AvaloniaObject_Constructor_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new TestDipatcherImpl()))) Assert.Throws<InvalidOperationException>(() =>
{ ThreadRunHelper.RunOnDedicatedThread(cb).GetAwaiter().GetResult());
Assert.Throws<InvalidOperationException>(() => new Class1());
}
} }
[Fact] [Fact]
public void StyledProperty_GetValue_Should_Throw() public void StyledProperty_GetValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; target.GetValue(Class1.StyledProperty);
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.StyledProperty));
AssertThrowsOnDifferentThread(() => target.GetValue(Class1.StyledProperty));
} }
} }
[Fact] [Fact]
public void StyledProperty_SetValue_Should_Throw() public void StyledProperty_SetValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.SetValue(Class1.StyledProperty, "foo"));
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.StyledProperty, "foo"));
} }
} }
[Fact] [Fact]
public void Setting_StyledProperty_Binding_Should_Throw() public void Setting_StyledProperty_Binding_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() =>
Assert.Throws<InvalidOperationException>(() =>
target.Bind( target.Bind(
Class1.StyledProperty, Class1.StyledProperty,
new BehaviorSubject<string>("foo"))); new BehaviorSubject<string>("foo")));
@ -61,55 +56,50 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void StyledProperty_ClearValue_Should_Throw() public void StyledProperty_ClearValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.StyledProperty));
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.StyledProperty));
} }
} }
[Fact] [Fact]
public void StyledProperty_IsSet_Should_Throw() public void StyledProperty_IsSet_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.IsSet(Class1.StyledProperty));
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.StyledProperty));
} }
} }
[Fact] [Fact]
public void DirectProperty_GetValue_Should_Throw() public void DirectProperty_GetValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.GetValue(Class1.DirectProperty));
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.DirectProperty));
} }
} }
[Fact] [Fact]
public void DirectProperty_SetValue_Should_Throw() public void DirectProperty_SetValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.SetValue(Class1.DirectProperty, "foo"));
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.DirectProperty, "foo"));
} }
} }
[Fact] [Fact]
public void Setting_DirectProperty_Binding_Should_Throw() public void Setting_DirectProperty_Binding_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() =>
Assert.Throws<InvalidOperationException>(() =>
target.Bind( target.Bind(
Class1.DirectProperty, Class1.DirectProperty,
new BehaviorSubject<string>("foo"))); new BehaviorSubject<string>("foo")));
@ -119,22 +109,20 @@ namespace Avalonia.Base.UnitTests
[Fact] [Fact]
public void DirectProperty_ClearValue_Should_Throw() public void DirectProperty_ClearValue_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.DirectProperty));
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.DirectProperty));
} }
} }
[Fact] [Fact]
public void DirectProperty_IsSet_Should_Throw() public void DirectProperty_IsSet_Should_Throw()
{ {
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading))) using (UnitTestApplication.Start())
{ {
var target = new Class1(); var target = new Class1();
_threading.CurrentThreadIsLoopThread = false; AssertThrowsOnDifferentThread(() => target.IsSet(Class1.DirectProperty));
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.DirectProperty));
} }
} }
@ -146,27 +134,5 @@ namespace Avalonia.Base.UnitTests
public static readonly DirectProperty<Class1, string?> DirectProperty = public static readonly DirectProperty<Class1, string?> DirectProperty =
AvaloniaProperty.RegisterDirect<Class1, string?>("Qux", _ => null, (o, v) => { }); AvaloniaProperty.RegisterDirect<Class1, string?>("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();
}
} }
} }

155
tests/Avalonia.Base.UnitTests/DispatcherTests.Exception.cs

@ -1,43 +1,76 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests; 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 // 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 const string ExpectedExceptionText = "Exception thrown inside Dispatcher.Invoke / Dispatcher.BeginInvoke.";
private int _numberOfHandlerOnUnhandledEventInvoked; private int _numberOfHandlerOnUnhandledEventInvoked;
private int _numberOfHandlerOnUnhandledEventFilterInvoked; private int _numberOfHandlerOnUnhandledEventFilterInvoked;
private Dispatcher _uiThread;
public DispatcherTests() public DispatcherTests()
{ {
_numberOfHandlerOnUnhandledEventInvoked = 0; _numberOfHandlerOnUnhandledEventInvoked = 0;
_numberOfHandlerOnUnhandledEventFilterInvoked = 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] [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 dispatcher = Dispatcher.CurrentDispatcher;
var disp = new Dispatcher(impl); 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 handled = false;
var executed = false; var executed = false;
disp.UnhandledException += (sender, args) => _uiThread.UnhandledException += (sender, args) =>
{ {
handled = true; handled = true;
args.Handled = true; args.Handled = true;
}; };
disp.Post(() => ThrowAnException()); _uiThread.Post(() => ThrowAnException());
disp.Post(() => executed = true); _uiThread.Post(() => executed = true);
disp.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(handled); Assert.True(handled);
Assert.True(executed); Assert.True(executed);
@ -46,14 +79,11 @@ public partial class DispatcherTests
[Fact] [Fact]
public void SyncContextExceptionCanBeHandledWithPost() public void SyncContextExceptionCanBeHandledWithPost()
{ {
var impl = new ManagedDispatcherImpl(null); var syncContext = _uiThread.GetContextWithPriority(DispatcherPriority.Background);
var disp = new Dispatcher(impl);
var syncContext = disp.GetContextWithPriority(DispatcherPriority.Background);
var handled = false; var handled = false;
var executed = false; var executed = false;
disp.UnhandledException += (sender, args) => _uiThread.UnhandledException += (sender, args) =>
{ {
handled = true; handled = true;
args.Handled = true; args.Handled = true;
@ -62,7 +92,7 @@ public partial class DispatcherTests
syncContext.Post(_ => ThrowAnException(), null); syncContext.Post(_ => ThrowAnException(), null);
syncContext.Post(_ => executed = true, null); syncContext.Post(_ => executed = true, null);
disp.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(handled); Assert.True(handled);
Assert.True(executed); Assert.True(executed);
@ -71,24 +101,22 @@ public partial class DispatcherTests
[Fact] [Fact]
public void CanRemoveDispatcherExceptionHandler() public void CanRemoveDispatcherExceptionHandler()
{ {
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
var caughtCorrectException = false; var caughtCorrectException = false;
dispatcher.UnhandledExceptionFilter += _uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled; HandlerOnUnhandledExceptionNotHandled;
dispatcher.UnhandledExceptionFilter -= _uiThread.UnhandledExceptionFilter -=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException -= _uiThread.UnhandledException -=
HandlerOnUnhandledExceptionNotHandled; HandlerOnUnhandledExceptionNotHandled;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@ -103,19 +131,16 @@ public partial class DispatcherTests
[Fact] [Fact]
public void CanHandleExceptionWithUnhandledException() public void CanHandleExceptionWithUnhandledException()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled; HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = true; var caughtCorrectException = true;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception) catch (Exception)
{ {
@ -131,20 +156,17 @@ public partial class DispatcherTests
[Fact] [Fact]
public void InvokeMethodDoesntTriggerUnhandledException() public void InvokeMethodDoesntTriggerUnhandledException()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled; HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false; var caughtCorrectException = false;
try try
{ {
// Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. // 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); _uiThread.Invoke(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@ -160,21 +182,18 @@ public partial class DispatcherTests
[Fact] [Fact]
public void InvokeAsyncMethodDoesntTriggerUnhandledException() public void InvokeAsyncMethodDoesntTriggerUnhandledException()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled; HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false; var caughtCorrectException = false;
try try
{ {
// Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException. // 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(); op.Wait();
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@ -190,19 +209,16 @@ public partial class DispatcherTests
[Fact] [Fact]
public void CanRethrowExceptionWithUnhandledException() public void CanRethrowExceptionWithUnhandledException()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled; HandlerOnUnhandledExceptionNotHandled;
var caughtCorrectException = false; var caughtCorrectException = false;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@ -217,23 +233,20 @@ public partial class DispatcherTests
[Fact] [Fact]
public void MultipleUnhandledExceptionFilterCannotResetRequestCatchFlag() public void MultipleUnhandledExceptionFilterCannotResetRequestCatchFlag()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterNotRequestCatch; HandlerOnUnhandledExceptionFilterNotRequestCatch;
dispatcher.UnhandledExceptionFilter += _uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled; HandlerOnUnhandledExceptionNotHandled;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled; HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false; var caughtCorrectException = false;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {
@ -248,22 +261,19 @@ public partial class DispatcherTests
[Fact] [Fact]
public void MultipleUnhandledExceptionCannotResetHandleFlag() public void MultipleUnhandledExceptionCannotResetHandleFlag()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch; HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled; HandlerOnUnhandledExceptionHandled;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled; HandlerOnUnhandledExceptionNotHandled;
var caughtCorrectException = true; var caughtCorrectException = true;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception) catch (Exception)
{ {
@ -279,19 +289,16 @@ public partial class DispatcherTests
[Fact] [Fact]
public void CanPushFrameAndShutdownDispatcherFromUnhandledException() public void CanPushFrameAndShutdownDispatcherFromUnhandledException()
{ {
var impl = new ManagedDispatcherImpl(null); _uiThread.UnhandledExceptionFilter +=
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame; HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame;
dispatcher.UnhandledException += _uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandledPushFrame; HandlerOnUnhandledExceptionHandledPushFrame;
var caughtCorrectException = false; var caughtCorrectException = false;
try try
{ {
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal); _uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken); _uiThread.RunJobs(null, TestContext.Current.CancellationToken);
} }
catch (Exception e) catch (Exception e)
{ {

35
tests/Avalonia.Base.UnitTests/DispatcherTests.cs

@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Utilities; using Avalonia.Utilities;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests; namespace Avalonia.Base.UnitTests;
@ -121,11 +122,11 @@ public partial class DispatcherTests
public void DispatcherExecutesJobsAccordingToPriority() public void DispatcherExecutesJobsAccordingToPriority()
{ {
var impl = new SimpleDispatcherImpl(); var impl = new SimpleDispatcherImpl();
var disp = new Dispatcher(impl); Dispatcher.InitializeUIThreadDispatcher(impl);
var actions = new List<string>(); var actions = new List<string>();
disp.Post(()=>actions.Add("Background"), DispatcherPriority.Background); _uiThread.Post(()=>actions.Add("Background"), DispatcherPriority.Background);
disp.Post(()=>actions.Add("Render"), DispatcherPriority.Render); _uiThread.Post(()=>actions.Add("Render"), DispatcherPriority.Render);
disp.Post(()=>actions.Add("Input"), DispatcherPriority.Input); _uiThread.Post(()=>actions.Add("Input"), DispatcherPriority.Input);
Assert.True(impl.AskedForSignal); Assert.True(impl.AskedForSignal);
impl.ExecuteSignal(); impl.ExecuteSignal();
Assert.Equal(new[] { "Render", "Input", "Background" }, actions); Assert.Equal(new[] { "Render", "Input", "Background" }, actions);
@ -135,12 +136,11 @@ public partial class DispatcherTests
public void DispatcherPreservesOrderWhenChangingPriority() public void DispatcherPreservesOrderWhenChangingPriority()
{ {
var impl = new SimpleDispatcherImpl(); var impl = new SimpleDispatcherImpl();
var disp = new Dispatcher(impl); Dispatcher.InitializeUIThreadDispatcher(impl);
var actions = new List<string>(); var actions = new List<string>();
var toPromote = disp.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background, TestContext.Current.CancellationToken); var toPromote = _uiThread.InvokeAsync(()=>actions.Add("PromotedRender"), DispatcherPriority.Background, TestContext.Current.CancellationToken);
var toPromote2 = disp.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input, TestContext.Current.CancellationToken); var toPromote2 = _uiThread.InvokeAsync(()=>actions.Add("PromotedRender2"), DispatcherPriority.Input, TestContext.Current.CancellationToken);
disp.Post(() => actions.Add("Render"), DispatcherPriority.Render); _uiThread.Post(() => actions.Add("Render"), DispatcherPriority.Render);
toPromote.Priority = DispatcherPriority.Render; toPromote.Priority = DispatcherPriority.Render;
toPromote2.Priority = DispatcherPriority.Render; toPromote2.Priority = DispatcherPriority.Render;
@ -154,12 +154,13 @@ public partial class DispatcherTests
public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached() public void DispatcherStopsItemProcessingWhenInteractivityDeadlineIsReached()
{ {
var impl = new SimpleDispatcherImpl(); var impl = new SimpleDispatcherImpl();
var disp = new Dispatcher(impl); Dispatcher.ResetForUnitTests();
_uiThread = new Dispatcher(impl);
var actions = new List<int>(); var actions = new List<int>();
for (var c = 0; c < 10; c++) for (var c = 0; c < 10; c++)
{ {
var itemId = c; var itemId = c;
disp.Post(() => _uiThread.Post(() =>
{ {
actions.Add(itemId); actions.Add(itemId);
impl.Now += 20; impl.Now += 20;
@ -195,14 +196,17 @@ public partial class DispatcherTests
[Fact] [Fact]
public void DispatcherStopsItemProcessingWhenInputIsPending() public void DispatcherStopsItemProcessingWhenInputIsPending()
{ {
Dispatcher.ResetForUnitTests();
var impl = new SimpleDispatcherImpl(); var impl = new SimpleDispatcherImpl();
impl.TestInputPending = true; impl.TestInputPending = true;
var disp = new Dispatcher(impl); _uiThread = new Dispatcher(impl);
var actions = new List<int>(); var actions = new List<int>();
for (var c = 0; c < 10; c++) for (var c = 0; c < 10; c++)
{ {
var itemId = c; var itemId = c;
disp.Post(() => _uiThread.Post(() =>
{ {
actions.Add(itemId); actions.Add(itemId);
if (itemId == 0 || itemId == 3 || itemId == 7) if (itemId == 0 || itemId == 3 || itemId == 7)
@ -249,10 +253,10 @@ public partial class DispatcherTests
public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground) public void CanWaitForDispatcherOperationFromTheSameThread(bool controlled, bool foreground)
{ {
var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl(); var impl = controlled ? new SimpleControlledDispatcherImpl() : new SimpleDispatcherImpl();
var disp = new Dispatcher(impl); Dispatcher.InitializeUIThreadDispatcher(impl);
bool finished = false; bool finished = false;
disp.InvokeAsync(() => finished = true, _uiThread.InvokeAsync(() => finished = true,
foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait(); foreground ? DispatcherPriority.Default : DispatcherPriority.Background).Wait();
Assert.True(finished); Assert.True(finished);
@ -268,7 +272,6 @@ public partial class DispatcherTests
public DispatcherServices(IDispatcherImpl impl) public DispatcherServices(IDispatcherImpl impl)
{ {
_scope = AvaloniaLocator.EnterScope(); _scope = AvaloniaLocator.EnterScope();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(impl);
Dispatcher.ResetForUnitTests(); Dispatcher.ResetForUnitTests();
SynchronizationContext.SetSynchronizationContext(null); SynchronizationContext.SetSynchronizationContext(null);
} }

8
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@ -18,17 +18,13 @@ namespace Avalonia.Base.UnitTests.Input
{ {
using var scope = AvaloniaLocator.EnterScope(); using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock<IPlatformSettings>(); var settingsMock = new Mock<IPlatformSettings>();
var dispatcherMock = new Mock<IDispatcherImpl>();
dispatcherMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
AvaloniaLocator.CurrentMutable.BindToSelf(this) AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(settingsMock.Object); .Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
using var app = UnitTestApplication.Start( using var app = UnitTestApplication.Start(
new TestServices( new TestServices(
inputManager: new InputManager(), inputManager: new InputManager()));
dispatcherImpl: dispatcherMock.Object));
var renderer = new Mock<IHitTester>(); var renderer = new Mock<IHitTester>();
var device = new MouseDevice(); var device = new MouseDevice();

2
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

@ -213,7 +213,7 @@ namespace Avalonia.Input.UnitTests
private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan()) private IDisposable UnitTestApp(TimeSpan doubleClickTime = new TimeSpan())
{ {
var unitTestApp = UnitTestApplication.Start( var unitTestApp = UnitTestApplication.Start(
new TestServices(inputManager: new InputManager(), dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true))); new TestServices(inputManager: new InputManager()));
var iSettingsMock = new Mock<IPlatformSettings>(); var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny<PointerType>())).Returns(doubleClickTime); iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny<PointerType>())).Returns(doubleClickTime);
iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16)); iSettingsMock.Setup(x => x.GetDoubleTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));

6
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -28,7 +28,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Should_Set_ExitCode_After_Shutdown() 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()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); lifetime.SetupCore(Array.Empty<string>());
@ -326,7 +326,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event() 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()) using (var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
var lifetimeEvents = new Mock<IPlatformLifetimeEventsImpl>(); var lifetimeEvents = new Mock<IPlatformLifetimeEventsImpl>();
@ -475,7 +475,7 @@ namespace Avalonia.Controls.UnitTests
[Fact] [Fact]
public void Shutdown_NotCancellable_By_Preventing_Window_Close() 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()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); lifetime.SetupCore(Array.Empty<string>());

3
tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs

@ -1,7 +1,6 @@
using System; using System;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives namespace Avalonia.Controls.UnitTests.Primitives

2
tests/Avalonia.LeakTests/AvaloniaObjectTests.cs

@ -149,7 +149,7 @@ namespace Avalonia.LeakTests
} }
var weakTarget = SetupBinding(); var weakTarget = SetupBinding();
CollectGarbage(); CollectGarbage();
Assert.False(weakTarget.IsAlive); Assert.False(weakTarget.IsAlive);
} }

3
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs

@ -24,8 +24,9 @@ public class BindingTests_Delay : ScopedTestBase, IDisposable
public BindingTests_Delay() public BindingTests_Delay()
{ {
_app = UnitTestApplication.Start(new(keyboardDevice: () => new KeyboardDevice()));
_dispatcher = new ManualTimerDispatcher(); _dispatcher = new ManualTimerDispatcher();
_app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, keyboardDevice: () => new KeyboardDevice())); _ = new Dispatcher(_dispatcher);
_source = new BindingTests.Source { Foo = InitialFooValue }; _source = new BindingTests.Source { Foo = InitialFooValue };
_target = new TextBox { DataContext = _source }; _target = new TextBox { DataContext = _source };

7
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@ -4,11 +4,10 @@ using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Xunit;
using Avalonia.Utilities;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.UnitTests;
using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Parsers;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers namespace Avalonia.Markup.UnitTests.Parsers
{ {

1
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs

@ -6,7 +6,6 @@ using Avalonia.Diagnostics;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Xunit; using Xunit;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Data.Core.Parsers; using Avalonia.Data.Core.Parsers;

10
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -41,5 +41,15 @@
</Compile> </Compile>
<Compile Include="../Shared/ScopedSanityCheck.cs"/> <Compile Include="../Shared/ScopedSanityCheck.cs"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Update="xunit.runner.console" Version="2.9.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Update="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
</Project> </Project>

34
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -28,16 +28,9 @@ namespace Avalonia.Skia.RenderTests;
static class TestRenderHelper static class TestRenderHelper
{ {
private static readonly TestDispatcherImpl s_dispatcherImpl =
new TestDispatcherImpl();
static TestRenderHelper() static TestRenderHelper()
{ {
SkiaPlatform.Initialize(); SkiaPlatform.Initialize();
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>()
.ToConstant(s_dispatcherImpl);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader()); AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
AvaloniaLocator.CurrentMutable.Bind<ITextShaperImpl>().ToConstant(new HarfBuzzTextShaper()); AvaloniaLocator.CurrentMutable.Bind<ITextShaperImpl>().ToConstant(new HarfBuzzTextShaper());
} }
@ -48,7 +41,7 @@ static class TestRenderHelper
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path);
Assert.NotNull(dir); Assert.NotNull(dir);
if (!Directory.Exists(dir)) if (!Directory.Exists(dir))
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
@ -110,13 +103,14 @@ static class TestRenderHelper
public static void BeginTest() public static void BeginTest()
{ {
s_dispatcherImpl.MainThread = Thread.CurrentThread; Dispatcher.ResetBeforeUnitTests();
} }
public static void EndTest() public static void EndTest()
{ {
if (Dispatcher.UIThread.CheckAccess()) if (Dispatcher.UIThread.CheckAccess())
Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs();
Dispatcher.ResetForUnitTests();
} }
public static string GetTestsDirectory() public static string GetTestsDirectory()
@ -131,28 +125,6 @@ static class TestRenderHelper
Assert.NotNull(path); Assert.NotNull(path);
return 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) public static void AssertCompareImages(string actualPath, string expectedPath)
{ {

24
tests/Avalonia.UnitTests/TestServices.cs

@ -15,34 +15,32 @@ namespace Avalonia.UnitTests
{ {
public class TestServices public class TestServices
{ {
public static readonly TestServices StyledWindow = new TestServices( public static TestServices StyledWindow => new TestServices(
assetLoader: new StandardAssetLoader(), assetLoader: new StandardAssetLoader(),
platform: new StandardRuntimePlatform(), platform: new StandardRuntimePlatform(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
standardCursorFactory: new HeadlessCursorFactoryStub(), standardCursorFactory: new HeadlessCursorFactoryStub(),
theme: () => CreateSimpleTheme(), theme: () => CreateSimpleTheme(),
dispatcherImpl: new NullDispatcherImpl(),
fontManagerImpl: new TestFontManager(), fontManagerImpl: new TestFontManager(),
textShaperImpl: new HarfBuzzTextShaper(), textShaperImpl: new HarfBuzzTextShaper(),
windowingPlatform: new MockWindowingPlatform()); windowingPlatform: new MockWindowingPlatform());
public static readonly TestServices MockPlatformRenderInterface = new TestServices( public static TestServices MockPlatformRenderInterface => new TestServices(
assetLoader: new StandardAssetLoader(), assetLoader: new StandardAssetLoader(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
fontManagerImpl: new TestFontManager(), fontManagerImpl: new TestFontManager(),
textShaperImpl: new HarfBuzzTextShaper()); textShaperImpl: new HarfBuzzTextShaper());
public static readonly TestServices MockPlatformWrapper = new TestServices( public static TestServices MockPlatformWrapper => new TestServices(
platform: Mock.Of<IRuntimePlatform>()); platform: Mock.Of<IRuntimePlatform>());
public static readonly TestServices MockThreadingInterface = new TestServices( public static TestServices MockThreadingInterface => new TestServices(
dispatcherImpl: new NullDispatcherImpl(),
assetLoader: new StandardAssetLoader()); assetLoader: new StandardAssetLoader());
public static readonly TestServices MockWindowingPlatform = new TestServices( public static TestServices MockWindowingPlatform => new TestServices(
windowingPlatform: new MockWindowingPlatform()); windowingPlatform: new MockWindowingPlatform());
public static readonly TestServices RealFocus = new TestServices( public static TestServices RealFocus => new TestServices(
keyboardDevice: () => new KeyboardDevice(), keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(), keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(), inputManager: new InputManager(),
@ -51,7 +49,7 @@ namespace Avalonia.UnitTests
fontManagerImpl: new TestFontManager(), fontManagerImpl: new TestFontManager(),
textShaperImpl: new HarfBuzzTextShaper()); textShaperImpl: new HarfBuzzTextShaper());
public static readonly TestServices FocusableWindow = new TestServices( public static TestServices FocusableWindow => new TestServices(
keyboardDevice: () => new KeyboardDevice(), keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(), keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(), inputManager: new InputManager(),
@ -60,12 +58,11 @@ namespace Avalonia.UnitTests
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
standardCursorFactory: new HeadlessCursorFactoryStub(), standardCursorFactory: new HeadlessCursorFactoryStub(),
theme: () => CreateSimpleTheme(), theme: () => CreateSimpleTheme(),
dispatcherImpl: new NullDispatcherImpl(),
fontManagerImpl: new TestFontManager(), fontManagerImpl: new TestFontManager(),
textShaperImpl: new HarfBuzzTextShaper(), textShaperImpl: new HarfBuzzTextShaper(),
windowingPlatform: new MockWindowingPlatform()); windowingPlatform: new MockWindowingPlatform());
public static readonly TestServices TextServices = new TestServices( public static TestServices TextServices => new TestServices(
assetLoader: new StandardAssetLoader(), assetLoader: new StandardAssetLoader(),
renderInterface: new HeadlessPlatformRenderInterface(), renderInterface: new HeadlessPlatformRenderInterface(),
fontManagerImpl: new TestFontManager(), fontManagerImpl: new TestFontManager(),
@ -82,7 +79,6 @@ namespace Avalonia.UnitTests
IPlatformRenderInterface? renderInterface = null, IPlatformRenderInterface? renderInterface = null,
ICursorFactory? standardCursorFactory = null, ICursorFactory? standardCursorFactory = null,
Func<IStyle>? theme = null, Func<IStyle>? theme = null,
IDispatcherImpl? dispatcherImpl = null,
IFontManagerImpl? fontManagerImpl = null, IFontManagerImpl? fontManagerImpl = null,
ITextShaperImpl? textShaperImpl = null, ITextShaperImpl? textShaperImpl = null,
IWindowImpl? windowImpl = null, IWindowImpl? windowImpl = null,
@ -102,7 +98,6 @@ namespace Avalonia.UnitTests
TextShaperImpl = textShaperImpl; TextShaperImpl = textShaperImpl;
StandardCursorFactory = standardCursorFactory; StandardCursorFactory = standardCursorFactory;
Theme = theme; Theme = theme;
DispatcherImpl = dispatcherImpl;
WindowImpl = windowImpl; WindowImpl = windowImpl;
WindowingPlatform = windowingPlatform; WindowingPlatform = windowingPlatform;
} }
@ -120,7 +115,6 @@ namespace Avalonia.UnitTests
public ITextShaperImpl? TextShaperImpl { get; } public ITextShaperImpl? TextShaperImpl { get; }
public ICursorFactory? StandardCursorFactory { get; } public ICursorFactory? StandardCursorFactory { get; }
public Func<IStyle>? Theme { get; } public Func<IStyle>? Theme { get; }
public IDispatcherImpl? DispatcherImpl { get; }
public IWindowImpl? WindowImpl { get; } public IWindowImpl? WindowImpl { get; }
public IWindowingPlatform? WindowingPlatform { get; } public IWindowingPlatform? WindowingPlatform { get; }
@ -138,7 +132,6 @@ namespace Avalonia.UnitTests
IScheduler? scheduler = null, IScheduler? scheduler = null,
ICursorFactory? standardCursorFactory = null, ICursorFactory? standardCursorFactory = null,
Func<IStyle>? theme = null, Func<IStyle>? theme = null,
IDispatcherImpl? dispatcherImpl = null,
IFontManagerImpl? fontManagerImpl = null, IFontManagerImpl? fontManagerImpl = null,
ITextShaperImpl? textShaperImpl = null, ITextShaperImpl? textShaperImpl = null,
IWindowImpl? windowImpl = null, IWindowImpl? windowImpl = null,
@ -158,7 +151,6 @@ namespace Avalonia.UnitTests
textShaperImpl: textShaperImpl ?? TextShaperImpl, textShaperImpl: textShaperImpl ?? TextShaperImpl,
standardCursorFactory: standardCursorFactory ?? StandardCursorFactory, standardCursorFactory: standardCursorFactory ?? StandardCursorFactory,
theme: theme ?? Theme, theme: theme ?? Theme,
dispatcherImpl: dispatcherImpl ?? DispatcherImpl,
windowingPlatform: windowingPlatform ?? WindowingPlatform, windowingPlatform: windowingPlatform ?? WindowingPlatform,
windowImpl: windowImpl ?? WindowImpl); windowImpl: windowImpl ?? WindowImpl);
} }

2
tests/Avalonia.UnitTests/ThreadRunHelper.cs

@ -1,5 +1,3 @@
#nullable enable
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

2
tests/Avalonia.UnitTests/UnitTestApplication.cs

@ -52,6 +52,7 @@ namespace Avalonia.UnitTests
(AvaloniaLocator.Current.GetService<IToolTipService>() as ToolTipService)?.Dispose(); (AvaloniaLocator.Current.GetService<IToolTipService>() as ToolTipService)?.Dispose();
(AvaloniaLocator.Current.GetService<FontManager>() as IDisposable)?.Dispose(); (AvaloniaLocator.Current.GetService<FontManager>() as IDisposable)?.Dispose();
(AvaloniaLocator.Current.GetService<IInputManager>() as IDisposable)?.Dispose();
Dispatcher.ResetForUnitTests(); Dispatcher.ResetForUnitTests();
scope.Dispose(); scope.Dispose();
@ -84,7 +85,6 @@ namespace Avalonia.UnitTests
.Bind<IPlatformRenderInterface?>().ToConstant(Services.RenderInterface) .Bind<IPlatformRenderInterface?>().ToConstant(Services.RenderInterface)
.Bind<IFontManagerImpl?>().ToConstant(Services.FontManagerImpl) .Bind<IFontManagerImpl?>().ToConstant(Services.FontManagerImpl)
.Bind<ITextShaperImpl?>().ToConstant(Services.TextShaperImpl) .Bind<ITextShaperImpl?>().ToConstant(Services.TextShaperImpl)
.Bind<IDispatcherImpl?>().ToConstant(Services.DispatcherImpl)
.Bind<ICursorFactory?>().ToConstant(Services.StandardCursorFactory) .Bind<ICursorFactory?>().ToConstant(Services.StandardCursorFactory)
.Bind<IWindowingPlatform?>().ToConstant(Services.WindowingPlatform) .Bind<IWindowingPlatform?>().ToConstant(Services.WindowingPlatform)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

Loading…
Cancel
Save