File: System\Windows\Forms\ControlTests_InvokeAsync.cs
Web Access
Project: src\src\test\unit\System.Windows.Forms\System.Windows.Forms.Tests.csproj (System.Windows.Forms.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
namespace System.Windows.Forms.Tests;
 
// We need to do tests with this, so need to disable this rule.
#pragma warning disable xUnit1030 // Do not call ConfigureAwait(false) in test method
#pragma warning disable xUnit1051 // Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken
public partial class ControlTests
{
    private sealed class TestControl : Control
    {
        public void EnsureHandle() => _ = Handle;
        public void DestroyTestHandle() => DestroyHandle();
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Action_ExecutesOnUIThread()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int? originalThread = Environment.CurrentManagedThreadId;
        Assert.NotNull(originalThread);
 
        int? newTaskThread = null;
        int? invokeThread = null;
 
        await Task.Run(async () =>
        {
            newTaskThread = Environment.CurrentManagedThreadId;
            Assert.True(control.InvokeRequired);
 
            // Invoke the Action on the UI thread.
            await control.InvokeAsync(UiAccessAction)
                .ConfigureAwait(false);
        }).ConfigureAwait(true);
 
        Assert.NotNull(newTaskThread);
        Assert.NotEqual(originalThread.Value, newTaskThread.Value);
        Assert.Equal(originalThread, invokeThread);
 
        // Add verification that we're back on the original thread
        Assert.Equal(originalThread.Value, Environment.CurrentManagedThreadId);
 
        // Local function, which becomes the Action to be invoked.
        void UiAccessAction()
        {
            invokeThread = Environment.CurrentManagedThreadId;
 
            for (int i = 0; i < 10; i++)
            {
                control.Text += i.ToString();
            }
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_FuncT_ExecutesOnUIThread_AndReturnsValue()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int? originalThread = Environment.CurrentManagedThreadId;
        Assert.NotNull(originalThread);
 
        int? newTaskThread = null;
        int? invokeThread = null;
        int result = 0;
 
        await Task.Run(async () =>
        {
            newTaskThread = Environment.CurrentManagedThreadId;
            Assert.True(control.InvokeRequired);
 
            // Invoke the Func<T> on the UI thread.
            result = await control.InvokeAsync(UiAccessFunc)
                .ConfigureAwait(false);
        }).ConfigureAwait(false);
 
        Assert.NotNull(newTaskThread);
        Assert.NotEqual(originalThread.Value, newTaskThread.Value);
        Assert.Equal(originalThread, invokeThread);
        Assert.Equal(42, result);
 
        // Local function, which becomes the Func<T> to be invoked.
        int UiAccessFunc()
        {
            invokeThread = Environment.CurrentManagedThreadId;
 
            return 42;
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallback_ExecutesOnUIThread()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int? originalThread = Environment.CurrentManagedThreadId;
        Assert.NotNull(originalThread);
 
        int? newTaskThread = null;
        int? invokeThreadBeforeAwaitInInvokeDelegate = null;
        int? invokeThreadAfterAwaitInInvokeDelegate = null;
 
        await Task.Run(async () =>
        {
            newTaskThread = Environment.CurrentManagedThreadId;
            Assert.True(control.InvokeRequired);
 
            // Invoke the async callback on the UI thread.
            await control.InvokeAsync(UiAccessAsyncCallback)
                .ConfigureAwait(false);
        }).ConfigureAwait(false);
 
        Assert.NotNull(newTaskThread);
        Assert.NotEqual(originalThread.Value, newTaskThread.Value);
        Assert.Equal(originalThread, invokeThreadBeforeAwaitInInvokeDelegate);
 
        // Local function, which becomes the async callback to be invoked.
        async ValueTask UiAccessAsyncCallback(CancellationToken ct)
        {
            invokeThreadBeforeAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
 
            await Task.Delay(10, ct).ConfigureAwait(true);
 
            invokeThreadAfterAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
            Assert.Equal(invokeThreadBeforeAwaitInInvokeDelegate, invokeThreadAfterAwaitInInvokeDelegate);
 
            await Task.Delay(10, ct).ConfigureAwait(false);
 
            invokeThreadAfterAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
            Assert.NotEqual(invokeThreadBeforeAwaitInInvokeDelegate, invokeThreadAfterAwaitInInvokeDelegate);
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallback_ExecutesOnUIThread_ControlInvocation()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int? originalThread = Environment.CurrentManagedThreadId;
        Assert.NotNull(originalThread);
 
        int? newTaskThread = null;
        int? invokeThreadInInvokeDelegate = null;
 
        await Task.Run(async () =>
        {
            newTaskThread = Environment.CurrentManagedThreadId;
            Assert.True(control.InvokeRequired);
 
            // Invoke the async callback on the UI thread.
            await control.InvokeAsync(UiAccessAsyncCallback)
                .ConfigureAwait(false);
        }).ConfigureAwait(false);
 
        Assert.NotNull(newTaskThread);
        Assert.NotEqual(originalThread.Value, newTaskThread.Value);
        Assert.Equal(originalThread, invokeThreadInInvokeDelegate);
 
        // Local function, which becomes the async callback to be invoked.
        async ValueTask UiAccessAsyncCallback(CancellationToken ct)
        {
            invokeThreadInInvokeDelegate = Environment.CurrentManagedThreadId;
 
            await Task.Delay(10, ct).ConfigureAwait(true);
            Assert.False(control.InvokeRequired, "InvokeAsync should not be required after the first await with ConfigureAwait(true).");
 
            await Task.Delay(10, ct).ConfigureAwait(true);
            Assert.False(control.InvokeRequired, "InvokeAsync should not be required after a subsequent await with ConfigureAwait(true).");
 
            await Task.Delay(10, ct).ConfigureAwait(false);
            Assert.True(control.InvokeRequired, "InvokeAsync should always be required after an await with ConfigureAwait(false).");
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallbackT_ExecutesOnUIThread_AndReturnsValue()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int? originalThread = Environment.CurrentManagedThreadId;
        Assert.NotNull(originalThread);
 
        int? newTaskThread = null;
        int? invokeThreadBeforeAwaitInInvokeDelegate = null;
        int? invokeThreadAfterAwaitInInvokeDelegate = null;
        int result = 0;
 
        await Task.Run(async () =>
        {
            newTaskThread = Environment.CurrentManagedThreadId;
            Assert.True(control.InvokeRequired);
 
            // Invoke the async callback on the UI thread.
            result = await control.InvokeAsync(UiAccessAsyncCallback)
                .ConfigureAwait(false);
        }).ConfigureAwait(false);
 
        Assert.NotNull(newTaskThread);
        Assert.NotEqual(originalThread.Value, newTaskThread.Value);
        Assert.Equal(originalThread, invokeThreadBeforeAwaitInInvokeDelegate);
        Assert.Equal(99, result);
 
        // Local function, which becomes the async callback to be invoked.
        async ValueTask<int> UiAccessAsyncCallback(CancellationToken ct)
        {
            invokeThreadBeforeAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
 
            await Task.Delay(10, ct).ConfigureAwait(true);
 
            invokeThreadAfterAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
            Assert.Equal(invokeThreadBeforeAwaitInInvokeDelegate, invokeThreadAfterAwaitInInvokeDelegate);
 
            await Task.Delay(10, ct).ConfigureAwait(false);
 
            invokeThreadAfterAwaitInInvokeDelegate = Environment.CurrentManagedThreadId;
            Assert.NotEqual(invokeThreadBeforeAwaitInInvokeDelegate, invokeThreadAfterAwaitInInvokeDelegate);
 
            return 99;
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Action_Cancellation_PreCancelledToken_ReturnsEarly()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync().ConfigureAwait(false);
 
        await control.InvokeAsync(
            () => throw new ArgumentOutOfRangeException("Should not run"), cts.Token)
            .ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_FuncT_Cancellation_PreCancelledToken_ReturnsDefault()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync().ConfigureAwait(false);
 
        int result = await control.InvokeAsync(
            CallBack,
            cts.Token).ConfigureAwait(false);
 
        Assert.Equal(0, result);
 
        // Not using the CancellationToken in the callback, but we need to meet the signature.
        static ValueTask<int> CallBack(CancellationToken ct)
        {
            throw new ArgumentOutOfRangeException("Should not run");
        }
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallback_Cancellation_PreCancelledToken_ReturnsEarly()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync().ConfigureAwait(true);
 
        await control.InvokeAsync(
            ct => throw new ArgumentOutOfRangeException("Should not run"),
            cts.Token).ConfigureAwait(true);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallbackT_Cancellation_PreCancelledToken_ReturnsDefault()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
        await cts.CancelAsync().ConfigureAwait(false);
 
        int result = await control.InvokeAsync<int>(
            ct => throw new ArgumentOutOfRangeException("Should not run"),
            cts.Token).ConfigureAwait(false);
 
        Assert.Equal(0, result);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Action_Cancellation_BeforeExecution()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
 
        // Queue multiple operations to ensure our cancelled one is truly queued
        var blockingTask = control.InvokeAsync(() => Thread.Sleep(50));
 
        // Immediately queue another task with pre-cancelled token
        cts.Cancel();
 
        var cancelledTask = control.InvokeAsync(
            () => throw new InvalidOperationException("Should not execute"),
            cts.Token);
 
        // The task should complete successfully without executing the callback
        // This is the documented behavior for pre-cancelled tokens
        await cancelledTask.ConfigureAwait(false);
 
        // The blocking task should complete normally
        await blockingTask.ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Action_Cancellation_DuringExecution()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
        var started = new TaskCompletionSource<bool>();
 
        Task task = control.InvokeAsync(async ct =>
        {
            started.SetResult(true);
 
            // Simulate work that checks cancellation
            for (int i = 0; i < 10; i++)
            {
                ct.ThrowIfCancellationRequested();
                await Task.Delay(10, ct).ConfigureAwait(false);
            }
        }, cts.Token);
 
        // Wait for it to start
        await started.Task.ConfigureAwait(false);
 
        // Cancel while running
        await cts.CancelAsync().ConfigureAwait(false);
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await task.ConfigureAwait(false))
            .ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_AsyncCallback_Cancellation_WhileRunning()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        using var cts = new CancellationTokenSource();
 
        Task task = control.InvokeAsync(async ct =>
        {
            await cts.CancelAsync().ConfigureAwait(false);
            ct.ThrowIfCancellationRequested();
 
            await Task.Delay(10, ct).ConfigureAwait(false);
        }, cts.Token);
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            async () => await task.ConfigureAwait(false)).ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Throws_InvalidOperationException_IfHandleNotCreated()
    {
        using var control = new TestControl();
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync(ct => default, CancellationToken.None)).ConfigureAwait(true);
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync(
                ct => new ValueTask<int>(1),
                CancellationToken.None)).ConfigureAwait(false);
 
        // Test for destroyed handle
        control.EnsureHandle();
        control.DestroyTestHandle();
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync(() => { })).ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Propagates_Exception_FromCallback()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync(() => throw new InvalidOperationException()))
            .ConfigureAwait(false);
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync<int>(() => throw new InvalidOperationException()))
            .ConfigureAwait(false);
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync(ct => throw new InvalidOperationException()))
            .ConfigureAwait(false);
 
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            control.InvokeAsync<int>(ct => throw new InvalidOperationException()))
            .ConfigureAwait(false);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_Reentry_Supported()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        bool innerCalled = false;
 
        await control.InvokeAsync(async ct =>
        {
            await control.InvokeAsync(() => innerCalled = true, ct)
            .ConfigureAwait(false);
        }).ConfigureAwait(false);
 
        Assert.True(innerCalled);
    }
 
    [WinFormsFact]
    public async Task InvokeAsync_MultipleConcurrentCalls_AreThreadSafe()
    {
        using var control = new TestControl();
        control.EnsureHandle();
 
        int counter = 0;
        Task[] tasks = new Task[10];
 
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = control.InvokeAsync(() => Interlocked.Increment(ref counter));
        }
 
        await Task.WhenAll(tasks).ConfigureAwait(false);
        Assert.Equal(10, counter);
    }
}
#pragma warning restore xUnit1030 // Do not call ConfigureAwait(false) in test method
#pragma warning restore xUnit1051 // Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken