Skip to content

Commit 5b22c35

Browse files
Add review suggestions and improve docs accordingly.
1 parent 4266df2 commit 5b22c35

File tree

1 file changed

+118
-4
lines changed

1 file changed

+118
-4
lines changed

src/System.Windows.Forms/System/Windows/Forms/Control_InvokeAsync.cs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,34 @@ public partial class Control
2929
/// <see cref="InvokeAsync(Func{CancellationToken, ValueTask}, CancellationToken)"/> or
3030
/// <see cref="InvokeAsync{T}(Func{CancellationToken, ValueTask{T}}, CancellationToken)"/>.
3131
/// </para>
32+
/// <para>
33+
/// <b>Note:</b> If the control is disposed (or its handle is destroyed) before the
34+
/// marshaled callback runs, the returned task may never complete. This is the same
35+
/// behavior as <see cref="BeginInvoke(Delegate)"/>.
36+
/// To avoid this, either:
37+
/// </para>
38+
/// <list type="bullet">
39+
/// <item>
40+
/// <description>Ensure the control outlives the awaited operation, or</description>
41+
/// </item>
42+
/// <item>
43+
/// <description>
44+
/// Always pass a <see cref="CancellationToken"/> that you cancel when the
45+
/// control is disposing/its handle is destroyed (recommended). A common pattern is to
46+
/// use a token linked to the control's lifetime.
47+
/// </description>
48+
/// </item>
49+
/// </list>
3250
/// </remarks>
3351
public async Task InvokeAsync(Action callback, CancellationToken cancellationToken = default)
3452
{
3553
ArgumentNullException.ThrowIfNull(callback);
3654

55+
if (!IsHandleCreated)
56+
{
57+
throw new InvalidOperationException(SR.ErrorNoMarshalingThread);
58+
}
59+
3760
if (cancellationToken.IsCancellationRequested)
3861
{
3962
return;
@@ -64,7 +87,15 @@ void WrappedAction()
6487
}
6588
catch (Exception ex)
6689
{
67-
completion.TrySetException(ex);
90+
if (ex is OperationCanceledException oce
91+
&& oce.CancellationToken == cancellationToken)
92+
{
93+
completion.TrySetCanceled(cancellationToken);
94+
}
95+
else
96+
{
97+
completion.TrySetException(ex);
98+
}
6899
}
69100
}
70101
}
@@ -100,11 +131,34 @@ void WrappedAction()
100131
/// in a "fire-and-forget" manner. To properly await asynchronous operations, use the overloads that accept
101132
/// <see cref="Func{CancellationToken, ValueTask}"/> (or <see cref="ValueTask{T}"/>).
102133
/// </para>
134+
/// <para>
135+
/// <b>Note:</b> If the control is disposed (or its handle is destroyed) before the
136+
/// marshaled callback runs, the returned task may never complete. This is the same
137+
/// behavior as <see cref="BeginInvoke(Delegate)"/>.
138+
/// To avoid this, either:
139+
/// </para>
140+
/// <list type="bullet">
141+
/// <item>
142+
/// <description>Ensure the control outlives the awaited operation, or</description>
143+
/// </item>
144+
/// <item>
145+
/// <description>
146+
/// Always pass a <see cref="CancellationToken"/> that you cancel when the
147+
/// control is disposing/its handle is destroyed (recommended). A common pattern is to
148+
/// use a token linked to the control's lifetime.
149+
/// </description>
150+
/// </item>
151+
/// </list>
103152
/// </remarks>
104153
public async Task<T> InvokeAsync<T>(Func<T> callback, CancellationToken cancellationToken = default)
105154
{
106155
ArgumentNullException.ThrowIfNull(callback);
107156

157+
if (!IsHandleCreated)
158+
{
159+
throw new InvalidOperationException(SR.ErrorNoMarshalingThread);
160+
}
161+
108162
if (cancellationToken.IsCancellationRequested)
109163
{
110164
return default!;
@@ -135,7 +189,15 @@ void WrappedCallback()
135189
}
136190
catch (Exception ex)
137191
{
138-
completion.TrySetException(ex);
192+
if (ex is OperationCanceledException oce
193+
&& oce.CancellationToken == cancellationToken)
194+
{
195+
completion.TrySetCanceled(cancellationToken);
196+
}
197+
else
198+
{
199+
completion.TrySetException(ex);
200+
}
139201
}
140202
}
141203
}
@@ -177,6 +239,24 @@ void WrappedCallback()
177239
/// For synchronous operations, use <see cref="InvokeAsync(Action, CancellationToken)"/> or
178240
/// <see cref="InvokeAsync{T}(Func{T}, CancellationToken)"/>.
179241
/// </para>
242+
/// <para>
243+
/// <b>Note:</b> If the control is disposed (or its handle is destroyed) before the
244+
/// marshaled callback runs, the returned task may never complete. This is the same
245+
/// behavior as <see cref="BeginInvoke(Delegate)"/>.
246+
/// To avoid this, either:
247+
/// </para>
248+
/// <list type="bullet">
249+
/// <item>
250+
/// <description>Ensure the control outlives the awaited operation, or</description>
251+
/// </item>
252+
/// <item>
253+
/// <description>
254+
/// Always pass a <see cref="CancellationToken"/> that you cancel when the
255+
/// control is disposing/its handle is destroyed (recommended). A common pattern is to
256+
/// use a token linked to the control's lifetime.
257+
/// </description>
258+
/// </item>
259+
/// </list>
180260
/// </remarks>
181261
public async Task InvokeAsync(
182262
Func<CancellationToken, ValueTask> callback,
@@ -223,7 +303,15 @@ async Task WrappedCallbackAsync()
223303
}
224304
catch (Exception ex)
225305
{
226-
completion.TrySetException(ex);
306+
if (ex is OperationCanceledException oce
307+
&& oce.CancellationToken == cancellationToken)
308+
{
309+
completion.TrySetCanceled(cancellationToken);
310+
}
311+
else
312+
{
313+
completion.TrySetException(ex);
314+
}
227315
}
228316
}
229317
}
@@ -265,6 +353,24 @@ async Task WrappedCallbackAsync()
265353
/// For synchronous operations, use <see cref="InvokeAsync(Action, CancellationToken)"/> or
266354
/// <see cref="InvokeAsync{T}(Func{T}, CancellationToken)"/>.
267355
/// </para>
356+
/// <para>
357+
/// <b>Note:</b> If the control is disposed (or its handle is destroyed) before the
358+
/// marshaled callback runs, the returned task may never complete. This is the same
359+
/// behavior as <see cref="BeginInvoke(Delegate)"/>.
360+
/// To avoid this, either:
361+
/// </para>
362+
/// <list type="bullet">
363+
/// <item>
364+
/// <description>Ensure the control outlives the awaited operation, or</description>
365+
/// </item>
366+
/// <item>
367+
/// <description>
368+
/// Always pass a <see cref="CancellationToken"/> that you cancel when the
369+
/// control is disposing/its handle is destroyed (recommended). A common pattern is to
370+
/// use a token linked to the control's lifetime.
371+
/// </description>
372+
/// </item>
373+
/// </list>
268374
/// </remarks>
269375
public async Task<T> InvokeAsync<T>(
270376
Func<CancellationToken, ValueTask<T>> callback,
@@ -311,7 +417,15 @@ async Task WrappedCallbackAsync()
311417
}
312418
catch (Exception ex)
313419
{
314-
completion.TrySetException(ex);
420+
if (ex is OperationCanceledException oce
421+
&& oce.CancellationToken == cancellationToken)
422+
{
423+
completion.TrySetCanceled(cancellationToken);
424+
}
425+
else
426+
{
427+
completion.TrySetException(ex);
428+
}
315429
}
316430
}
317431
}

0 commit comments

Comments
 (0)