@@ -29,11 +29,34 @@ public partial class Control
29
29
/// <see cref="InvokeAsync(Func{CancellationToken, ValueTask}, CancellationToken)"/> or
30
30
/// <see cref="InvokeAsync{T}(Func{CancellationToken, ValueTask{T}}, CancellationToken)"/>.
31
31
/// </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>
32
50
/// </remarks>
33
51
public async Task InvokeAsync ( Action callback , CancellationToken cancellationToken = default )
34
52
{
35
53
ArgumentNullException . ThrowIfNull ( callback ) ;
36
54
55
+ if ( ! IsHandleCreated )
56
+ {
57
+ throw new InvalidOperationException ( SR . ErrorNoMarshalingThread ) ;
58
+ }
59
+
37
60
if ( cancellationToken . IsCancellationRequested )
38
61
{
39
62
return ;
@@ -64,7 +87,15 @@ void WrappedAction()
64
87
}
65
88
catch ( Exception ex )
66
89
{
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
+ }
68
99
}
69
100
}
70
101
}
@@ -100,11 +131,34 @@ void WrappedAction()
100
131
/// in a "fire-and-forget" manner. To properly await asynchronous operations, use the overloads that accept
101
132
/// <see cref="Func{CancellationToken, ValueTask}"/> (or <see cref="ValueTask{T}"/>).
102
133
/// </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>
103
152
/// </remarks>
104
153
public async Task < T > InvokeAsync < T > ( Func < T > callback , CancellationToken cancellationToken = default )
105
154
{
106
155
ArgumentNullException . ThrowIfNull ( callback ) ;
107
156
157
+ if ( ! IsHandleCreated )
158
+ {
159
+ throw new InvalidOperationException ( SR . ErrorNoMarshalingThread ) ;
160
+ }
161
+
108
162
if ( cancellationToken . IsCancellationRequested )
109
163
{
110
164
return default ! ;
@@ -135,7 +189,15 @@ void WrappedCallback()
135
189
}
136
190
catch ( Exception ex )
137
191
{
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
+ }
139
201
}
140
202
}
141
203
}
@@ -177,6 +239,24 @@ void WrappedCallback()
177
239
/// For synchronous operations, use <see cref="InvokeAsync(Action, CancellationToken)"/> or
178
240
/// <see cref="InvokeAsync{T}(Func{T}, CancellationToken)"/>.
179
241
/// </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>
180
260
/// </remarks>
181
261
public async Task InvokeAsync (
182
262
Func < CancellationToken , ValueTask > callback ,
@@ -223,7 +303,15 @@ async Task WrappedCallbackAsync()
223
303
}
224
304
catch ( Exception ex )
225
305
{
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
+ }
227
315
}
228
316
}
229
317
}
@@ -265,6 +353,24 @@ async Task WrappedCallbackAsync()
265
353
/// For synchronous operations, use <see cref="InvokeAsync(Action, CancellationToken)"/> or
266
354
/// <see cref="InvokeAsync{T}(Func{T}, CancellationToken)"/>.
267
355
/// </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>
268
374
/// </remarks>
269
375
public async Task < T > InvokeAsync < T > (
270
376
Func < CancellationToken , ValueTask < T > > callback ,
@@ -311,7 +417,15 @@ async Task WrappedCallbackAsync()
311
417
}
312
418
catch ( Exception ex )
313
419
{
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
+ }
315
429
}
316
430
}
317
431
}
0 commit comments