This is the first part of the Async Wandering series. For your convenience you can find other parts using the links below (or by guessing the address):
Part 1 — Why creating Form from WinForms in unit tests breaks async?
Part 2 — Synchronous call on WinForms control and ObjectDisposedException
Part 3 — Awaiting in synchronous WinForms code
Part 4 — Awaiting for void methods
Part 5 — Catching exceptions from async void
Part 6 — Exceptions logging
Part 7 — Exceptions on unobserved tasks
Part 8 — async and await — the biggest C# mistake?
Part 9 — awaiting with timeout
Part 10 — Project Loom in .NET – awaiting with fibers
Part 11 — Wrapping fibers in context
Part 12 — Fibers with generics
Part 13 — Reentrant recursive async lock
Part 14 — Async with Fibers reimplemented in .NET Core
Part 15 — How async in C# tricks you and how to order async continuations
Recently I was debugging little unit test which was creating Form
(WinForms) and later was executing asynchronous method with async
keyword which was causing a deadlock. Apart from the stupidity o testing UI this way (which I am not going to explain since it is irrelevant to this note), the code was so simple that there was next to nothing to spoil. The code looked similar to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[TestMethod] public async Task DoWork_FormPassed_ExecutedSuccessfully_TrueReturned() { // Arrange var form = new Form(); var sut = CreateSUT(); // Act var result = await sut.DoWork(form); // Assert Assert.That(result, Is.True); } |
We simply create system under test and Form
, next we execute a method, and finally we assert. Nothing fancy here, however, call to DoWork
never returned and the process was stalled.
Observations
The interesting thing here is: if we pass null
to DoWork
, everything works fine. We could try to fake it but FakeItEasy is unable to do that. So it looks like creating Form
causes this whole mess. But why? Let’s dig into the code (decompiled with R#):
First, the Form
constructor:
1 2 3 4 5 6 7 |
public Form() { int num = this.IsRestrictedWindow ? 1 : 0; this.formStateEx[Form.FormStateExShowIcon] = 1; this.SetState(2, false); this.SetState(524288, true); } |
Well, it looks decent. Some flags, some state, nothing else. First line is integersing — why does it create an unused variable? Maybe this.IsRestrictedWindow
does something in getter? Let’s see:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/// <summary>Gets a value indicating whether the form can use all windows and user input events without restriction.</summary> /// <returns>true if the form has restrictions; otherwise, false. The default is true.</returns> /// <filterpriority>1</filterpriority> [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Advanced)] public bool IsRestrictedWindow { get { if (this.formState[Form.FormStateIsRestrictedWindowChecked] == 0) { this.formState[Form.FormStateIsRestrictedWindow] = 0; try { IntSecurity.WindowAdornmentModification.Demand(); } catch (SecurityException ex) { this.formState[Form.FormStateIsRestrictedWindow] = 1; } catch { this.formState[Form.FormStateIsRestrictedWindow] = 1; this.formState[Form.FormStateIsRestrictedWindowChecked] = 1; throw; } this.formState[Form.FormStateIsRestrictedWindowChecked] = 1; } return (uint) this.formState[Form.FormStateIsRestrictedWindow] > 0U; } } |
Some magic but nothing here should break async
so this is a dead end. Well, let’s check class hierarchy:
1 |
public class Form : ContainerControl |
Hmm, it looks like this is a derived class (which should not be a surprise). Let’s go to the base’s constructor:
1 2 3 4 5 6 |
/// <summary>Initializes a new instance of the <see cref="T:System.Windows.Forms.ContainerControl" /> class.</summary> public ContainerControl() { this.SetStyle(ControlStyles.AllPaintingInWmPaint, false); this.SetState2(2048, true); } |
Still nothing. Let’s go up:
1 2 3 4 5 6 7 |
/// <summary>Initializes a new instance of the <see cref="T:System.Windows.Forms.ScrollableControl" /> class.</summary> public ScrollableControl() { this.SetStyle(ControlStyles.ContainerControl, true); this.SetStyle(ControlStyles.AllPaintingInWmPaint, false); this.SetScrollState(1, false); } |
And up:
1 2 3 4 5 |
/// <summary>Initializes a new instance of the <see cref="T:System.Windows.Forms.Control" /> class with default settings.</summary> public Control() : this(true) { } |
Oh, here is something interesting. Let’s see the constructor with parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
internal Control(bool autoInstallSyncContext) { this.propertyStore = new PropertyStore(); this.window = new Control.ControlNativeWindow(this); this.RequiredScalingEnabled = true; this.RequiredScaling = BoundsSpecified.All; this.tabIndex = -1; this.state = 131086; this.state2 = 8; this.SetStyle(ControlStyles.UserPaint | ControlStyles.StandardClick | ControlStyles.Selectable | ControlStyles.StandardDoubleClick | ControlStyles.AllPaintingInWmPaint | ControlStyles.UseTextForAccessibility, true); this.InitMouseWheelSupport(); if (this.DefaultMargin != CommonProperties.DefaultMargin) this.Margin = this.DefaultMargin; if (this.DefaultMinimumSize != CommonProperties.DefaultMinimumSize) this.MinimumSize = this.DefaultMinimumSize; if (this.DefaultMaximumSize != CommonProperties.DefaultMaximumSize) this.MaximumSize = this.DefaultMaximumSize; Size defaultSize = this.DefaultSize; this.width = defaultSize.Width; this.height = defaultSize.Height; CommonProperties.xClearPreferredSizeCache((IArrangedElement) this); if (this.width != 0 && this.height != 0) { NativeMethods.RECT lpRect = new NativeMethods.RECT(); // ISSUE: explicit reference operation // ISSUE: variable of a reference type NativeMethods.RECT& local = @lpRect; int num1; int num2 = num1 = 0; // ISSUE: explicit reference operation (^local).bottom = num1; int num3; int num4 = num3 = num2; // ISSUE: explicit reference operation (^local).top = num3; int num5; int num6 = num5 = num4; // ISSUE: explicit reference operation (^local).right = num5; int num7 = num6; // ISSUE: explicit reference operation (^local).left = num7; CreateParams createParams = this.CreateParams; SafeNativeMethods.AdjustWindowRectEx(ref lpRect, createParams.Style, false, createParams.ExStyle); this.clientWidth = this.width - (lpRect.right - lpRect.left); this.clientHeight = this.height - (lpRect.bottom - lpRect.top); } if (!autoInstallSyncContext) return; WindowsFormsSynchronizationContext.InstallIfNeeded(); } |
Most of this is not interesting, however, parameter’s name is very suspicious: autoInstallSyncContext
. Last thing of this function is:
1 2 3 |
if (!autoInstallSyncContext) return; WindowsFormsSynchronizationContext.InstallIfNeeded(); |
Yes, got it. This class installs synchronization context. Let’s see it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
internal static void InstallIfNeeded() { if (!WindowsFormsSynchronizationContext.AutoInstall || WindowsFormsSynchronizationContext.inSyncContextInstallation) return; if (SynchronizationContext.Current == null) WindowsFormsSynchronizationContext.previousSyncContext = (SynchronizationContext) null; if (WindowsFormsSynchronizationContext.previousSyncContext != null) return; WindowsFormsSynchronizationContext.inSyncContextInstallation = true; try { SynchronizationContext synchronizationContext = AsyncOperationManager.SynchronizationContext; if (synchronizationContext != null && !(synchronizationContext.GetType() == typeof (SynchronizationContext))) return; WindowsFormsSynchronizationContext.previousSyncContext = synchronizationContext; new PermissionSet(PermissionState.Unrestricted).Assert(); try { AsyncOperationManager.SynchronizationContext = (SynchronizationContext) new WindowsFormsSynchronizationContext(); } finally { CodeAccessPermission.RevertAssert(); } } finally { WindowsFormsSynchronizationContext.inSyncContextInstallation = false; } } |
And we can see that it indeed replaces synchronization context with different one. We have our culprit!
Solution
What can we do? Well, we can hack it in the following way:
1 2 3 |
var context = SynchronizationContext.Current; new Form(); SynchronizationContext.SetSynchronizationContext(context); |
So we store existing context, create problematic instance, and restore the context. After this change in test everything works fine.
However… It is better to simply not test UI (or at least use interfaces or form provider).