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. 11
      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. 8
      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. 125
      src/Avalonia.Base/Threading/Dispatcher.cs
  12. 29
      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. 9
      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. 77
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  24. 16
      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. 6
      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. 3
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs
  33. 7
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  34. 1
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AvaloniaProperty.cs
  35. 10
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  36. 32
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  37. 24
      tests/Avalonia.UnitTests/TestServices.cs
  38. 2
      tests/Avalonia.UnitTests/ThreadRunHelper.cs
  39. 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>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</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>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Controls.Primitives.ChromeOverlayLayer</Target>
@ -697,6 +703,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Controls.dll</Right>
</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>
<DiagnosticId>CP0001</DiagnosticId>
<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();
Dispatcher.InitializeUIThreadDispatcher(new AndroidDispatcherImpl());
AvaloniaLocator.CurrentMutable
.Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new AndroidDispatcherImpl())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

11
src/Avalonia.Base/AvaloniaObject.cs

@ -34,7 +34,6 @@ namespace Avalonia
/// </summary>
public AvaloniaObject()
{
VerifyAccess();
_values = new ValueStore(this);
}
@ -109,16 +108,22 @@ namespace Avalonia
/// </summary>
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>
/// Returns a value indicating whether the current thread is the UI thread.
/// </summary>
/// <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>
/// Checks that the current thread is the UI thread and throws if not.
/// </summary>
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
public void VerifyAccess() => Dispatcher.VerifyAccess();
/// <summary>
/// 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
/// for processing.
/// </summary>
internal class InputManager : IInputManager
internal class InputManager : IInputManager, IDisposable
{
private readonly LightweightSubject<RawInputEventArgs> _preProcess = new();
private readonly LightweightSubject<RawInputEventArgs> _process = new();
@ -36,5 +36,12 @@ namespace Avalonia.Input
_process.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.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform;
using Avalonia.Threading;
@ -30,9 +31,7 @@ internal abstract class BatchStreamPoolBase<T> : IDisposable
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this);
if (
reclaimImmediately
|| (
AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null
&& AvaloniaLocator.Current.GetService<IDispatcherImpl>() == null))
|| Dispatcher.FromThread(Thread.CurrentThread) == null)
_reclaimImmediately = true;
else
StartUpdateTimer(startTimer, updateRef);

13
src/Avalonia.Base/StyledElement.cs

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

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

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

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

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

@ -3,7 +3,10 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Controls.Platform;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Threading;
@ -17,63 +20,60 @@ namespace Avalonia.Threading;
public partial class Dispatcher : IDispatcher
{
private IDispatcherImpl _impl;
private bool _initialized;
internal object InstanceLock { get; } = new();
private IControlledDispatcherImpl? _controlledImpl;
private static Dispatcher? s_uiThread;
private IDispatcherImplWithPendingInput? _pendingInputImpl;
private readonly IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl;
private IDispatcherImplWithExplicitBackgroundProcessing? _backgroundProcessingImpl;
private readonly Thread _thread;
private readonly AvaloniaSynchronizationContext?[] _priorityContexts =
new AvaloniaSynchronizationContext?[DispatcherPriority.MaxValue - DispatcherPriority.MinValue + 1];
internal Dispatcher(IDispatcherImpl impl)
internal Dispatcher(IDispatcherImpl? impl)
{
_impl = impl;
impl.Timer += OnOSTimer;
impl.Signaled += Signaled;
_controlledImpl = _impl as IControlledDispatcherImpl;
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput;
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing;
_maximumInputStarvationTime = _backgroundProcessingImpl == null ?
MaximumInputStarvationTimeInFallbackMode :
MaximumInputStarvationTimeInExplicitProcessingExplicitMode;
if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
#if DEBUG
if (AvaloniaLocator.Current.GetService<IDispatcherImpl>() != null
|| AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() != null)
throw new InvalidOperationException(
"Registering IDispatcherImpl or IPlatformThreadingInterface via locator is no longer valid");
#endif
lock (s_globalLock)
{
_thread = Thread.CurrentThread;
if (FromThread(_thread) != null)
throw new InvalidOperationException("The current thread already has a dispatcher");
_unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this);
_exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);
// The first created dispatcher becomes "UI thread one"
s_uiThread ??= this;
s_dispatchers.Remove(Thread.CurrentThread);
s_dispatchers.Add(Thread.CurrentThread,
s_currentThreadDispatcher = new() { Reference = new WeakReference<Dispatcher>(this) });
}
public static Dispatcher UIThread
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
if (impl is null)
{
return s_uiThread ??= CreateUIThreadDispatcher();
}
var st = Stopwatch.StartNew();
_timeProvider = () => st.ElapsedMilliseconds;
}
else
_timeProvider = () => impl.Now;
public bool SupportsRunLoops => _controlledImpl != null;
_impl = null!; // Set by ReplaceImplementation
ReplaceImplementation(impl);
[MethodImpl(MethodImplOptions.NoInlining)]
private static Dispatcher CreateUIThreadDispatcher()
{
var impl = AvaloniaLocator.Current.GetService<IDispatcherImpl>();
if (impl == null)
{
var platformThreading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
if (platformThreading != null)
impl = new LegacyDispatcherImpl(platformThreading);
else
impl = new NullDispatcherImpl();
}
return new Dispatcher(impl);
_unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this);
_exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);
}
public bool SupportsRunLoops => _controlledImpl != null;
/// <summary>
/// Checks that the current thread is the UI thread.
/// </summary>
public bool CheckAccess() => _impl.CurrentThreadIsLoopThread;
public bool CheckAccess() => Thread.CurrentThread == _thread;
/// <summary>
/// Checks that the current thread is the UI thread and throws if not.
@ -89,15 +89,64 @@ public partial class Dispatcher : IDispatcher
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
static void ThrowVerifyAccess()
=> throw new InvalidOperationException("Call from invalid thread");
=> throw new InvalidOperationException("The calling thread cannot access this object because a different thread owns it.");
ThrowVerifyAccess();
}
}
public Thread Thread => _thread;
internal AvaloniaSynchronizationContext GetContextWithPriority(DispatcherPriority priority)
{
DispatcherPriority.Validate(priority, nameof(priority));
var index = priority - DispatcherPriority.MinValue;
return _priorityContexts[index] ??= new(this, priority);
}
[PrivateApi]
public IDispatcherImpl PlatformImpl => _impl;
private void ReplaceImplementation(IDispatcherImpl? impl)
{
// TODO: Consider moving the helper out of Avalonia.Win32 so
// it's usable earlier
using var _ = NonPumpingLockHelper.Use();
if (impl?.CurrentThreadIsLoopThread == false)
throw new InvalidOperationException("IDispatcherImpl belongs to a different thread");
if (_impl != null!) // Null in ctor
{
_impl.Timer -= OnOSTimer;
_impl.Signaled -= Signaled;
if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing -= OnReadyForExplicitBackgroundProcessing;
_impl = null!;
_controlledImpl = null;
_pendingInputImpl = null;
_backgroundProcessingImpl = null;
}
if (impl != null)
_initialized = true;
else
impl = new ManagedDispatcherImpl(null);
_impl = impl;
impl.Timer += OnOSTimer;
impl.Signaled += Signaled;
_controlledImpl = _impl as IControlledDispatcherImpl;
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput;
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing;
_maximumInputStarvationTime = _backgroundProcessingImpl == null ?
MaximumInputStarvationTimeInFallbackMode :
MaximumInputStarvationTimeInExplicitProcessingExplicitMode;
if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
if (_signaled)
_impl.Signal();
_osTimerSetTo = null;
UpdateOSTimer();
}
}

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

@ -81,32 +81,3 @@ internal class LegacyDispatcherImpl : IDispatcherImpl
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<IKeyboardDevice>().ToConstant(Keyboard)
.Bind<IPlatformSettings>().ToSingleton<DefaultPlatformSettings>()
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IRenderTimer>().ToConstant(new UiThreadRenderTimer(60))
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

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

3
src/Avalonia.Native/CallbackBase.cs

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

9
src/Avalonia.X11/X11Platform.cs

@ -42,6 +42,7 @@ namespace Avalonia.X11
public X11Globals Globals { get; private set; } = null!;
public XResources Resources { get; private set; } = null!;
public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new();
public IX11PlatformDispatcher DispatcherImpl { get; private set; } = null!;
public void Initialize(X11PlatformOptions options)
{
@ -79,10 +80,12 @@ namespace Avalonia.X11
var clipboard = new Input.Platform.Clipboard(clipboardImpl);
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IDispatcherImpl>().ToConstant<IDispatcherImpl>(options.UseGLibMainLoop
.Bind<IWindowingPlatform>().ToConstant(this);
DispatcherImpl = options.UseGLibMainLoop
? new GlibDispatcherImpl(this)
: new X11PlatformThreading(this))
: new X11PlatformThreading(this);
Dispatcher.InitializeUIThreadDispatcher(DispatcherImpl);
AvaloniaLocator.CurrentMutable
.Bind<IRenderTimer>().ToConstant(timer)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.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 readonly X11Info _x11;
private readonly X11Window.XEmbedClientWindowMode _mode;
private readonly AvaloniaX11Platform _platform;
private XEmbedPlug(IntPtr? parentXid)
{
var platform = AvaloniaLocator.Current.GetRequiredService<AvaloniaX11Platform>();
_platform = AvaloniaLocator.Current.GetRequiredService<AvaloniaX11Platform>();
_mode = new X11Window.XEmbedClientWindowMode();
_root = new EmbeddableControlRoot(new X11Window(platform, null, _mode));
_root = new EmbeddableControlRoot(new X11Window(_platform, null, _mode));
_root.Prepare();
_x11 = platform.Info;
_x11 = _platform.Info;
if (parentXid.HasValue)
XLib.XReparentWindow(platform.Display, Handle, parentXid.Value, 0, 0);
XLib.XReparentWindow(_platform.Display, Handle, parentXid.Value, 0, 0);
// Make sure that the newly created XID is visible for other clients
XLib.XSync(platform.Display, false);
XLib.XSync(_platform.Display, false);
}
private EmbeddableControlRoot Root
@ -60,8 +61,7 @@ public class XEmbedPlug : IDisposable
public void ProcessInteractiveResize(PixelSize size)
{
var events = (IX11PlatformDispatcher)AvaloniaLocator.Current.GetRequiredService<IDispatcherImpl>();
var events = _platform.DispatcherImpl;
events.EventDispatcher.DispatchX11Events(CancellationToken.None);
_mode.ProcessInteractiveResize(size);
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<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }))
.Bind<IActivatableLifetime>().ToSingleton<BrowserActivatableLifetime>();
if (IsManagedDispatcherEnabled)
{
EventGrouperDispatchQueue = new();
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToConstant(
new ManagedDispatcherImpl(new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
Dispatcher.InitializeUIThreadDispatcher(
new ManagedDispatcherImpl(
new ManualRawEventGrouperDispatchQueueDispatcherInputProvider(EventGrouperDispatchQueue)));
}
else
{
AvaloniaLocator.CurrentMutable.Bind<IDispatcherImpl>().ToSingleton<BrowserDispatcherImpl>();
Dispatcher.InitializeUIThreadDispatcher(new BrowserDispatcherImpl());
}
// 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);
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new ManagedDispatcherImpl(null))
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
.Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
@ -19,7 +20,7 @@ using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Binding
public class AvaloniaObjectTests_Binding : ScopedTestBase
{
[Fact]
public void Bind_Sets_Current_Value()
@ -858,37 +859,28 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)]
public void Typed_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
using (UnitTestApplication.Start())
{
var target = new Class1();
var source = new Subject<string>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
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) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Theory]
@ -896,37 +888,28 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)]
public void Untyped_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
using (UnitTestApplication.Start())
{
var target = new Class1();
var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
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) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Theory]
@ -934,59 +917,41 @@ namespace Avalonia.Base.UnitTests
[InlineData(BindingPriority.Style)]
public void BindingValue_Bind_Executes_On_UIThread(BindingPriority priority)
{
AsyncContext.Run(async () =>
{
using var _ = UnitTestApplication.Start();
var target = new Class1();
var source = new Subject<BindingValue<string>>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
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) =>
{
Assert.Equal(currentThreadId, Thread.CurrentThread.ManagedThreadId);
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source, priority);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.GetValue(Class1.FooProperty));
Assert.Equal(1, raised);
}
});
}
[Fact]
public async Task Bind_With_Scheduler_Executes_On_UI_Thread()
[SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Explicit threading test")]
public void Bind_With_Scheduler_Executes_On_UI_Thread()
{
using var _ = UnitTestApplication.Start();
var target = new Class1();
var source = new Subject<double>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
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]

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

@ -492,19 +492,13 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Bind_Executes_On_UIThread()
{
AsyncContext.Run(async () =>
using(UnitTestApplication.Start())
{
var target = new Class1();
var source = new Subject<object>();
var currentThreadId = Thread.CurrentThread.ManagedThreadId;
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) =>
{
@ -512,17 +506,15 @@ namespace Avalonia.Base.UnitTests
++raised;
};
using (UnitTestApplication.Start(services))
{
target.Bind(Class1.FooProperty, source);
await Task.Run(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs();
ThreadRunHelper.RunOnDedicatedThreadAndWait(() => source.OnNext("foobar"));
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal("foobar", target.Foo);
Assert.Equal(1, raised);
}
});
}
[Fact]

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

@ -1,5 +1,6 @@
using System;
using System.Reactive.Subjects;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform;
@ -9,49 +10,43 @@ using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Threading
public class AvaloniaObjectTests_Threading : ScopedTestBase
{
private TestDipatcherImpl _threading = new(true);
[Fact]
public void AvaloniaObject_Constructor_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new TestDipatcherImpl())))
void AssertThrowsOnDifferentThread(Action cb)
{
Assert.Throws<InvalidOperationException>(() => new Class1());
}
Assert.Throws<InvalidOperationException>(() =>
ThreadRunHelper.RunOnDedicatedThread(cb).GetAwaiter().GetResult());
}
[Fact]
public void StyledProperty_GetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.StyledProperty));
target.GetValue(Class1.StyledProperty);
AssertThrowsOnDifferentThread(() => target.GetValue(Class1.StyledProperty));
}
}
[Fact]
public void StyledProperty_SetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.StyledProperty, "foo"));
AssertThrowsOnDifferentThread(() => target.SetValue(Class1.StyledProperty, "foo"));
}
}
[Fact]
public void Setting_StyledProperty_Binding_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() =>
AssertThrowsOnDifferentThread(() =>
target.Bind(
Class1.StyledProperty,
new BehaviorSubject<string>("foo")));
@ -61,55 +56,50 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void StyledProperty_ClearValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.StyledProperty));
AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.StyledProperty));
}
}
[Fact]
public void StyledProperty_IsSet_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.StyledProperty));
AssertThrowsOnDifferentThread(() => target.IsSet(Class1.StyledProperty));
}
}
[Fact]
public void DirectProperty_GetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.GetValue(Class1.DirectProperty));
AssertThrowsOnDifferentThread(() => target.GetValue(Class1.DirectProperty));
}
}
[Fact]
public void DirectProperty_SetValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.SetValue(Class1.DirectProperty, "foo"));
AssertThrowsOnDifferentThread(() => target.SetValue(Class1.DirectProperty, "foo"));
}
}
[Fact]
public void Setting_DirectProperty_Binding_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() =>
AssertThrowsOnDifferentThread(() =>
target.Bind(
Class1.DirectProperty,
new BehaviorSubject<string>("foo")));
@ -119,22 +109,20 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void DirectProperty_ClearValue_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.ClearValue(Class1.DirectProperty));
AssertThrowsOnDifferentThread(() => target.ClearValue(Class1.DirectProperty));
}
}
[Fact]
public void DirectProperty_IsSet_Should_Throw()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: _threading)))
using (UnitTestApplication.Start())
{
var target = new Class1();
_threading.CurrentThreadIsLoopThread = false;
Assert.Throws<InvalidOperationException>(() => target.IsSet(Class1.DirectProperty));
AssertThrowsOnDifferentThread(() => target.IsSet(Class1.DirectProperty));
}
}
@ -146,27 +134,5 @@ namespace Avalonia.Base.UnitTests
public static readonly DirectProperty<Class1, string?> DirectProperty =
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.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests;
// Some of these exceptions are based from https://github.com/dotnet/wpf-test/blob/05797008bb4975ceeb71be36c47f01688f535d53/src/Test/ElementServices/FeatureTests/Untrusted/Dispatcher/UnhandledExceptionTest.cs#L30
public partial class DispatcherTests
public partial class DispatcherTests : ScopedTestBase
{
private const string ExpectedExceptionText = "Exception thrown inside Dispatcher.Invoke / Dispatcher.BeginInvoke.";
private int _numberOfHandlerOnUnhandledEventInvoked;
private int _numberOfHandlerOnUnhandledEventFilterInvoked;
private Dispatcher _uiThread;
public DispatcherTests()
{
_numberOfHandlerOnUnhandledEventInvoked = 0;
_numberOfHandlerOnUnhandledEventFilterInvoked = 0;
VerifyDispatcherSanity();
_uiThread = Dispatcher.CurrentDispatcher;
}
void VerifyDispatcherSanity()
{
// Verify that we are in a clear-ish state. Do this for every test to ensure that our reset procedure is working
Assert.Null(Dispatcher.FromThread(Thread.CurrentThread));
Assert.Null(Dispatcher.TryGetUIThread());
// The first (this) dispatcher becomes UI thread one
Assert.NotNull(Dispatcher.CurrentDispatcher);
Assert.Equal(Dispatcher.TryGetUIThread(), Dispatcher.CurrentDispatcher);
Assert.Equal(Dispatcher.UIThread, Dispatcher.CurrentDispatcher);
// Dispatcher.FromThread works
Assert.Equal(Dispatcher.CurrentDispatcher, Dispatcher.FromThread(Thread.CurrentThread));
Assert.Equal(Dispatcher.UIThread, Dispatcher.FromThread(Thread.CurrentThread));
}
[Fact]
public void DispatcherHandlesExceptionWithPost()
[SuppressMessage("Usage", "xUnit1031:Do not use blocking task operations in test method", Justification = "Tests the dispatcher itself")]
public void Different_Threads_Auto_Spawn_Dispatchers()
{
var dispatcher = Dispatcher.CurrentDispatcher;
ThreadRunHelper.RunOnDedicatedThread(() =>
{
var impl = new ManagedDispatcherImpl(null);
var disp = new Dispatcher(impl);
Assert.Null(Dispatcher.FromThread(Thread.CurrentThread));
Assert.NotNull(Dispatcher.CurrentDispatcher);
Assert.NotEqual(dispatcher, Dispatcher.CurrentDispatcher);
Assert.Equal(Dispatcher.CurrentDispatcher, Dispatcher.FromThread(Thread.CurrentThread));
}).GetAwaiter().GetResult();
}
[Fact]
public void DispatcherHandlesExceptionWithPost()
{
var handled = false;
var executed = false;
disp.UnhandledException += (sender, args) =>
_uiThread.UnhandledException += (sender, args) =>
{
handled = true;
args.Handled = true;
};
disp.Post(() => ThrowAnException());
disp.Post(() => executed = true);
_uiThread.Post(() => ThrowAnException());
_uiThread.Post(() => executed = true);
disp.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(handled);
Assert.True(executed);
@ -46,14 +79,11 @@ public partial class DispatcherTests
[Fact]
public void SyncContextExceptionCanBeHandledWithPost()
{
var impl = new ManagedDispatcherImpl(null);
var disp = new Dispatcher(impl);
var syncContext = disp.GetContextWithPriority(DispatcherPriority.Background);
var syncContext = _uiThread.GetContextWithPriority(DispatcherPriority.Background);
var handled = false;
var executed = false;
disp.UnhandledException += (sender, args) =>
_uiThread.UnhandledException += (sender, args) =>
{
handled = true;
args.Handled = true;
@ -62,7 +92,7 @@ public partial class DispatcherTests
syncContext.Post(_ => ThrowAnException(), null);
syncContext.Post(_ => executed = true, null);
disp.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(handled);
Assert.True(executed);
@ -71,24 +101,22 @@ public partial class DispatcherTests
[Fact]
public void CanRemoveDispatcherExceptionHandler()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
var caughtCorrectException = false;
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled;
dispatcher.UnhandledExceptionFilter -=
_uiThread.UnhandledExceptionFilter -=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException -=
_uiThread.UnhandledException -=
HandlerOnUnhandledExceptionNotHandled;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{
@ -103,19 +131,16 @@ public partial class DispatcherTests
[Fact]
public void CanHandleExceptionWithUnhandledException()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = true;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception)
{
@ -131,20 +156,17 @@ public partial class DispatcherTests
[Fact]
public void InvokeMethodDoesntTriggerUnhandledException()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false;
try
{
// Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException.
dispatcher.Invoke(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Invoke(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{
@ -160,21 +182,18 @@ public partial class DispatcherTests
[Fact]
public void InvokeAsyncMethodDoesntTriggerUnhandledException()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false;
try
{
// Since both Invoke and InvokeAsync can throw exception, there is no need to pass them to the UnhandledException.
var op = dispatcher.InvokeAsync(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken);
var op = _uiThread.InvokeAsync(ThrowAnException, DispatcherPriority.Normal, TestContext.Current.CancellationToken);
op.Wait();
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{
@ -190,19 +209,16 @@ public partial class DispatcherTests
[Fact]
public void CanRethrowExceptionWithUnhandledException()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled;
var caughtCorrectException = false;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{
@ -217,23 +233,20 @@ public partial class DispatcherTests
[Fact]
public void MultipleUnhandledExceptionFilterCannotResetRequestCatchFlag()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterNotRequestCatch;
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled;
var caughtCorrectException = false;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{
@ -248,22 +261,19 @@ public partial class DispatcherTests
[Fact]
public void MultipleUnhandledExceptionCannotResetHandleFlag()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterRequestCatch;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandled;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionNotHandled;
var caughtCorrectException = true;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception)
{
@ -279,19 +289,16 @@ public partial class DispatcherTests
[Fact]
public void CanPushFrameAndShutdownDispatcherFromUnhandledException()
{
var impl = new ManagedDispatcherImpl(null);
var dispatcher = new Dispatcher(impl);
dispatcher.UnhandledExceptionFilter +=
_uiThread.UnhandledExceptionFilter +=
HandlerOnUnhandledExceptionFilterNotRequestCatchPushFrame;
dispatcher.UnhandledException +=
_uiThread.UnhandledException +=
HandlerOnUnhandledExceptionHandledPushFrame;
var caughtCorrectException = false;
try
{
dispatcher.Post(ThrowAnException, DispatcherPriority.Normal);
dispatcher.RunJobs(null, TestContext.Current.CancellationToken);
_uiThread.Post(ThrowAnException, DispatcherPriority.Normal);
_uiThread.RunJobs(null, TestContext.Current.CancellationToken);
}
catch (Exception e)
{

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

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

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

@ -18,17 +18,13 @@ namespace Avalonia.Base.UnitTests.Input
{
using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock<IPlatformSettings>();
var dispatcherMock = new Mock<IDispatcherImpl>();
dispatcherMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
using var app = UnitTestApplication.Start(
new TestServices(
inputManager: new InputManager(),
dispatcherImpl: dispatcherMock.Object));
inputManager: new InputManager()));
var renderer = new Mock<IHitTester>();
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())
{
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>();
iSettingsMock.Setup(x => x.GetDoubleTapTime(It.IsAny<PointerType>())).Returns(doubleClickTime);
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]
public void Should_Set_ExitCode_After_Shutdown()
{
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new ManagedDispatcherImpl(null))))
using (UnitTestApplication.Start(new TestServices()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
@ -326,7 +326,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event()
{
using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: new ManagedDispatcherImpl(null))))
using (UnitTestApplication.Start(TestServices.StyledWindow.With()))
using (var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var lifetimeEvents = new Mock<IPlatformLifetimeEventsImpl>();
@ -475,7 +475,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Shutdown_NotCancellable_By_Preventing_Window_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop())))
using (UnitTestApplication.Start(TestServices.StyledWindow.With()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());

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

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

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

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

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

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

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

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

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

@ -41,5 +41,15 @@
</Compile>
<Compile Include="../Shared/ScopedSanityCheck.cs"/>
</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" />
</Project>

32
tests/Avalonia.RenderTests/TestRenderHelper.cs

@ -28,16 +28,9 @@ namespace Avalonia.Skia.RenderTests;
static class TestRenderHelper
{
private static readonly TestDispatcherImpl s_dispatcherImpl =
new TestDispatcherImpl();
static TestRenderHelper()
{
SkiaPlatform.Initialize();
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>()
.ToConstant(s_dispatcherImpl);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
AvaloniaLocator.CurrentMutable.Bind<ITextShaperImpl>().ToConstant(new HarfBuzzTextShaper());
}
@ -110,13 +103,14 @@ static class TestRenderHelper
public static void BeginTest()
{
s_dispatcherImpl.MainThread = Thread.CurrentThread;
Dispatcher.ResetBeforeUnitTests();
}
public static void EndTest()
{
if (Dispatcher.UIThread.CheckAccess())
Dispatcher.UIThread.RunJobs();
Dispatcher.ResetForUnitTests();
}
public static string GetTestsDirectory()
@ -132,28 +126,6 @@ static class TestRenderHelper
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)
{
using (var expected = Image.Load<Rgba32>(expectedPath))

24
tests/Avalonia.UnitTests/TestServices.cs

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

2
tests/Avalonia.UnitTests/ThreadRunHelper.cs

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Threading;
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<FontManager>() as IDisposable)?.Dispose();
(AvaloniaLocator.Current.GetService<IInputManager>() as IDisposable)?.Dispose();
Dispatcher.ResetForUnitTests();
scope.Dispose();
@ -84,7 +85,6 @@ namespace Avalonia.UnitTests
.Bind<IPlatformRenderInterface?>().ToConstant(Services.RenderInterface)
.Bind<IFontManagerImpl?>().ToConstant(Services.FontManagerImpl)
.Bind<ITextShaperImpl?>().ToConstant(Services.TextShaperImpl)
.Bind<IDispatcherImpl?>().ToConstant(Services.DispatcherImpl)
.Bind<ICursorFactory?>().ToConstant(Services.StandardCursorFactory)
.Bind<IWindowingPlatform?>().ToConstant(Services.WindowingPlatform)
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

Loading…
Cancel
Save