diff --git a/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj b/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj index ab0ab1e310c..6cc21e4eea6 100644 --- a/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj +++ b/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj @@ -8,6 +8,7 @@ Windows Community Toolkit - Common (UWP) This package includes code only helpers such as Color conversion tool, Storage file handling, a Stream helper class, SystemInformation helpers, etc. Storage;File;Folder;Color;Conversion;Stream;Helpers;Extensions;System;Information + true diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs index a2f172a0024..a5e88985ece 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.WinUI /// /// Helpers for executing code in a . /// - public static class DispatcherQueueExtensions + public static partial class DispatcherQueueExtensions { /// /// Invokes a given function on the target and returns a diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs new file mode 100644 index 00000000000..080f1f15c29 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using CommunityToolkit.WinUI.Interop; +using Microsoft.UI.Dispatching; +using WinRT; + +#nullable enable + +namespace CommunityToolkit.WinUI +{ + /// + /// A callback that will be executed on the thread. + /// + /// The type of state to receive as input. + /// The input state for the callback. + public delegate void DispatcherQueueHandler(T state) + where T : class; + + /// + /// A callback that will be executed on the thread. + /// + /// The type of the first state to receive as input. + /// The type of the second state to receive as input. + /// The first input state for the callback. + /// The second input state for the callback. + public delegate void DispatcherQueueHandler(T1 state1, T2 state2) + where T1 : class + where T2 : class; + + /// + /// Helpers for executing code in a . + /// + public static partial class DispatcherQueueExtensions + { + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of state to capture. + /// The target to invoke the code on. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T state) + where T : class + { + return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler1.Create(callback, state)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of state to capture. + /// The target to invoke the code on. + /// The desired priority for the callback to schedule. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T state) + where T : class + { + return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler1.Create(callback, state)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The target to invoke the code on. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T1 state1, T2 state2) + where T1 : class + where T2 : class + { + return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The target to invoke the code on. + /// The desired priority for the callback to schedule. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T1 state1, T2 state2) + where T1 : class + where T2 : class + { + return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The target to invoke the code on. + /// The desired priority for the callback to schedule (if available). + /// The input callback to enqueue. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority? priority, THandler* dispatcherQueueHandler) + where THandler : unmanaged, IDispatcherQueueHandler + { + bool success; + int hResult; + + try + { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + + if (priority.HasValue) + { + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority.GetValueOrDefault(), dispatcherQueueHandler, (byte*)&success); + } + else + { + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); + } + + GC.KeepAlive(dispatcherQueue); + } + finally + { + // This call doesn not have a corresponding AddRef() invocation that is visible, and + // that is because the static constructors for all existing custom handlers already + // set the internal reference count to 1. Structuring the code like this makes it + // possible to centralize all logic in this method without each caller needing to + // explicitly have a try/finally block for the allocated handler that is passed here. + dispatcherQueueHandler->Release(); + } + + if (hResult != 0) + { + ExceptionHelpers.ThrowExceptionForHR(hResult); + } + + return success; + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs new file mode 100644 index 00000000000..f7dc8efd514 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using WinRT; +using static CommunityToolkit.WinUI.Interop.Windows; + +#nullable enable + +#pragma warning disable SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A custom IDispatcherQueueHandler object, that internally stores a captured instance + /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. + /// + internal unsafe struct DispatcherQueueProxyHandler1 : IDispatcherQueueHandler + { + /// + /// The shared vtable pointer for instances. + /// + private static readonly void** Vtbl = InitVtbl(); + + /// + /// Setups the vtable pointer for . + /// + /// The initialized vtable pointer for . + /// + /// The vtable itself is allocated with , + /// which allocates memory in the high frequency heap associated with the input runtime type. This will be + /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. + /// + private static void** InitVtbl() + { + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler1), sizeof(void*) * 4); + + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + + return lpVtbl; + } + + /// + /// The vtable pointer for the current instance. + /// + private void** lpVtbl; + + /// + /// The to the captured (for some unknown TState type). + /// + private GCHandle callbackHandle; + + /// + /// The to the captured state (with an unknown TState type). + /// + private GCHandle stateHandle; + + /// + /// The generic stub to invoke the current callback with the right generic context. + /// + private delegate* stub; + + /// + /// The current reference count for the object (from IUnknown). + /// + private volatile uint referenceCount; + + /// + /// Creates a new instance for the input callback and state. + /// + /// The type of state currently being used. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler1* Create(DispatcherQueueHandler handler, T state) + where T : class + { + DispatcherQueueProxyHandler1* @this = (DispatcherQueueProxyHandler1*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler1)); + + @this->lpVtbl = Vtbl; + @this->callbackHandle = GCHandle.Alloc(handler); + @this->stateHandle = GCHandle.Alloc(state); + @this->stub = &Impl.Invoke; + @this->referenceCount = 1; + + return @this; + } + + /// + /// Devirtualized API for IUnknown.AddRef(). + /// + /// The updated reference count for the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AddRef() + { + return Interlocked.Increment(ref referenceCount); + } + + /// + /// Devirtualized API for IUnknown.Release(). + /// + /// The updated reference count for the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + uint referenceCount = Interlocked.Decrement(ref this.referenceCount); + + if (referenceCount == 0) + { + callbackHandle.Free(); + stateHandle.Free(); + + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); + } + + return referenceCount; + } + + /// + /// A private type with the implementation of the unmanaged methods for . + /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. + /// + private static class Impl + { + /// + /// Implements IUnknown.QueryInterface(REFIID, void**). + /// + [UnmanagedCallersOnly] + public static int QueryInterface(DispatcherQueueProxyHandler1* @this, Guid* riid, void** ppvObject) + { + if (riid->Equals(GuidOfIUnknown) || + riid->Equals(GuidOfIAgileObject) || + riid->Equals(GuidOfIDispatcherQueueHandler)) + { + @this->AddRef(); + + *ppvObject = @this; + + return S_OK; + } + + return E_NOINTERFACE; + } + + /// + /// Implements IUnknown.AddRef(). + /// + [UnmanagedCallersOnly] + public static uint AddRef(DispatcherQueueProxyHandler1* @this) + { + return Interlocked.Increment(ref @this->referenceCount); + } + + /// + /// Implements IUnknown.Release(). + /// + [UnmanagedCallersOnly] + public static uint Release(DispatcherQueueProxyHandler1* @this) + { + uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); + + if (referenceCount == 0) + { + @this->callbackHandle.Free(); + @this->stateHandle.Free(); + + Marshal.FreeHGlobal((IntPtr)@this); + } + + return referenceCount; + } + + /// + /// Implements IDispatcherQueueHandler.Invoke(). + /// + [UnmanagedCallersOnly] + public static int Invoke(DispatcherQueueProxyHandler1* @this) + { + return @this->stub(@this); + } + + /// + /// Implements IDispatcherQueueHandler.Invoke() from within a generic context. + /// + public static int Invoke(DispatcherQueueProxyHandler1* @this) + where T : class + { + object callback = @this->callbackHandle.Target!; + object state = @this->stateHandle.Target!; + + try + { + Unsafe.As>(callback)(Unsafe.As(state)); + } + catch (Exception e) + { + ExceptionHelpers.SetErrorInfo(e); + + return ExceptionHelpers.GetHRForException(e); + } + + return S_OK; + } + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs new file mode 100644 index 00000000000..cfb4161313d --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using WinRT; +using static CommunityToolkit.WinUI.Interop.Windows; + +#nullable enable + +#pragma warning disable SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A custom IDispatcherQueueHandler object, that internally stores a captured instance + /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. + /// + internal unsafe struct DispatcherQueueProxyHandler2 : IDispatcherQueueHandler + { + /// + /// The shared vtable pointer for instances. + /// + private static readonly void** Vtbl = InitVtbl(); + + /// + /// Setups the vtable pointer for . + /// + /// The initialized vtable pointer for . + /// + /// The vtable itself is allocated with , + /// which allocates memory in the high frequency heap associated with the input runtime type. This will be + /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. + /// + private static void** InitVtbl() + { + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler2), sizeof(void*) * 4); + + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + + return lpVtbl; + } + + /// + /// The vtable pointer for the current instance. + /// + private void** lpVtbl; + + /// + /// The to the captured (for some unknown TState type). + /// + private GCHandle callbackHandle; + + /// + /// The to the first captured state (with an unknown T1 type). + /// + private GCHandle state1Handle; + + /// + /// The to the second captured state (with an unknown T2 type). + /// + private GCHandle state2Handle; + + /// + /// The generic stub to invoke the current callback with the right generic context. + /// + private delegate* stub; + + /// + /// The current reference count for the object (from IUnknown). + /// + private volatile uint referenceCount; + + /// + /// Creates a new instance for the input callback and state. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler2* Create(DispatcherQueueHandler handler, T1 state1, T2 state2) + where T1 : class + where T2 : class + { + DispatcherQueueProxyHandler2* @this = (DispatcherQueueProxyHandler2*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler2)); + + @this->lpVtbl = Vtbl; + @this->callbackHandle = GCHandle.Alloc(handler); + @this->state1Handle = GCHandle.Alloc(state1); + @this->state2Handle = GCHandle.Alloc(state2); + @this->stub = &Impl.Invoke; + @this->referenceCount = 1; + + return @this; + } + + /// + /// Devirtualized API for IUnknown.AddRef(). + /// + /// The updated reference count for the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AddRef() + { + return Interlocked.Increment(ref referenceCount); + } + + /// + /// Devirtualized API for IUnknown.Release(). + /// + /// The updated reference count for the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + uint referenceCount = Interlocked.Decrement(ref this.referenceCount); + + if (referenceCount == 0) + { + callbackHandle.Free(); + state1Handle.Free(); + state2Handle.Free(); + + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); + } + + return referenceCount; + } + + /// + /// A private type with the implementation of the unmanaged methods for . + /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. + /// + private static class Impl + { + /// + /// Implements IUnknown.QueryInterface(REFIID, void**). + /// + [UnmanagedCallersOnly] + public static int QueryInterface(DispatcherQueueProxyHandler2* @this, Guid* riid, void** ppvObject) + { + if (riid->Equals(GuidOfIUnknown) || + riid->Equals(GuidOfIAgileObject) || + riid->Equals(GuidOfIDispatcherQueueHandler)) + { + @this->AddRef(); + + *ppvObject = @this; + + return S_OK; + } + + return E_NOINTERFACE; + } + + /// + /// Implements IUnknown.AddRef(). + /// + [UnmanagedCallersOnly] + public static uint AddRef(DispatcherQueueProxyHandler2* @this) + { + return Interlocked.Increment(ref @this->referenceCount); + } + + /// + /// Implements IUnknown.Release(). + /// + [UnmanagedCallersOnly] + public static uint Release(DispatcherQueueProxyHandler2* @this) + { + uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); + + if (referenceCount == 0) + { + @this->callbackHandle.Free(); + @this->state1Handle.Free(); + @this->state2Handle.Free(); + + Marshal.FreeHGlobal((IntPtr)@this); + } + + return referenceCount; + } + + /// + /// Implements IDispatcherQueueHandler.Invoke(). + /// + [UnmanagedCallersOnly] + public static int Invoke(DispatcherQueueProxyHandler2* @this) + { + return @this->stub(@this); + } + + /// + /// Implements IDispatcherQueueHandler.Invoke() from within a generic context. + /// + public static int Invoke(DispatcherQueueProxyHandler2* @this) + where T1 : class + where T2 : class + { + object callback = @this->callbackHandle.Target!; + object state1 = @this->state1Handle.Target!; + object state2 = @this->state2Handle.Target!; + + try + { + Unsafe.As>(callback)( + Unsafe.As(state1), + Unsafe.As(state2)); + } + catch (Exception e) + { + ExceptionHelpers.SetErrorInfo(e); + + return ExceptionHelpers.GetHRForException(e); + } + + return S_OK; + } + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs new file mode 100644 index 00000000000..c9950307764 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.UI.Dispatching; + +#pragma warning disable CS0649, SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A struct mapping the native WinRT IDispatcherQueue interface. + /// + internal unsafe struct IDispatcherQueue + { + /// + /// The vtable pointer for the current instance. + /// + private readonly void** lpVtbl; + + /// + /// Native API for . + /// + /// A pointer to an object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + /// + /// The parameter is assumed to be a pointer to an object, but it + /// is just typed as a to avoid unnecessary generic instantiations for this method (as it just needs to pass a + /// pointer to the native side anyway). The is an actual C# interface and not a C++ one as it + /// is only used internally to constrain type parameters and allow calls to to be inlined. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueue(void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); + } + + /// + /// Native API for . + /// + /// The priority for the input callback. + /// A pointer to an object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs new file mode 100644 index 00000000000..af01bb29c63 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// An interface mapping the native WinRT IDispatcherQueueHandler interface. + /// + internal interface IDispatcherQueueHandler + { + /// + /// Implements IUnknown.Release(). + /// + /// The updated reference count for the current instance. + uint Release(); + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs new file mode 100644 index 00000000000..95df0bf79e9 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A helper class with some shared constants from the Windows headers. + /// + internal static class Windows + { + /// + /// The HRESULT for a successful operation. + /// + public const int S_OK = 0; + + /// + /// The HRESULT for an invalid cast from IUnknown.QueryInterface. + /// + public const int E_NOINTERFACE = unchecked((int)0x80004002); + + /// + /// The GUID for the IUnknown COM interface. + /// + public static readonly Guid GuidOfIUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + /// + /// The GUID for the IAgileObject WinRT interface. + /// + public static readonly Guid GuidOfIAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); + + /// + /// The GUID for the IDispatcherQueueHandler WinRT interface. + /// + public static readonly Guid GuidOfIDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs b/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs index 5313f5bdcd8..6d1663cf68e 100644 --- a/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs +++ b/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs @@ -287,5 +287,167 @@ public void Test_DispatcherQueueHelper_FuncOfTaskOfT_Exception() Assert.IsNotNull(task.Exception); Assert.IsInstanceOfType(task.Exception.InnerExceptions.FirstOrDefault(), typeof(ArgumentException)); } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue(x => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread)); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + taskSource.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread)); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue((x, tcs) => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread)); + Assert.AreSame(tcs, taskSource); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + tcs.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread), taskSource); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue( + DispatcherQueuePriority.Normal, + x => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread)); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + taskSource.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread)); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue( + DispatcherQueuePriority.Normal, + (x, tcs) => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread)); + Assert.AreSame(tcs, taskSource); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + tcs.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread), taskSource); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } } } \ No newline at end of file