From 53cb427ea0204c0eddb90d85fece382fd3b21320 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 14 Aug 2025 21:51:27 -0700 Subject: [PATCH 01/17] Initial updates; need code fixes --- .../controls/how-to-make-thread-safe-calls.md | 56 +++++++ .../cs/InvokeAsyncExamples.cs | 73 +++++++++ .../vb/InvokeAsyncExamples.vb | 65 ++++++++ dotnet-desktop-guide/winforms/forms/events.md | 98 ++++++++++++ .../snippets/events/cs/AsyncEventHandlers.cs | 147 ++++++++++++++++++ .../events/cs/AsyncEventHandlers.csproj | 11 ++ .../snippets/events/vb/AsyncEventHandlers.vb | 129 +++++++++++++++ .../events/vb/AsyncEventHandlers.vbproj | 9 ++ 8 files changed, 588 insertions(+) create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 7b6067a30e..fdccc4f875 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -3,6 +3,7 @@ title: How to make thread-safe calls to controls description: Learn how to implement multithreading in your app by calling cross-thread controls in a thread-safe way. If you encounter the 'cross-thread operation not valid' error, use the InvokeRequired property to detect this error. The BackgroundWorker component is also an alternative to creating new threads. ms.date: 06/20/2021 ms.service: dotnet-desktop +ai-usage: ai-assisted dev_langs: - "csharp" - "vb" @@ -61,3 +62,58 @@ The example counts from 0 to 10 in the `DoWork` event, pausing for one second be :::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs" id="Background"::: :::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb" id="Background"::: + +## Example: Use Control.InvokeAsync (.NET 9 and later) + +Starting with .NET 9, Windows Forms includes the method, which provides async-friendly marshaling to the UI thread. This method is particularly useful for async event handlers and eliminates many common deadlock scenarios. + +> [!NOTE] +> `Control.InvokeAsync` is only available in .NET 9 and later. It is not supported in .NET Framework. + +### Understanding the difference: Invoke vs InvokeAsync + +**Control.Invoke (Sending - Blocking):** + +- Synchronously sends the delegate to the UI thread's message queue +- The calling thread waits until the UI thread processes the delegate +- Can lead to UI freezes if overused during long-running operations +- Useful when immediate results are needed from the UI thread + +**Control.InvokeAsync (Posting - Non-blocking):** + +- Asynchronously posts the delegate to the UI thread's message queue +- The calling thread doesn't wait and continues its work immediately +- Returns a `Task` that can be awaited for completion +- Ideal for async scenarios and prevents UI thread bottlenecks + +### Choosing the right InvokeAsync overload + +`Control.InvokeAsync` provides four overloads for different scenarios: + +| Overload | Use Case | Example | +|----------|----------|---------| +| `InvokeAsync(Action)` | Sync operation, no return value | Update control properties | +| `InvokeAsync(Func)` | Sync operation, with return value | Get control state | +| `InvokeAsync(Func)` | Async operation, no return value | Long-running UI updates | +| `InvokeAsync(Func>)` | Async operation, with return value | Async data fetching with result | + +The following example demonstrates using `InvokeAsync` to safely update controls from a background thread: + +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs" id="snippet_InvokeAsyncBasic"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb" id="snippet_InvokeAsyncBasic"::: + +For async operations that need to run on the UI thread, use the async overload: + +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs" id="snippet_InvokeAsyncAdvanced"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb" id="snippet_InvokeAsyncAdvanced"::: + +### Advantages of InvokeAsync + +- **Async-friendly**: Returns a `Task` that can be awaited +- **Deadlock prevention**: Eliminates common deadlock scenarios found with synchronous invoke patterns +- **Non-blocking**: Doesn't block the calling thread, improving overall application responsiveness +- **Cancellation support**: Supports `CancellationToken` for operation cancellation +- **Exception propagation**: Properly propagates exceptions back to the calling code +- **Analyzer support**: .NET 9 includes analyzer warnings (WFO2001) to detect potential misuse + +For comprehensive guidance on async event handlers and best practices, see [Events overview](../forms/events.md#async-event-handlers). diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs new file mode 100644 index 0000000000..5c21cb764b --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs @@ -0,0 +1,73 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace ThreadSafeCallsExample +{ + public partial class InvokeAsyncForm : Form + { + private Button button1; + private Button button2; + private TextBox textBox1; + + // + private async void button1_Click(object sender, EventArgs e) + { + button1.Enabled = false; + + try + { + // Perform background work + await Task.Run(async () => + { + for (int i = 0; i <= 100; i += 10) + { + // Simulate work + await Task.Delay(100); + + // Update UI safely from background thread + await textBox1.InvokeAsync(() => + { + textBox1.Text = $"Progress: {i}%"; + }); + } + }); + + // Update UI after completion + await textBox1.InvokeAsync(() => + { + textBox1.Text = "Operation completed!"; + }); + } + finally + { + button1.Enabled = true; + } + } + // + + // + private async void button2_Click(object sender, EventArgs e) + { + await this.InvokeAsync(async (cancellationToken) => + { + // This runs on UI thread but doesn't block it + textBox1.Text = "Starting operation..."; + + // Perform async work on UI thread + var result = await SomeAsyncApiCall(); + + // Update UI directly since we're on UI thread + textBox1.Text = $"Result: {result}"; + }); + } + + private async Task SomeAsyncApiCall() + { + using var client = new HttpClient(); + return await client.GetStringAsync("https://api.example.com/data"); + } + // + } +} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb new file mode 100644 index 0000000000..ee56e6ad2d --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb @@ -0,0 +1,65 @@ +Imports System +Imports System.Net.Http +Imports System.Threading.Tasks +Imports System.Windows.Forms + +Namespace ThreadSafeCallsExample + Public Partial Class InvokeAsyncForm + Inherits Form + + Private button1 As Button + Private button2 As Button + Private textBox1 As TextBox + + ' + Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click + Button1.Enabled = False + + Try + ' Perform background work + Await Task.Run(Async Function() + For i As Integer = 0 To 100 Step 10 + ' Simulate work + Await Task.Delay(100) + + ' Update UI safely from background thread + Await TextBox1.InvokeAsync(Sub() + TextBox1.Text = $"Progress: {i}%" + End Sub) + Next + End Function) + + ' Update UI after completion + Await TextBox1.InvokeAsync(Sub() + TextBox1.Text = "Operation completed!" + End Sub) + Finally + Button1.Enabled = True + End Try + End Sub + ' + + ' + Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click + Await Me.InvokeAsync(Async Function(cancellationToken) + ' This runs on UI thread but doesn't block it + TextBox1.Text = "Starting operation..." + + ' Perform async work on UI thread + Dim result As String = Await SomeAsyncApiCall() + + ' Update UI directly since we're on UI thread + TextBox1.Text = $"Result: {result}" + + Return Nothing + End Function) + End Sub + + Private Async Function SomeAsyncApiCall() As Task(Of String) + Using client As New HttpClient() + Return Await client.GetStringAsync("https://api.example.com/data") + End Using + End Function + ' + End Class +End Namespace diff --git a/dotnet-desktop-guide/winforms/forms/events.md b/dotnet-desktop-guide/winforms/forms/events.md index ea443e7c28..1fbaf5b32e 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -4,6 +4,7 @@ description: "A brief overview about events with .NET Windows Forms." ms.date: 04/02/2025 ms.service: dotnet-desktop ms.topic: overview +ai-usage: ai-assisted dev_langs: ["csharp", "vb"] helpviewer_keywords: - "Windows Forms, event handling" @@ -66,6 +67,103 @@ Typically each event produces an event handler with a different event-object typ You can also use the same event handler to handle the same event for different controls. For example, if you have a group of controls on a form, you could create a single event handler for the event of every `RadioButton`. For more information, see [How to handle a control event](../controls/how-to-add-an-event-handler.md#how-to-use-multiple-events-with-the-same-handler). +## Async event handlers + +Modern applications often need to perform asynchronous operations in response to user actions, such as downloading data from a web service or accessing files. Windows Forms event handlers can be declared as `async` methods to support these scenarios, but there are important considerations to avoid common pitfalls. + +### Basic async event handler pattern + +Event handlers can be declared with the `async` modifier and use `await` for asynchronous operations. Since event handlers must return `void`, they are one of the rare acceptable uses of `async void`: + +:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_BasicAsyncEventHandler"::: +:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: + +> [!IMPORTANT] +> While `async void` is generally discouraged, it's necessary for event handlers since they cannot return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the example above. + +### Common pitfalls and deadlocks + +> [!WARNING] +> Never use blocking calls like `.Wait()`, `.Result`, or `.GetAwaiter().GetResult()` in event handlers or any UI code. These patterns can cause deadlocks. + +The following code demonstrates a common anti-pattern that causes deadlocks: + +:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_DeadlockAntiPattern"::: +:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_DeadlockAntiPattern"::: + +**Why this causes deadlocks:** + +1. The UI thread calls the async method and blocks waiting for the result +2. The async method captures the UI thread's `SynchronizationContext` +3. When the async operation completes, it tries to continue on the captured UI thread +4. The UI thread is blocked waiting for the operation to complete +5. Deadlock occurs because neither operation can proceed + +### Cross-thread operations + +When you need to update UI controls from background threads within async operations, use the appropriate marshaling techniques. Understanding the difference between blocking and non-blocking approaches is crucial for responsive applications. + +# [.NET](#tab/dotnet) + +.NET 9 introduced , which provides async-friendly marshaling to the UI thread. Unlike `Control.Invoke` which **sends** (blocks the calling thread), `InvokeAsync` **posts** (non-blocking) to the UI thread's message queue. + +**Key advantages of InvokeAsync:** + +- **Non-blocking**: Returns immediately, allowing the calling thread to continue +- **Async-friendly**: Returns a `Task` that can be awaited +- **Exception propagation**: Properly propagates exceptions back to the calling code +- **Cancellation support**: Supports `CancellationToken` for operation cancellation + +**Choosing the right overload:** + +| Scenario | Overload | Example | +|----------|----------|---------| +| Sync operation, no return value | `InvokeAsync(Action)` | `await control.InvokeAsync(() => label.Text = "Done")` | +| Sync operation, with return value | `InvokeAsync(Func)` | `int count = await control.InvokeAsync(() => listBox.Items.Count)` | +| Async operation, no return value | `InvokeAsync(Func)` | `await control.InvokeAsync(async ct => await UpdateUIAsync())` | +| Async operation, with return value | `InvokeAsync(Func>)` | `var result = await control.InvokeAsync(async ct => await ComputeAsync())` | + +:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_InvokeAsyncNet9"::: +:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_InvokeAsyncNet9"::: + +For truly async operations that need to run on the UI thread: + +:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_InvokeAsyncUIThread"::: +:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_InvokeAsyncUIThread"::: + +> [!TIP] +> .NET 9 includes analyzer warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) to help detect when async methods are incorrectly passed to synchronous overloads of `InvokeAsync`. This helps prevent "fire-and-forget" behavior. + +# [.NET Framework](#tab/dotnetframework) + +For applications targeting .NET Framework, use traditional patterns with proper async handling: + +:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_LegacyNetFramework"::: +:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_LegacyNetFramework"::: + +--- + +### Best practices + +- **Use async/await consistently**: Don't mix async patterns with blocking calls +- **Handle exceptions**: Always wrap async operations in try-catch blocks in `async void` event handlers +- **Provide user feedback**: Update the UI to show operation progress or status +- **Disable controls during operations**: Prevent users from starting multiple operations +- **Use CancellationToken**: Support operation cancellation for long-running tasks +- **Consider ConfigureAwait(false)**: Use in library code to avoid capturing the UI context when not needed + +### Additional async APIs (.NET 9) + +.NET 9 also introduces experimental async APIs for forms and dialogs: + +- **Form.ShowAsync()** and **Form.ShowDialogAsync()**: Show forms asynchronously +- **TaskDialog.ShowDialogAsync()**: Display task dialogs asynchronously + +> [!NOTE] +> These APIs are experimental and require suppressing compiler warning WFO5002. Add `$(NoWarn);WFO5002` to your project file to use them. + +For more detailed information about thread-safe operations in Windows Forms, see [How to make thread-safe calls to controls](../controls/how-to-make-thread-safe-calls.md). + ## Related content - [Handling and raising events in .NET](/dotnet/standard/events/index) diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs new file mode 100644 index 0000000000..8c57406e4c --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs @@ -0,0 +1,147 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AsyncEventHandlersExample +{ + public partial class ExampleForm : Form + { + private Button downloadButton; + private Button processButton; + private Button complexButton; + private Button badButton; + private TextBox resultTextBox; + private Label statusLabel; + private ProgressBar progressBar; + + // + private async void downloadButton_Click(object sender, EventArgs e) + { + downloadButton.Enabled = false; + statusLabel.Text = "Downloading..."; + + try + { + using var httpClient = new HttpClient(); + string content = await httpClient.GetStringAsync("https://api.example.com/data"); + + // Update UI with the result + resultTextBox.Text = content; + statusLabel.Text = "Download complete"; + } + catch (Exception ex) + { + statusLabel.Text = $"Error: {ex.Message}"; + } + finally + { + downloadButton.Enabled = true; + } + } + // + + // + // DON'T DO THIS - causes deadlocks + private void badButton_Click(object sender, EventArgs e) + { + try + { + using var httpClient = new HttpClient(); + // This blocks the UI thread and causes a deadlock + string content = httpClient.GetStringAsync("https://api.example.com/data").GetAwaiter().GetResult(); + resultTextBox.Text = content; + } + catch (Exception ex) + { + MessageBox.Show($"Error: {ex.Message}"); + } + } + // + + // + private async void processButton_Click(object sender, EventArgs e) + { + processButton.Enabled = false; + + // Start background work + await Task.Run(async () => + { + for (int i = 0; i <= 100; i += 10) + { + // Simulate work + await Task.Delay(200); + + // Update UI safely from background thread + await progressBar.InvokeAsync(() => + { + progressBar.Value = i; + statusLabel.Text = $"Progress: {i}%"; + }); + } + }); + + processButton.Enabled = true; + } + // + + // + private async void complexButton_Click(object sender, EventArgs e) + { + await this.InvokeAsync(async (cancellationToken) => + { + // This runs on UI thread but doesn't block it + statusLabel.Text = "Starting complex operation..."; + + var result = await SomeAsyncApiCall(); + + // Update UI directly since we're already on UI thread + resultTextBox.Text = result; + statusLabel.Text = "Operation completed"; + }); + } + + private async Task SomeAsyncApiCall() + { + using var httpClient = new HttpClient(); + return await httpClient.GetStringAsync("https://api.example.com/data"); + } + // + + // + private async void legacyButton_Click(object sender, EventArgs e) + { + var legacyButton = sender as Button; + legacyButton.Enabled = false; + + try + { + // Move to background thread to avoid blocking UI + await Task.Run(async () => + { + var result = await SomeAsyncOperation(); + + // Marshal back to UI thread + this.Invoke(new Action(() => + { + resultTextBox.Text = result; + statusLabel.Text = "Complete"; + })); + }); + } + finally + { + // This runs on the UI thread since the await completed + legacyButton.Enabled = true; + } + } + + private async Task SomeAsyncOperation() + { + using var httpClient = new HttpClient(); + return await httpClient.GetStringAsync("https://api.example.com/data"); + } + // + } +} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj new file mode 100644 index 0000000000..96e6c47284 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj @@ -0,0 +1,11 @@ + + + + WinExe + net9.0-windows + true + enable + enable + + + diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb new file mode 100644 index 0000000000..1339f769fb --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb @@ -0,0 +1,129 @@ +Imports System +Imports System.Net.Http +Imports System.Threading +Imports System.Threading.Tasks +Imports System.Windows.Forms + +Namespace AsyncEventHandlersExample + Public Partial Class ExampleForm + Inherits Form + + Private downloadButton As Button + Private processButton As Button + Private complexButton As Button + Private badButton As Button + Private resultTextBox As TextBox + Private statusLabel As Label + Private progressBar As ProgressBar + + ' + Private Async Sub downloadButton_Click(sender As Object, e As EventArgs) Handles downloadButton.Click + downloadButton.Enabled = False + statusLabel.Text = "Downloading..." + + Try + Using httpClient As New HttpClient() + Dim content As String = Await httpClient.GetStringAsync("https://api.example.com/data") + + ' Update UI with the result + resultTextBox.Text = content + statusLabel.Text = "Download complete" + End Using + Catch ex As Exception + statusLabel.Text = $"Error: {ex.Message}" + Finally + downloadButton.Enabled = True + End Try + End Sub + ' + + ' + ' DON'T DO THIS - causes deadlocks + Private Sub badButton_Click(sender As Object, e As EventArgs) Handles badButton.Click + Try + Using httpClient As New HttpClient() + ' This blocks the UI thread and causes a deadlock + Dim content As String = httpClient.GetStringAsync("https://api.example.com/data").GetAwaiter().GetResult() + resultTextBox.Text = content + End Using + Catch ex As Exception + MessageBox.Show($"Error: {ex.Message}") + End Try + End Sub + ' + + ' + Private Async Sub processButton_Click(sender As Object, e As EventArgs) Handles processButton.Click + processButton.Enabled = False + + ' Start background work + Await Task.Run(Async Function() + For i As Integer = 0 To 100 Step 10 + ' Simulate work + Await Task.Delay(200) + + ' Update UI safely from background thread + Await progressBar.InvokeAsync(Sub() + progressBar.Value = i + statusLabel.Text = $"Progress: {i}%" + End Sub) + Next + End Function) + + processButton.Enabled = True + End Sub + ' + + ' + Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click + Await Me.InvokeAsync(Async Function(cancellationToken) + ' This runs on UI thread but doesn't block it + statusLabel.Text = "Starting complex operation..." + + Dim result As String = Await SomeAsyncApiCall() + + ' Update UI directly since we're already on UI thread + resultTextBox.Text = result + statusLabel.Text = "Operation completed" + + Return Nothing + End Function) + End Sub + + Private Async Function SomeAsyncApiCall() As Task(Of String) + Using httpClient As New HttpClient() + Return Await httpClient.GetStringAsync("https://api.example.com/data") + End Using + End Function + ' + + ' + Private Async Sub legacyButton_Click(sender As Object, e As EventArgs) Handles legacyButton.Click + Dim legacyButton As Button = TryCast(sender, Button) + legacyButton.Enabled = False + + Try + ' Move to background thread to avoid blocking UI + Await Task.Run(Async Function() + Dim result As String = Await SomeAsyncOperation() + + ' Marshal back to UI thread + Me.Invoke(New Action(Sub() + resultTextBox.Text = result + statusLabel.Text = "Complete" + End Sub)) + End Function) + Finally + ' This runs on the UI thread since the await completed + legacyButton.Enabled = True + End Try + End Sub + + Private Async Function SomeAsyncOperation() As Task(Of String) + Using httpClient As New HttpClient() + Return Await httpClient.GetStringAsync("https://api.example.com/data") + End Using + End Function + ' + End Class +End Namespace diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj new file mode 100644 index 0000000000..5ced7dea55 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj @@ -0,0 +1,9 @@ + + + + WinExe + net9.0-windows + true + + + From dc1a6e8556ebf3bc4c110d4fd276ccee9eb84a4c Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 12:06:12 -0700 Subject: [PATCH 02/17] Fix sample code --- .../cs/Form1.Designer.cs | 123 +++++++------- .../how-to-make-thread-safe-calls/cs/Form1.cs | 55 +++--- .../cs/FormBackgroundWorker.Designer.cs | 139 ++++++++------- .../cs/FormBackgroundWorker.cs | 51 +++--- .../cs/FormBad.Designer.cs | 121 +++++++------ .../cs/FormBad.cs | 31 ++-- .../cs/FormThread.Designer.cs | 123 +++++++------- .../cs/FormThread.cs | 61 ++++--- .../cs/InvokeAsyncExamples.cs | 129 ++++++++------ .../cs/InvokeAsyncExamples.resx | 120 +++++++++++++ .../cs/Program.cs | 25 ++- .../vb/InvokeAsyncExamples.resx | 120 +++++++++++++ .../vb/InvokeAsyncExamples.vb | 159 +++++++++++------- .../snippets/events/cs/AsyncEventHandlers.cs | 7 +- .../events/cs/AsyncEventHandlers.csproj | 8 +- .../snippets/events/cs/Form1.Designer.cs | 38 +++++ .../forms/snippets/events/cs/Form1.cs | 9 + .../forms/snippets/events/cs/Program.cs | 16 ++ .../snippets/events/vb/AsyncEventHandlers.vb | 36 ++-- .../events/vb/AsyncEventHandlers.vbproj | 14 +- .../snippets/events/vb/Form1.Designer.vb | 31 ++++ .../forms/snippets/events/vb/Form1.vb | 3 + .../forms/snippets/events/vb/Program.vb | 11 ++ 23 files changed, 930 insertions(+), 500 deletions(-) create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs index 48a1b183b9..78d58eb2ea 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs @@ -1,73 +1,72 @@ -namespace project +namespace project; + +partial class Form1 { - partial class Form1 - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); + components.Dispose(); } + base.Dispose(disposing); + } - #region Windows Form Designer generated code + #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(46, 133); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(30, 37); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(333, 23); - this.textBox1.TabIndex = 1; - // - // Form1 - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "Form1"; - this.Text = "Form1"; - this.Load += new System.EventHandler(this.Form1_Load); - this.ResumeLayout(false); - this.PerformLayout(); + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(46, 133); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(30, 37); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(333, 23); + this.textBox1.TabIndex = 1; + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.button1); + this.Name = "Form1"; + this.Text = "Form1"; + this.Load += new System.EventHandler(this.Form1_Load); + this.ResumeLayout(false); + this.PerformLayout(); - } + } - #endregion + #endregion - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; - } + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox1; } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs index 27de7e3db7..7c6afb53e5 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs @@ -9,40 +9,39 @@ using System.Threading.Tasks; using System.Windows.Forms; -namespace project +namespace project; + +public partial class Form1 : Form { - public partial class Form1 : Form + public Form1() { - public Form1() - { - InitializeComponent(); - } + InitializeComponent(); + } - private void Form1_Load(object sender, EventArgs e) - { + private void Form1_Load(object sender, EventArgs e) + { - } + } - private async void button1_Click(object sender, EventArgs e) - { - //var thread2 = new Thread(new ThreadStart(WriteTextUnsafe)); - //thread2.Start(); - await Task.Factory.StartNew(WriteTextUnsafe, new CancellationToken(false), TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); - await Task.Delay(6000); - Action de = delegate { WriteTextSafe(); }; - textBox1.Invoke(de); - } + private async void button1_Click(object sender, EventArgs e) + { + //var thread2 = new Thread(new ThreadStart(WriteTextUnsafe)); + //thread2.Start(); + await Task.Factory.StartNew(WriteTextUnsafe, new CancellationToken(false), TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); + await Task.Delay(6000); + Action de = delegate { WriteTextSafe(); }; + textBox1.Invoke(de); + } - private async void WriteTextUnsafe() - { - textBox1.Text = "This text was set unsafely."; - await Task.Delay(4000); - textBox1.Text = "This text was set unsafely twice!"; - } + private async void WriteTextUnsafe() + { + textBox1.Text = "This text was set unsafely."; + await Task.Delay(4000); + textBox1.Text = "This text was set unsafely twice!"; + } - private void WriteTextSafe() - { - textBox1.Text = "This text was set safely."; - } + private void WriteTextSafe() + { + textBox1.Text = "This text was set safely."; } } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.Designer.cs index aecb7f459e..edce9e2dfd 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.Designer.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.Designer.cs @@ -1,80 +1,79 @@  -namespace project +namespace project; + +partial class FormBackgroundWorker { - partial class FormBackgroundWorker - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); + components.Dispose(); } + base.Dispose(disposing); + } - #region Windows Form Designer generated code + #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(24, 135); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(24, 106); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(236, 23); - this.textBox1.TabIndex = 1; - // - // backgroundWorker1 - // - this.backgroundWorker1.WorkerReportsProgress = true; - this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork); - this.backgroundWorker1.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.backgroundWorker1_ProgressChanged); - // - // FormBackgroundWorker - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "FormBackgroundWorker"; - this.Text = "FormBackgroundWorker"; - this.ResumeLayout(false); - this.PerformLayout(); + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(24, 135); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(24, 106); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(236, 23); + this.textBox1.TabIndex = 1; + // + // backgroundWorker1 + // + this.backgroundWorker1.WorkerReportsProgress = true; + this.backgroundWorker1.DoWork += new System.ComponentModel.DoWorkEventHandler(this.backgroundWorker1_DoWork); + this.backgroundWorker1.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.backgroundWorker1_ProgressChanged); + // + // FormBackgroundWorker + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.button1); + this.Name = "FormBackgroundWorker"; + this.Text = "FormBackgroundWorker"; + this.ResumeLayout(false); + this.PerformLayout(); - } + } - #endregion + #endregion - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; - private System.ComponentModel.BackgroundWorker backgroundWorker1; - } -} \ No newline at end of file + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox1; + private System.ComponentModel.BackgroundWorker backgroundWorker1; +} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs index 8480bf8959..e166dcf60c 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs @@ -8,37 +8,36 @@ using System.Threading.Tasks; using System.Windows.Forms; -namespace project +namespace project; + +public partial class FormBackgroundWorker : Form { - public partial class FormBackgroundWorker : Form + public FormBackgroundWorker() { - public FormBackgroundWorker() - { - InitializeComponent(); - } + InitializeComponent(); + } - // - private void button1_Click(object sender, EventArgs e) - { - if (!backgroundWorker1.IsBusy) - backgroundWorker1.RunWorkerAsync(); - } + // + private void button1_Click(object sender, EventArgs e) + { + if (!backgroundWorker1.IsBusy) + backgroundWorker1.RunWorkerAsync(); + } - private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) - { - int counter = 0; - int max = 10; + private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) + { + int counter = 0; + int max = 10; - while (counter <= max) - { - backgroundWorker1.ReportProgress(0, counter.ToString()); - System.Threading.Thread.Sleep(1000); - counter++; - } + while (counter <= max) + { + backgroundWorker1.ReportProgress(0, counter.ToString()); + System.Threading.Thread.Sleep(1000); + counter++; } - - private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) => - textBox1.Text = (string)e.UserState; - // } + + private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) => + textBox1.Text = (string)e.UserState; + // } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs index 9fbf81a92d..502e74e9e2 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs @@ -1,72 +1,71 @@  -namespace project +namespace project; + +partial class FormBad { - partial class FormBad - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); + components.Dispose(); } + base.Dispose(disposing); + } - #region Windows Form Designer generated code + #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(57, 165); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(57, 136); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(217, 23); - this.textBox1.TabIndex = 1; - // - // FormBad - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "FormBad"; - this.Text = "FormBad"; - this.ResumeLayout(false); - this.PerformLayout(); + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(57, 165); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(57, 136); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(217, 23); + this.textBox1.TabIndex = 1; + // + // FormBad + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.button1); + this.Name = "FormBad"; + this.Text = "FormBad"; + this.ResumeLayout(false); + this.PerformLayout(); - } + } - #endregion + #endregion - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; - } + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox1; } \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs index 9a854be92c..123869824f 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs @@ -8,24 +8,23 @@ using System.Threading.Tasks; using System.Windows.Forms; -namespace project +namespace project; + +public partial class FormBad : Form { - public partial class FormBad : Form + public FormBad() { - public FormBad() - { - InitializeComponent(); - } - - // - private void button1_Click(object sender, EventArgs e) - { - var thread2 = new System.Threading.Thread(WriteTextUnsafe); - thread2.Start(); - } + InitializeComponent(); + } - private void WriteTextUnsafe() => - textBox1.Text = "This text was set unsafely."; - // + // + private void button1_Click(object sender, EventArgs e) + { + System.Threading.Thread thread2 = new(WriteTextUnsafe); + thread2.Start(); } + + private void WriteTextUnsafe() => + textBox1.Text = "This text was set unsafely."; + // } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs index a3f5ee942e..29ac43a8bf 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs @@ -1,73 +1,72 @@  -namespace project +namespace project; + +partial class FormThread { - partial class FormThread - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); + components.Dispose(); } + base.Dispose(disposing); + } - #region Windows Form Designer generated code + #region Windows Form Designer generated code - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(21, 84); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(21, 55); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(267, 23); - this.textBox1.TabIndex = 1; - this.textBox1.Enter += new System.EventHandler(this.textBox1_Enter); - // - // FormThread - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "FormThread"; - this.Text = "FormThread"; - this.ResumeLayout(false); - this.PerformLayout(); + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.textBox1 = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(21, 84); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(75, 23); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.button1_Click); + // + // textBox1 + // + this.textBox1.Location = new System.Drawing.Point(21, 55); + this.textBox1.Name = "textBox1"; + this.textBox1.Size = new System.Drawing.Size(267, 23); + this.textBox1.TabIndex = 1; + this.textBox1.Enter += new System.EventHandler(this.textBox1_Enter); + // + // FormThread + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.textBox1); + this.Controls.Add(this.button1); + this.Name = "FormThread"; + this.Text = "FormThread"; + this.ResumeLayout(false); + this.PerformLayout(); - } + } - #endregion + #endregion - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; - } + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox1; } \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs index bdde2acee9..d1ff1d0f4b 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs @@ -8,43 +8,42 @@ using System.Threading.Tasks; using System.Windows.Forms; -namespace project +namespace project; + +public partial class FormThread : Form { - public partial class FormThread : Form + public FormThread() { - public FormThread() - { - InitializeComponent(); - } + InitializeComponent(); + } - // - private void button1_Click(object sender, EventArgs e) - { - var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); }); - var thread2 = new System.Threading.Thread(threadParameters); - thread2.Start(); - } + // + private void button1_Click(object sender, EventArgs e) + { + var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); }); + var thread2 = new System.Threading.Thread(threadParameters); + thread2.Start(); + } - public void WriteTextSafe(string text) + public void WriteTextSafe(string text) + { + if (textBox1.InvokeRequired) { - if (textBox1.InvokeRequired) - { - // Call this same method but append THREAD2 to the text - Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); }; - textBox1.Invoke(safeWrite); - } - else - textBox1.Text = text; + // Call this same method but append THREAD2 to the text + Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); }; + textBox1.Invoke(safeWrite); } - // + else + textBox1.Text = text; + } + // - private void textBox1_Enter(object sender, System.EventArgs e) - { - //if (!String.IsNullOrEmpty(textBox1.Text)) - //{ - // textBox1.SelectionStart = 0; - // textBox1.SelectionLength = textBox1.Text.Length; - //} - } + private void textBox1_Enter(object sender, System.EventArgs e) + { + //if (!String.IsNullOrEmpty(textBox1.Text)) + //{ + // textBox1.SelectionStart = 0; + // textBox1.SelectionLength = textBox1.Text.Length; + //} } } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs index 5c21cb764b..a3c68f4dd9 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs @@ -1,73 +1,108 @@ -using System; +using System; using System.Net.Http; +using System.Threading; // Added for CancellationToken using System.Threading.Tasks; using System.Windows.Forms; -namespace ThreadSafeCallsExample +namespace project; + +public partial class InvokeAsyncForm : Form { - public partial class InvokeAsyncForm : Form + private Button button1; + private Button button2; + private TextBox textBox1; + + public InvokeAsyncForm() { - private Button button1; - private Button button2; - private TextBox textBox1; + button1 = new Button { Text = "Invoke Async Basic", Location = new System.Drawing.Point(10, 10), Width = 200 }; + button2 = new Button { Text = "Invoke Async Advanced", Location = new System.Drawing.Point(10, 50), Width = 200 }; + textBox1 = new TextBox { Location = new System.Drawing.Point(10, 90), Width = 200, Multiline = true, Height =500 }; + button1.Click += button1_Click; + button2.Click += button2_Click; + Controls.Add(button1); + Controls.Add(button2); + Controls.Add(textBox1); + } - // - private async void button1_Click(object sender, EventArgs e) + // + private async void button1_Click(object sender, EventArgs e) + { + button1.Enabled = false; + + try { - button1.Enabled = false; - - try + // Perform background work + await Task.Run(async () => { - // Perform background work - await Task.Run(async () => + for (int i = 0; i <= 100; i += 10) { - for (int i = 0; i <= 100; i += 10) + // Simulate work + await Task.Delay(100); + + // Create local variable to avoid closure issues + int currentProgress = i; + + // Update UI safely from background thread + await textBox1.InvokeAsync(() => { - // Simulate work - await Task.Delay(100); - - // Update UI safely from background thread - await textBox1.InvokeAsync(() => - { - textBox1.Text = $"Progress: {i}%"; - }); - } - }); - - // Update UI after completion - await textBox1.InvokeAsync(() => - { - textBox1.Text = "Operation completed!"; - }); - } - finally + textBox1.Text = $"Progress: {currentProgress}%"; + }); + } + }); + + // Update UI after completion + await textBox1.InvokeAsync(() => { - button1.Enabled = true; - } + textBox1.Text = "Operation completed!"; + }); + } + finally + { + button1.Enabled = true; } - // + } + // - // - private async void button2_Click(object sender, EventArgs e) + // + private async void button2_Click(object sender, EventArgs e) + { + button2.Enabled = false; + try { await this.InvokeAsync(async (cancellationToken) => { // This runs on UI thread but doesn't block it textBox1.Text = "Starting operation..."; - // Perform async work on UI thread - var result = await SomeAsyncApiCall(); - - // Update UI directly since we're on UI thread - textBox1.Text = $"Result: {result}"; + try + { + // Perform async work on UI thread + var result = await SomeAsyncApiCall(cancellationToken); + + // Update UI directly since we're on UI thread + textBox1.Text = $"Result: {result}"; + } + catch (OperationCanceledException) + { + textBox1.Text = "Operation canceled."; + } + catch (Exception ex) + { + textBox1.Text = $"Error: {ex.Message}"; + } }); } - - private async Task SomeAsyncApiCall() + finally { - using var client = new HttpClient(); - return await client.GetStringAsync("https://api.example.com/data"); + button2.Enabled = true; } - // } + + private async Task SomeAsyncApiCall(CancellationToken cancellationToken) + { + using var client = new HttpClient(); + await Task.Delay(2000, cancellationToken); // Simulate network delay + return await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md", cancellationToken); + } + // } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs index 745b07829e..4b4c699ac6 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs @@ -4,20 +4,19 @@ using System.Threading.Tasks; using System.Windows.Forms; -namespace project +namespace project; + +static class Program { - static class Program + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - Application.SetHighDpiMode(HighDpiMode.SystemAware); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new FormBackgroundWorker()); - } + Application.SetHighDpiMode(HighDpiMode.SystemAware); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); } } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb index ee56e6ad2d..2789fee876 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb @@ -1,65 +1,104 @@ -Imports System +Imports System Imports System.Net.Http +Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms -Namespace ThreadSafeCallsExample - Public Partial Class InvokeAsyncForm - Inherits Form - - Private button1 As Button - Private button2 As Button - Private textBox1 As TextBox - - ' - Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click - Button1.Enabled = False - - Try - ' Perform background work - Await Task.Run(Async Function() - For i As Integer = 0 To 100 Step 10 - ' Simulate work - Await Task.Delay(100) - - ' Update UI safely from background thread - Await TextBox1.InvokeAsync(Sub() - TextBox1.Text = $"Progress: {i}%" - End Sub) - Next - End Function) - - ' Update UI after completion - Await TextBox1.InvokeAsync(Sub() - TextBox1.Text = "Operation completed!" - End Sub) - Finally - Button1.Enabled = True - End Try - End Sub - ' - - ' - Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click - Await Me.InvokeAsync(Async Function(cancellationToken) - ' This runs on UI thread but doesn't block it - TextBox1.Text = "Starting operation..." - - ' Perform async work on UI thread - Dim result As String = Await SomeAsyncApiCall() - - ' Update UI directly since we're on UI thread - TextBox1.Text = $"Result: {result}" - - Return Nothing - End Function) - End Sub - - Private Async Function SomeAsyncApiCall() As Task(Of String) - Using client As New HttpClient() - Return Await client.GetStringAsync("https://api.example.com/data") +Partial Public Class InvokeAsyncForm + Inherits Form + + Private WithEvents button1 As Button + Private WithEvents button2 As Button + Private textBox1 As TextBox + + Public Sub New() + button1 = New Button With {.Text = "Invoke Async Basic", .Location = New System.Drawing.Point(10, 10), .Width = 200} + button2 = New Button With {.Text = "Invoke Async Advanced", .Location = New System.Drawing.Point(10, 50), .Width = 200} + textBox1 = New TextBox With {.Location = New System.Drawing.Point(10, 90), .Width = 200, .Multiline = True, .Height = 500} + Controls.Add(button1) + Controls.Add(button2) + Controls.Add(textBox1) + End Sub + + ' + Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click + button1.Enabled = False + + Try + ' Perform background work + Await Task.Run(Async Function() + For i As Integer = 0 To 100 Step 10 + ' Simulate work + Await Task.Delay(100) + + ' Create local variable to avoid closure issues + Dim currentProgress As Integer = i + + ' Update UI safely from background thread + Await textBox1.InvokeAsync(Sub() + textBox1.Text = $"Progress: {currentProgress}%" + End Sub) + Next + End Function) + + ' Update UI after completion + Await textBox1.InvokeAsync(Sub() + textBox1.Text = "Operation completed!" + End Sub) + Finally + button1.Enabled = True + End Try + End Sub + ' + + ' + Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click + button2.Enabled = False + Try + ' Create a cancellation token source for this operation + Using cts As New CancellationTokenSource() + ' For VB.NET, use a separate method to handle the async operation with cancellation + Await AsyncOperationWithCancellation(cts.Token) End Using - End Function - ' - End Class -End Namespace + Finally + button2.Enabled = True + End Try + End Sub + + Private Async Function AsyncOperationWithCancellation(cancellationToken As CancellationToken) As Task + ' Update UI to show starting state + Await Me.InvokeAsync(Sub() + textBox1.Text = "Starting operation..." + End Sub, cancellationToken) + + Dim resultMessage As String = "" + + Try + ' Perform async work with cancellation support + Dim result As String = Await SomeAsyncApiCall(cancellationToken) + resultMessage = $"Result: {result}" + + Catch ex As OperationCanceledException + resultMessage = "Operation canceled." + + Catch ex As Exception + resultMessage = $"Error: {ex.Message}" + + End Try + + ' Update UI with final result + Await Me.InvokeAsync(Sub() + textBox1.Text = resultMessage + End Sub, cancellationToken) + End Function + + Private Async Function SomeAsyncApiCall(cancellationToken As CancellationToken) As Task(Of String) + + Using client As New HttpClient() + Await Task.Delay(2000, cancellationToken) ' Simulate network delay + Return Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md", cancellationToken) + End Using + + End Function + ' +End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs index 8c57406e4c..1948a65e67 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs @@ -73,11 +73,14 @@ await Task.Run(async () => // Simulate work await Task.Delay(200); + // Create local variable to avoid closure issues + int currentProgress = i; + // Update UI safely from background thread await progressBar.InvokeAsync(() => { - progressBar.Value = i; - statusLabel.Text = $"Progress: {i}%"; + progressBar.Value = currentProgress; + statusLabel.Text = $"Progress: {currentProgress}%"; }); } }); diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj index 96e6c47284..5151c0a87e 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj @@ -1,11 +1,11 @@ - + WinExe - net9.0-windows + net10.0-windows + enable true enable - enable - + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs new file mode 100644 index 0000000000..3247f07d90 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs @@ -0,0 +1,38 @@ +namespace AsyncEventHandlers; + +partial class Form1 +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(800, 450); + Text = "Form1"; + } + + #endregion +} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs new file mode 100644 index 0000000000..0434aee030 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs @@ -0,0 +1,9 @@ +namespace AsyncEventHandlers; + +public partial class Form1 : Form +{ + public Form1() + { + InitializeComponent(); + } +} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs new file mode 100644 index 0000000000..c4719c5565 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs @@ -0,0 +1,16 @@ +namespace AsyncEventHandlers; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } +} \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb index 1339f769fb..64591b3851 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb @@ -8,10 +8,11 @@ Namespace AsyncEventHandlersExample Public Partial Class ExampleForm Inherits Form - Private downloadButton As Button - Private processButton As Button - Private complexButton As Button - Private badButton As Button + Private WithEvents downloadButton As Button + Private WithEvents processButton As Button + Private WithEvents complexButton As Button + Private WithEvents badButton As Button + Private WithEvents legacyButton As Button Private resultTextBox As TextBox Private statusLabel As Label Private progressBar As ProgressBar @@ -62,10 +63,13 @@ Namespace AsyncEventHandlersExample ' Simulate work Await Task.Delay(200) + ' Create local variable to avoid closure issues + Dim currentProgress As Integer = i + ' Update UI safely from background thread Await progressBar.InvokeAsync(Sub() - progressBar.Value = i - statusLabel.Text = $"Progress: {i}%" + progressBar.Value = currentProgress + statusLabel.Text = $"Progress: {currentProgress}%" End Sub) Next End Function) @@ -76,18 +80,20 @@ Namespace AsyncEventHandlersExample ' Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click - Await Me.InvokeAsync(Async Function(cancellationToken) - ' This runs on UI thread but doesn't block it + ' For VB.NET, we use a simpler approach since async lambdas with CancellationToken are more complex + Await Me.InvokeAsync(Sub() + ' This runs on UI thread statusLabel.Text = "Starting complex operation..." - - Dim result As String = Await SomeAsyncApiCall() - - ' Update UI directly since we're already on UI thread + End Sub) + + ' Perform the async operation + Dim result As String = Await SomeAsyncApiCall() + + ' Update UI after completion + Await Me.InvokeAsync(Sub() resultTextBox.Text = result statusLabel.Text = "Operation completed" - - Return Nothing - End Function) + End Sub) End Sub Private Async Function SomeAsyncApiCall() As Task(Of String) diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj index 5ced7dea55..870a53ca5f 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj @@ -1,9 +1,17 @@ - + WinExe - net9.0-windows + net10.0-windows + AsyncEventHandlers + Sub Main true - + + + + + + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb new file mode 100644 index 0000000000..0a21f031de --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb @@ -0,0 +1,31 @@ + +Partial Class Form1 + Inherits System.Windows.Forms.Form + + 'Form overrides dispose to clean up the component list. + + Protected Overrides Sub Dispose(disposing As Boolean) + Try + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + Finally + MyBase.Dispose(disposing) + End Try + End Sub + + 'Required by the Windows Form Designer + Private components As System.ComponentModel.IContainer + + 'NOTE: The following procedure is required by the Windows Form Designer + 'It can be modified using the Windows Form Designer. + 'Do not modify it using the code editor. + + Private Sub InitializeComponent() + components = New System.ComponentModel.Container() + AutoScaleMode = AutoScaleMode.Font + ClientSize = New Size(800, 450) + Text = "Form1" + End Sub + +End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb new file mode 100644 index 0000000000..17d659563f --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb @@ -0,0 +1,3 @@ +Public Class Form1 + +End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb new file mode 100644 index 0000000000..236767207e --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb @@ -0,0 +1,11 @@ +Friend Module Program + + + Friend Sub Main(args As String()) + Application.SetHighDpiMode(HighDpiMode.SystemAware) + Application.EnableVisualStyles() + Application.SetCompatibleTextRenderingDefault(False) + Application.Run(New Form1) + End Sub + +End Module From 60a185555e70145da7931cec6aa6b44f7911690a Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 12:45:49 -0700 Subject: [PATCH 03/17] Use net9 --- .../winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj | 2 +- .../winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj index 5151c0a87e..c27cd77f22 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj @@ -2,7 +2,7 @@ WinExe - net10.0-windows + net9.0-windows enable true enable diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj index 870a53ca5f..8f023ab8cb 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj @@ -2,7 +2,7 @@ WinExe - net10.0-windows + net9.0-windows AsyncEventHandlers Sub Main true From d48c4369c7117c5a4b552e09a9cbc1f9f5de4357 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 16:28:47 -0700 Subject: [PATCH 04/17] Reformat and simplify some things. --- .../controls/how-to-make-thread-safe-calls.md | 60 +++++++++++-------- dotnet-desktop-guide/winforms/forms/events.md | 59 ++++++------------ 2 files changed, 53 insertions(+), 66 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index fdccc4f875..9087432a16 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -1,7 +1,7 @@ --- title: How to make thread-safe calls to controls description: Learn how to implement multithreading in your app by calling cross-thread controls in a thread-safe way. If you encounter the 'cross-thread operation not valid' error, use the InvokeRequired property to detect this error. The BackgroundWorker component is also an alternative to creating new threads. -ms.date: 06/20/2021 +ms.date: 08/18/2025 ms.service: dotnet-desktop ai-usage: ai-assisted dev_langs: @@ -36,14 +36,21 @@ The Visual Studio debugger detects these unsafe thread calls by raising an method, which calls a delegate from the main thread to call the control. -2. A component, which offers an event-driven model. +- [Example: Use the Control.Invoke method](#example-use-the-controlinvoke-method): -In both examples, the background thread sleeps for one second to simulate work being done in that thread. + The method, which calls a delegate from the main thread to call the control. -## Example: Use the Invoke method +- [Example: Use a BackgroundWorker](#example-use-a-backgroundworker) + + A component, which offers an event-driven model. + +- [Example: Use Control.InvokeAsync (.NET 9 and later)](#example-use-controlinvokeasync-net-9-and-later) + + The method (.NET 9+), which provides async-friendly marshaling to the UI thread. + +## Example: Use the Control.Invoke method The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. It queries the property, which compares the control's creating thread ID to the calling thread ID. If they're different, you should call the method. @@ -52,6 +59,8 @@ The `WriteTextSafe` enables setting the cont :::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormThread.cs" id="Good"::: :::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormThread.vb" id="Good"::: +For more information on how `Invoke` differs from `InvokeAsync`, see [Understanding the difference: Invoke vs InvokeAsync](#understanding-the-difference-invoke-vs-invokeasync). + ## Example: Use a BackgroundWorker An easy way to implement multithreading is with the component, which uses an event-driven model. The background thread raises the event, which doesn't interact with the main thread. The main thread runs the and event handlers, which can call the main thread's controls. @@ -74,28 +83,28 @@ Starting with .NET 9, Windows Forms includes the (Func)` | Sync operation, with return value | Get control state | -| `InvokeAsync(Func)` | Async operation, no return value | Long-running UI updates | -| `InvokeAsync(Func>)` | Async operation, with return value | Async data fetching with result | +| Overload | Use Case | Example | +|---------------------------------------------------------|------------------------------------|---------------------------------| +| [`InvokeAsync(Action)`][invoke1] | Sync operation, no return value. | Update control properties. | +| [`InvokeAsync(Func)`](xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{``0},System.Threading.CancellationToken)) | Sync operation, with return value. | Get control state. | +| [`InvokeAsync(Func)`](xref:System.Windows.Forms.Control.InvokeAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Threading.CancellationToken)) | Async operation, no return value.* | Long-running UI updates. | +| [`InvokeAsync(Func>)`](xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``0}},System.Threading.CancellationToken)) | Async operation, with return value.* | Async data fetching with result. | The following example demonstrates using `InvokeAsync` to safely update controls from a background thread: @@ -109,11 +118,10 @@ For async operations that need to run on the UI thread, use the async overload: ### Advantages of InvokeAsync -- **Async-friendly**: Returns a `Task` that can be awaited -- **Deadlock prevention**: Eliminates common deadlock scenarios found with synchronous invoke patterns -- **Non-blocking**: Doesn't block the calling thread, improving overall application responsiveness -- **Cancellation support**: Supports `CancellationToken` for operation cancellation -- **Exception propagation**: Properly propagates exceptions back to the calling code -- **Analyzer support**: .NET 9 includes analyzer warnings (WFO2001) to detect potential misuse +`Control.InvokeAsync` has several advantages over the older `Control.Invoke` method. It returns a `Task` that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike `Control.Invoke`, the `InvokeAsync` method doesn't block your background thread, so your app stays more responsive. + +The method supports cancellation through `CancellationToken`, so you can cancel operations when needed. It also handles exceptions properly, passing them back to your code so you can deal with errors appropriately. .NET 9 includes compiler warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) that help you use the method correctly. For comprehensive guidance on async event handlers and best practices, see [Events overview](../forms/events.md#async-event-handlers). + +[invoke1]: xref:System.Windows.Forms.Control.InvokeAsync(System.Action,System.Threading.CancellationToken) diff --git a/dotnet-desktop-guide/winforms/forms/events.md b/dotnet-desktop-guide/winforms/forms/events.md index 1fbaf5b32e..230dde2b1e 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -1,7 +1,7 @@ --- title: "Events Overview" description: "A brief overview about events with .NET Windows Forms." -ms.date: 04/02/2025 +ms.date: 08/18/2025 ms.service: dotnet-desktop ms.topic: overview ai-usage: ai-assisted @@ -73,7 +73,7 @@ Modern applications often need to perform asynchronous operations in response to ### Basic async event handler pattern -Event handlers can be declared with the `async` modifier and use `await` for asynchronous operations. Since event handlers must return `void`, they are one of the rare acceptable uses of `async void`: +Event handlers can be declared with the `async` (`Async` in Visual Basic) modifier and use `await` (`Await` in Visual Basic) for asynchronous operations. Since event handlers must return `void` (or be declared as a `Sub` in Visual Basic), they are one of the rare acceptable uses of `async void` (or `Async Sub` in Visual Basic): :::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_BasicAsyncEventHandler"::: :::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: @@ -91,13 +91,13 @@ The following code demonstrates a common anti-pattern that causes deadlocks: :::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_DeadlockAntiPattern"::: :::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_DeadlockAntiPattern"::: -**Why this causes deadlocks:** +This causes a deadlock for the following reasons: -1. The UI thread calls the async method and blocks waiting for the result -2. The async method captures the UI thread's `SynchronizationContext` -3. When the async operation completes, it tries to continue on the captured UI thread -4. The UI thread is blocked waiting for the operation to complete -5. Deadlock occurs because neither operation can proceed +- The UI thread calls the async method and blocks waiting for the result. +- The async method captures the UI thread's `SynchronizationContext`. +- When the async operation completes, it tries to continue on the captured UI thread. +- The UI thread is blocked waiting for the operation to complete. +- Deadlock occurs because neither operation can proceed. ### Cross-thread operations @@ -105,23 +105,14 @@ When you need to update UI controls from background threads within async operati # [.NET](#tab/dotnet) -.NET 9 introduced , which provides async-friendly marshaling to the UI thread. Unlike `Control.Invoke` which **sends** (blocks the calling thread), `InvokeAsync` **posts** (non-blocking) to the UI thread's message queue. +.NET 9 introduced , which provides async-friendly marshaling to the UI thread. Unlike `Control.Invoke` which **sends** (blocks the calling thread), `Control.InvokeAsync` **posts** (non-blocking) to the UI thread's message queue. For more information about `Control.InvokeAsync`, see [How to make thread-safe calls to controls](../controls/how-to-make-thread-safe-calls.md#). **Key advantages of InvokeAsync:** -- **Non-blocking**: Returns immediately, allowing the calling thread to continue -- **Async-friendly**: Returns a `Task` that can be awaited -- **Exception propagation**: Properly propagates exceptions back to the calling code -- **Cancellation support**: Supports `CancellationToken` for operation cancellation - -**Choosing the right overload:** - -| Scenario | Overload | Example | -|----------|----------|---------| -| Sync operation, no return value | `InvokeAsync(Action)` | `await control.InvokeAsync(() => label.Text = "Done")` | -| Sync operation, with return value | `InvokeAsync(Func)` | `int count = await control.InvokeAsync(() => listBox.Items.Count)` | -| Async operation, no return value | `InvokeAsync(Func)` | `await control.InvokeAsync(async ct => await UpdateUIAsync())` | -| Async operation, with return value | `InvokeAsync(Func>)` | `var result = await control.InvokeAsync(async ct => await ComputeAsync())` | +- **Non-blocking**: Returns immediately, allowing the calling thread to continue. +- **Async-friendly**: Returns a `Task` that can be awaited. +- **Exception propagation**: Properly propagates exceptions back to the calling code. +- **Cancellation support**: Supports `CancellationToken` for operation cancellation. :::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_InvokeAsyncNet9"::: :::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_InvokeAsyncNet9"::: @@ -145,24 +136,12 @@ For applications targeting .NET Framework, use traditional patterns with proper ### Best practices -- **Use async/await consistently**: Don't mix async patterns with blocking calls -- **Handle exceptions**: Always wrap async operations in try-catch blocks in `async void` event handlers -- **Provide user feedback**: Update the UI to show operation progress or status -- **Disable controls during operations**: Prevent users from starting multiple operations -- **Use CancellationToken**: Support operation cancellation for long-running tasks -- **Consider ConfigureAwait(false)**: Use in library code to avoid capturing the UI context when not needed - -### Additional async APIs (.NET 9) - -.NET 9 also introduces experimental async APIs for forms and dialogs: - -- **Form.ShowAsync()** and **Form.ShowDialogAsync()**: Show forms asynchronously -- **TaskDialog.ShowDialogAsync()**: Display task dialogs asynchronously - -> [!NOTE] -> These APIs are experimental and require suppressing compiler warning WFO5002. Add `$(NoWarn);WFO5002` to your project file to use them. - -For more detailed information about thread-safe operations in Windows Forms, see [How to make thread-safe calls to controls](../controls/how-to-make-thread-safe-calls.md). +- **Use async/await consistently**: Don't mix async patterns with blocking calls. +- **Handle exceptions**: Always wrap async operations in try-catch blocks in `async void` event handlers. +- **Provide user feedback**: Update the UI to show operation progress or status. +- **Disable controls during operations**: Prevent users from starting multiple operations. +- **Use CancellationToken**: Support operation cancellation for long-running tasks. +- **Consider ConfigureAwait(false)**: Use in library code to avoid capturing the UI context when not needed. ## Related content From e631f217411908607b1ba8d953f3318dd90baae5 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 17:54:37 -0700 Subject: [PATCH 05/17] fix deadlock example --- .../snippets/events/cs/AsyncEventHandlers.cs | 296 ++++++++++------- .../events/cs/AsyncEventHandlers.resx | 120 +++++++ .../snippets/events/cs/Form1.Designer.cs | 38 --- .../forms/snippets/events/cs/Form1.cs | 9 - .../forms/snippets/events/cs/Program.cs | 6 +- .../snippets/events/vb/AsyncEventHandlers.vb | 304 ++++++++++-------- .../snippets/events/vb/Form1.Designer.vb | 31 -- .../forms/snippets/events/vb/Form1.vb | 3 - .../forms/snippets/events/vb/Program.vb | 2 +- 9 files changed, 477 insertions(+), 332 deletions(-) create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx delete mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs delete mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs delete mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb delete mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs index 1948a65e67..6344a9d4f4 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs @@ -1,150 +1,208 @@ -using System; +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -namespace AsyncEventHandlersExample +namespace AsyncEventHandlers; + +public partial class ExampleForm : Form { - public partial class ExampleForm : Form + private Button downloadButton; + private Button processButton; + private Button complexButton; + private Button badButton; + private TextBox resultTextBox; + private Label statusLabel; + private ProgressBar progressBar; + + public ExampleForm() { - private Button downloadButton; - private Button processButton; - private Button complexButton; - private Button badButton; - private TextBox resultTextBox; - private Label statusLabel; - private ProgressBar progressBar; - - // - private async void downloadButton_Click(object sender, EventArgs e) + Size = new System.Drawing.Size(400, 600); + downloadButton = new Button { Text = "Download Data" }; + processButton = new Button { Text = "Process Data" }; + complexButton = new Button { Text = "Complex Operation" }; + badButton = new Button { Text = "Bad Example (Deadlock)" }; + resultTextBox = new TextBox { Multiline = true, Width = 300, Height = 200 }; + statusLabel = new Label { Text = "Status: Ready" }; + progressBar = new ProgressBar { Width = 300 }; + downloadButton.Click += downloadButton_Click; + processButton.Click += processButton_Click; + complexButton.Click += complexButton_Click; + badButton.Click += badButton_Click; + + // Arrange controls + int margin = 20; + int spacing = 10; + + // Buttons stacked vertically. + downloadButton.Location = new System.Drawing.Point(margin, margin); + downloadButton.Width = 300; + + processButton.Location = new System.Drawing.Point(margin, downloadButton.Bottom + spacing); + processButton.Width = downloadButton.Width; + + complexButton.Location = new System.Drawing.Point(margin, processButton.Bottom + spacing); + complexButton.Width = downloadButton.Width; + + badButton.Location = new System.Drawing.Point(margin, complexButton.Bottom + spacing); + badButton.Width = downloadButton.Width; + + // Status and progress below buttons. + statusLabel.AutoSize = true; + statusLabel.Location = new System.Drawing.Point(margin, badButton.Bottom + spacing); + + progressBar.Location = new System.Drawing.Point(margin, statusLabel.Bottom + spacing); + progressBar.Width = downloadButton.Width; + + // Result text box below progress bar. + resultTextBox.Location = new System.Drawing.Point(margin, progressBar.Bottom + spacing); + + + Controls.Add(downloadButton); + Controls.Add(processButton); + Controls.Add(complexButton); + Controls.Add(badButton); + Controls.Add(resultTextBox); + Controls.Add(statusLabel); + Controls.Add(progressBar); + } + + // + private async void downloadButton_Click(object sender, EventArgs e) + { + downloadButton.Enabled = false; + statusLabel.Text = "Downloading..."; + + try { - downloadButton.Enabled = false; - statusLabel.Text = "Downloading..."; + using var httpClient = new HttpClient(); + string content = await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); - try - { - using var httpClient = new HttpClient(); - string content = await httpClient.GetStringAsync("https://api.example.com/data"); - - // Update UI with the result - resultTextBox.Text = content; - statusLabel.Text = "Download complete"; - } - catch (Exception ex) - { - statusLabel.Text = $"Error: {ex.Message}"; - } - finally - { - downloadButton.Enabled = true; - } + // Update UI with the result + resultTextBox.Text = content; + statusLabel.Text = "Download complete"; } - // - - // - // DON'T DO THIS - causes deadlocks - private void badButton_Click(object sender, EventArgs e) + catch (Exception ex) { - try - { - using var httpClient = new HttpClient(); - // This blocks the UI thread and causes a deadlock - string content = httpClient.GetStringAsync("https://api.example.com/data").GetAwaiter().GetResult(); - resultTextBox.Text = content; - } - catch (Exception ex) - { - MessageBox.Show($"Error: {ex.Message}"); - } + statusLabel.Text = $"Error: {ex.Message}"; } - // - - // - private async void processButton_Click(object sender, EventArgs e) + finally { - processButton.Enabled = false; - - // Start background work - await Task.Run(async () => - { - for (int i = 0; i <= 100; i += 10) - { - // Simulate work - await Task.Delay(200); - - // Create local variable to avoid closure issues - int currentProgress = i; - - // Update UI safely from background thread - await progressBar.InvokeAsync(() => - { - progressBar.Value = currentProgress; - statusLabel.Text = $"Progress: {currentProgress}%"; - }); - } - }); - - processButton.Enabled = true; + downloadButton.Enabled = true; } - // + } + // - // - private async void complexButton_Click(object sender, EventArgs e) + // + // DON'T DO THIS - causes deadlocks + private void badButton_Click(object sender, EventArgs e) + { + try { - await this.InvokeAsync(async (cancellationToken) => - { - // This runs on UI thread but doesn't block it - statusLabel.Text = "Starting complex operation..."; - - var result = await SomeAsyncApiCall(); - - // Update UI directly since we're already on UI thread - resultTextBox.Text = result; - statusLabel.Text = "Operation completed"; - }); + // This blocks the UI thread and causes a deadlock + string content = DownloadPageContentAsync().GetAwaiter().GetResult(); + resultTextBox.Text = content; } - - private async Task SomeAsyncApiCall() + catch (Exception ex) { - using var httpClient = new HttpClient(); - return await httpClient.GetStringAsync("https://api.example.com/data"); + MessageBox.Show($"Error: {ex.Message}"); } - // + } + + private async Task DownloadPageContentAsync() + { + using var httpClient = new HttpClient(); + await Task.Delay(2000); // Simulate delay + return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + } + // - // - private async void legacyButton_Click(object sender, EventArgs e) + // + private async void processButton_Click(object sender, EventArgs e) + { + processButton.Enabled = false; + + // Start background work + await Task.Run(async () => { - var legacyButton = sender as Button; - legacyButton.Enabled = false; - - try + for (int i = 0; i <= 100; i += 10) { - // Move to background thread to avoid blocking UI - await Task.Run(async () => + // Simulate work + await Task.Delay(200); + + // Create local variable to avoid closure issues + int currentProgress = i; + + // Update UI safely from background thread + await progressBar.InvokeAsync(() => { - var result = await SomeAsyncOperation(); - - // Marshal back to UI thread - this.Invoke(new Action(() => - { - resultTextBox.Text = result; - statusLabel.Text = "Complete"; - })); + progressBar.Value = currentProgress; + statusLabel.Text = $"Progress: {currentProgress}%"; }); } - finally + }); + + processButton.Enabled = true; + } + // + + // + private async void complexButton_Click(object sender, EventArgs e) + { + await this.InvokeAsync(async (cancellationToken) => + { + // This runs on UI thread but doesn't block it + statusLabel.Text = "Starting complex operation..."; + + var result = await SomeAsyncApiCall(); + + // Update UI directly since we're already on UI thread + resultTextBox.Text = result; + statusLabel.Text = "Operation completed"; + }); + } + + private async Task SomeAsyncApiCall() + { + using var httpClient = new HttpClient(); + return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + } + // + + // + private async void legacyButton_Click(object sender, EventArgs e) + { + var legacyButton = sender as Button; + legacyButton.Enabled = false; + + try + { + // Move to background thread to avoid blocking UI + await Task.Run(async () => { - // This runs on the UI thread since the await completed - legacyButton.Enabled = true; - } + var result = await SomeAsyncOperation(); + + // Marshal back to UI thread + this.Invoke(new Action(() => + { + resultTextBox.Text = result; + statusLabel.Text = "Complete"; + })); + }); } - - private async Task SomeAsyncOperation() + finally { - using var httpClient = new HttpClient(); - return await httpClient.GetStringAsync("https://api.example.com/data"); + // This runs on the UI thread since the await completed + legacyButton.Enabled = true; } - // } + + private async Task SomeAsyncOperation() + { + using var httpClient = new HttpClient(); + await Task.Delay(2000); // Simulate delay + return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + } + // } diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs deleted file mode 100644 index 3247f07d90..0000000000 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.Designer.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace AsyncEventHandlers; - -partial class Form1 -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - components = new System.ComponentModel.Container(); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(800, 450); - Text = "Form1"; - } - - #endregion -} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs deleted file mode 100644 index 0434aee030..0000000000 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Form1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace AsyncEventHandlers; - -public partial class Form1 : Form -{ - public Form1() - { - InitializeComponent(); - } -} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs index c4719c5565..a8260aefb3 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs @@ -1,4 +1,4 @@ -namespace AsyncEventHandlers; +namespace AsyncEventHandlers; static class Program { @@ -11,6 +11,6 @@ static void Main() // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - Application.Run(new Form1()); + Application.Run(new ExampleForm()); } -} \ No newline at end of file +} diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb index 64591b3851..6180cbf38f 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb @@ -1,135 +1,183 @@ -Imports System +Imports System Imports System.Net.Http Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms -Namespace AsyncEventHandlersExample - Public Partial Class ExampleForm - Inherits Form - - Private WithEvents downloadButton As Button - Private WithEvents processButton As Button - Private WithEvents complexButton As Button - Private WithEvents badButton As Button - Private WithEvents legacyButton As Button - Private resultTextBox As TextBox - Private statusLabel As Label - Private progressBar As ProgressBar - - ' - Private Async Sub downloadButton_Click(sender As Object, e As EventArgs) Handles downloadButton.Click - downloadButton.Enabled = False - statusLabel.Text = "Downloading..." - - Try - Using httpClient As New HttpClient() - Dim content As String = Await httpClient.GetStringAsync("https://api.example.com/data") - - ' Update UI with the result - resultTextBox.Text = content - statusLabel.Text = "Download complete" - End Using - Catch ex As Exception - statusLabel.Text = $"Error: {ex.Message}" - Finally - downloadButton.Enabled = True - End Try - End Sub - ' - - ' - ' DON'T DO THIS - causes deadlocks - Private Sub badButton_Click(sender As Object, e As EventArgs) Handles badButton.Click - Try - Using httpClient As New HttpClient() - ' This blocks the UI thread and causes a deadlock - Dim content As String = httpClient.GetStringAsync("https://api.example.com/data").GetAwaiter().GetResult() - resultTextBox.Text = content - End Using - Catch ex As Exception - MessageBox.Show($"Error: {ex.Message}") - End Try - End Sub - ' - - ' - Private Async Sub processButton_Click(sender As Object, e As EventArgs) Handles processButton.Click - processButton.Enabled = False - - ' Start background work - Await Task.Run(Async Function() - For i As Integer = 0 To 100 Step 10 - ' Simulate work - Await Task.Delay(200) - - ' Create local variable to avoid closure issues - Dim currentProgress As Integer = i - - ' Update UI safely from background thread - Await progressBar.InvokeAsync(Sub() - progressBar.Value = currentProgress - statusLabel.Text = $"Progress: {currentProgress}%" - End Sub) - Next - End Function) - - processButton.Enabled = True - End Sub - ' - - ' - Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click - ' For VB.NET, we use a simpler approach since async lambdas with CancellationToken are more complex - Await Me.InvokeAsync(Sub() - ' This runs on UI thread - statusLabel.Text = "Starting complex operation..." - End Sub) - - ' Perform the async operation - Dim result As String = Await SomeAsyncApiCall() - - ' Update UI after completion - Await Me.InvokeAsync(Sub() - resultTextBox.Text = result - statusLabel.Text = "Operation completed" - End Sub) - End Sub - - Private Async Function SomeAsyncApiCall() As Task(Of String) - Using httpClient As New HttpClient() - Return Await httpClient.GetStringAsync("https://api.example.com/data") - End Using - End Function - ' - - ' - Private Async Sub legacyButton_Click(sender As Object, e As EventArgs) Handles legacyButton.Click - Dim legacyButton As Button = TryCast(sender, Button) - legacyButton.Enabled = False - - Try - ' Move to background thread to avoid blocking UI - Await Task.Run(Async Function() - Dim result As String = Await SomeAsyncOperation() - - ' Marshal back to UI thread - Me.Invoke(New Action(Sub() - resultTextBox.Text = result - statusLabel.Text = "Complete" - End Sub)) - End Function) - Finally - ' This runs on the UI thread since the await completed - legacyButton.Enabled = True - End Try - End Sub - - Private Async Function SomeAsyncOperation() As Task(Of String) +Partial Public Class ExampleForm + Inherits Form + + Private WithEvents downloadButton As Button + Private WithEvents processButton As Button + Private WithEvents complexButton As Button + Private WithEvents badButton As Button + Private WithEvents legacyButton As Button + Private resultTextBox As TextBox + Private statusLabel As Label + Private progressBar As ProgressBar + + Public Sub New() + Size = New System.Drawing.Size(400, 600) + downloadButton = New Button With {.Text = "Download Data"} + processButton = New Button With {.Text = "Process Data"} + complexButton = New Button With {.Text = "Complex Operation"} + badButton = New Button With {.Text = "Bad Example (Deadlock)"} + resultTextBox = New TextBox With {.Multiline = True, .Width = 300, .Height = 200} + statusLabel = New Label With {.Text = "Status: Ready"} + progressBar = New ProgressBar With {.Width = 300} + + 'Arrange controls + Dim Margin = 20 + Dim spacing = 10 + + 'Buttons stacked vertically. + downloadButton.Location = New System.Drawing.Point(Margin, Margin) + downloadButton.Width = 300 + + processButton.Location = New System.Drawing.Point(Margin, downloadButton.Bottom + spacing) + processButton.Width = downloadButton.Width + + complexButton.Location = New System.Drawing.Point(Margin, processButton.Bottom + spacing) + complexButton.Width = downloadButton.Width + + badButton.Location = New System.Drawing.Point(Margin, complexButton.Bottom + spacing) + badButton.Width = downloadButton.Width + + 'Status And progress below buttons. + statusLabel.AutoSize = True + statusLabel.Location = New System.Drawing.Point(Margin, badButton.Bottom + spacing) + + progressBar.Location = New System.Drawing.Point(Margin, statusLabel.Bottom + spacing) + progressBar.Width = downloadButton.Width + + 'Result text box below progress bar. + resultTextBox.Location = New System.Drawing.Point(Margin, progressBar.Bottom + spacing) + + Controls.Add(downloadButton) + Controls.Add(processButton) + Controls.Add(complexButton) + Controls.Add(badButton) + Controls.Add(resultTextBox) + Controls.Add(statusLabel) + Controls.Add(progressBar) + End Sub + + ' + Private Async Sub downloadButton_Click(sender As Object, e As EventArgs) Handles downloadButton.Click + downloadButton.Enabled = False + statusLabel.Text = "Downloading..." + + Try Using httpClient As New HttpClient() - Return Await httpClient.GetStringAsync("https://api.example.com/data") + Dim content As String = Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + + ' Update UI with the result + resultTextBox.Text = content + statusLabel.Text = "Download complete" End Using - End Function - ' - End Class -End Namespace + Catch ex As Exception + statusLabel.Text = $"Error: {ex.Message}" + Finally + downloadButton.Enabled = True + End Try + End Sub + ' + + ' + ' DON'T DO THIS - causes deadlocks + Private Sub badButton_Click(sender As Object, e As EventArgs) Handles badButton.Click + Try + ' This blocks the UI thread and causes a deadlock + Dim content As String = DownloadPageContentAsync().GetAwaiter().GetResult() + resultTextBox.Text = content + Catch ex As Exception + MessageBox.Show($"Error: {ex.Message}") + End Try + End Sub + + Private Async Function DownloadPageContentAsync() As Task(Of String) + Using httpClient As New HttpClient() + Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + End Using + End Function + ' + + ' + Private Async Sub processButton_Click(sender As Object, e As EventArgs) Handles processButton.Click + processButton.Enabled = False + + ' Start background work + Await Task.Run(Async Function() + For i As Integer = 0 To 100 Step 10 + ' Simulate work + Await Task.Delay(200) + + ' Create local variable to avoid closure issues + Dim currentProgress As Integer = i + + ' Update UI safely from background thread + Await progressBar.InvokeAsync(Sub() + progressBar.Value = currentProgress + statusLabel.Text = $"Progress: {currentProgress}%" + End Sub) + Next + End Function) + + processButton.Enabled = True + End Sub + ' + + ' + Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click + ' For VB.NET, we use a simpler approach since async lambdas with CancellationToken are more complex + Await Me.InvokeAsync(Sub() + ' This runs on UI thread + statusLabel.Text = "Starting complex operation..." + End Sub) + + ' Perform the async operation + Dim result As String = Await SomeAsyncApiCall() + + ' Update UI after completion + Await Me.InvokeAsync(Sub() + resultTextBox.Text = result + statusLabel.Text = "Operation completed" + End Sub) + End Sub + + Private Async Function SomeAsyncApiCall() As Task(Of String) + Using httpClient As New HttpClient() + Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + End Using + End Function + ' + + ' + Private Async Sub legacyButton_Click(sender As Object, e As EventArgs) Handles legacyButton.Click + Dim legacyButton As Button = TryCast(sender, Button) + legacyButton.Enabled = False + + Try + ' Move to background thread to avoid blocking UI + Await Task.Run(Async Function() + Dim result As String = Await SomeAsyncOperation() + + ' Marshal back to UI thread + Me.Invoke(New Action(Sub() + resultTextBox.Text = result + statusLabel.Text = "Complete" + End Sub)) + End Function) + Finally + ' This runs on the UI thread since the await completed + legacyButton.Enabled = True + End Try + End Sub + + Private Async Function SomeAsyncOperation() As Task(Of String) + Using httpClient As New HttpClient() + Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + End Using + End Function + ' +End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb deleted file mode 100644 index 0a21f031de..0000000000 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.Designer.vb +++ /dev/null @@ -1,31 +0,0 @@ - -Partial Class Form1 - Inherits System.Windows.Forms.Form - - 'Form overrides dispose to clean up the component list. - - Protected Overrides Sub Dispose(disposing As Boolean) - Try - If disposing AndAlso components IsNot Nothing Then - components.Dispose() - End If - Finally - MyBase.Dispose(disposing) - End Try - End Sub - - 'Required by the Windows Form Designer - Private components As System.ComponentModel.IContainer - - 'NOTE: The following procedure is required by the Windows Form Designer - 'It can be modified using the Windows Form Designer. - 'Do not modify it using the code editor. - - Private Sub InitializeComponent() - components = New System.ComponentModel.Container() - AutoScaleMode = AutoScaleMode.Font - ClientSize = New Size(800, 450) - Text = "Form1" - End Sub - -End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb deleted file mode 100644 index 17d659563f..0000000000 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Form1.vb +++ /dev/null @@ -1,3 +0,0 @@ -Public Class Form1 - -End Class diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb index 236767207e..5240214fd6 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb @@ -5,7 +5,7 @@ Application.SetHighDpiMode(HighDpiMode.SystemAware) Application.EnableVisualStyles() Application.SetCompatibleTextRenderingDefault(False) - Application.Run(New Form1) + Application.Run(New ExampleForm) End Sub End Module From 2420806aa78f3642965c051bb46c5b29c8cadb68 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 17:55:02 -0700 Subject: [PATCH 06/17] Adjust table with link defs --- .../controls/how-to-make-thread-safe-calls.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 9087432a16..b89d0af404 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -99,12 +99,12 @@ Starting with .NET 9, Windows Forms includes the (Func)`](xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{``0},System.Threading.CancellationToken)) | Sync operation, with return value. | Get control state. | -| [`InvokeAsync(Func)`](xref:System.Windows.Forms.Control.InvokeAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Threading.CancellationToken)) | Async operation, no return value.* | Long-running UI updates. | -| [`InvokeAsync(Func>)`](xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``0}},System.Threading.CancellationToken)) | Async operation, with return value.* | Async data fetching with result. | +| Overload | Use Case | Example | +|--------------------------------------------------------------------------------------|--------------------------------------|----------------------------------| +| [`InvokeAsync(Action)`][invoke_action] | Sync operation, no return value. | Update control properties. | +| [`InvokeAsync(Func)`][invoke_func] | Sync operation, with return value. | Get control state. | +| [`InvokeAsync(Func)`][invoke_func_value] | Async operation, no return value.* | Long-running UI updates. | +| [`InvokeAsync(Func>)`][invoke1_func_value_return] | Async operation, with return value.* | Async data fetching with result. | The following example demonstrates using `InvokeAsync` to safely update controls from a background thread: @@ -124,4 +124,7 @@ The method supports cancellation through `CancellationToken`, so you can cancel For comprehensive guidance on async event handlers and best practices, see [Events overview](../forms/events.md#async-event-handlers). -[invoke1]: xref:System.Windows.Forms.Control.InvokeAsync(System.Action,System.Threading.CancellationToken) +[invoke_action]: xref:System.Windows.Forms.Control.InvokeAsync(System.Action,System.Threading.CancellationToken) +[invoke_func]: xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{``0},System.Threading.CancellationToken) +[invoke_func_value]: xref:System.Windows.Forms.Control.InvokeAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Threading.CancellationToken) +[invoke1_func_value_return]: xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``0}},System.Threading.CancellationToken) From 14fb0c925ed318b799dfd7f28c5a60a467aa378e Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Mon, 18 Aug 2025 18:17:30 -0700 Subject: [PATCH 07/17] Add note information about VB --- .../winforms/controls/how-to-make-thread-safe-calls.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index b89d0af404..9dcda4a73b 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -106,6 +106,8 @@ Starting with .NET 9, Windows Forms includes the )`][invoke_func_value] | Async operation, no return value.* | Long-running UI updates. | | [`InvokeAsync(Func>)`][invoke1_func_value_return] | Async operation, with return value.* | Async data fetching with result. | +*Visual Basic doesn't support using awaiting a . + The following example demonstrates using `InvokeAsync` to safely update controls from a background thread: :::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs" id="snippet_InvokeAsyncBasic"::: From b91f3394e09528e578d1058551aef72b0c6d209b Mon Sep 17 00:00:00 2001 From: "Andy (Steve) De George" <67293991+adegeo@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:21:19 -0700 Subject: [PATCH 08/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Klaus Löffelmann <9663150+KlausLoeffelmann@users.noreply.github.com> --- .../winforms/controls/how-to-make-thread-safe-calls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 9dcda4a73b..415463dd84 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -85,8 +85,8 @@ Starting with .NET 9, Windows Forms includes the Date: Tue, 19 Aug 2025 11:21:34 -0700 Subject: [PATCH 09/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Klaus Löffelmann <9663150+KlausLoeffelmann@users.noreply.github.com> --- .../winforms/controls/how-to-make-thread-safe-calls.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 415463dd84..c05f865192 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -91,7 +91,7 @@ Starting with .NET 9, Windows Forms includes the Date: Tue, 19 Aug 2025 16:01:15 -0700 Subject: [PATCH 10/17] Clarify statements. --- .../winforms/controls/how-to-make-thread-safe-calls.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index c05f865192..bbe0b6ffac 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -85,13 +85,13 @@ Starting with .NET 9, Windows Forms includes the Date: Wed, 20 Aug 2025 09:51:38 -0700 Subject: [PATCH 11/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Klaus Löffelmann <9663150+KlausLoeffelmann@users.noreply.github.com> --- .../winforms/controls/how-to-make-thread-safe-calls.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index bbe0b6ffac..c6ae09a8a8 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -120,7 +120,7 @@ For async operations that need to run on the UI thread, use the async overload: ### Advantages of InvokeAsync -`Control.InvokeAsync` has several advantages over the older `Control.Invoke` method. It returns a `Task` that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike `Control.Invoke`, the `InvokeAsync` method doesn't block your background thread, so your app stays more responsive. +`Control.InvokeAsync` has several advantages over the older `Control.Invoke` method. It returns a `Task` that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike `Control.Invoke`, the `InvokeAsync` method doesn't block the calling thread, which keeps your apps responsive and avoids hangs. The method supports cancellation through `CancellationToken`, so you can cancel operations when needed. It also handles exceptions properly, passing them back to your code so you can deal with errors appropriately. .NET 9 includes compiler warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) that help you use the method correctly. From dc93149e5cecb0b00f596d682d261619f967f685 Mon Sep 17 00:00:00 2001 From: "Andy (Steve) De George" <67293991+adegeo@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:32:29 -0700 Subject: [PATCH 12/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Klaus Löffelmann <9663150+KlausLoeffelmann@users.noreply.github.com> --- .../winforms/controls/how-to-make-thread-safe-calls.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index c6ae09a8a8..404f81d161 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -36,7 +36,11 @@ The Visual Studio debugger detects these unsafe thread calls by raising an component, which uses an event-driven model. The background thread raises the event, which doesn't interact with the main thread. The main thread runs the and event handlers, which can call the main thread's controls. +An easy way to implement multi-threading scenarios while guarateeng the access to the control form only the UI-Thread is with the component, which uses an event-driven model. The background thread raises the event, which doesn't interact with the main thread. The main thread runs the and event handlers, which can call the main thread's controls. + +**Note:** Using this component is no longer the recommended way to address asynchronous scenarios in WinForms application, but we keep supporting this approach for backwards compatibility reasons and have no plans to phase out this component. The reason is that the background worker takes _just_ care of offloading processor workload of the UI-Thread to another thread, but does not address other asynchronous scenarios, like writing a long file to an SSD or retrieving a stream of data over the network. In both cases there is a good chance that the processor is not even doing the actual job. Using async methods with `await` makes sure that you are applying the preferred way to invoke a method asynchronously - and that is something that the background worker component just cannot do. If you need to explicitly offload processor workload, simply use `Task.Run` to create and start a new Task, which you then can await like any other asynchronous operation. To make a thread-safe call by using , handle the event. There are two events the background worker uses to report status: and . The `ProgressChanged` event is used to communicate status updates to the main thread, and the `RunWorkerCompleted` event is used to signal that the background worker has completed its work. To start the background thread, call . From 159a1c171ce7e9599a3d3e572a5ab3fb027becd8 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 21 Aug 2025 10:11:55 -0700 Subject: [PATCH 13/17] Lots of code cleanup; Feedback --- .../controls/how-to-make-thread-safe-calls.md | 68 +++++----- .../cs/Form1.Designer.cs | 72 ----------- .../how-to-make-thread-safe-calls/cs/Form1.cs | 47 ------- .../cs/Form1.resx | 60 --------- .../cs/FormBad.Designer.cs | 71 ----------- .../cs/FormBad.cs | 30 ----- .../cs/FormBad.resx | 60 --------- .../cs/FormInvokeAsync.cs | 113 +++++++++++++++++ ...syncExamples.resx => FormInvokeAsync.resx} | 0 .../cs/FormInvokeSync.Designer.cs | 85 +++++++++++++ .../cs/FormInvokeSync.cs | 47 +++++++ .../cs/FormInvokeSync.resx | 120 ++++++++++++++++++ .../cs/FormThread.Designer.cs | 72 ----------- .../cs/FormThread.cs | 49 ------- .../cs/FormThread.resx | 60 --------- .../cs/InvokeAsyncExamples.cs | 108 ---------------- .../cs/Program.cs | 2 +- ...{project.csproj => ThreadSafeCalls.csproj} | 0 .../vb/Extensions.vb | 45 +++++++ .../vb/FormBad.Designer.vb | 61 --------- .../vb/FormBad.resx | 60 --------- .../vb/FormBad.vb | 12 -- ...syncExamples.resx => FormInvokeAsync.resx} | 0 .../vb/FormInvokeAsync.vb | 101 +++++++++++++++ .../vb/FormInvokeSync.Designer.vb | 74 +++++++++++ .../vb/FormInvokeSync.resx | 120 ++++++++++++++++++ .../vb/FormInvokeSync.vb | 35 +++++ .../vb/FormThread.Designer.vb | 61 --------- .../vb/FormThread.resx | 60 --------- .../vb/FormThread.vb | 28 ---- .../vb/InvokeAsyncExamples.vb | 104 --------------- .../vb/Program.vb | 2 +- ...{project.vbproj => ThreadSafeCalls.vbproj} | 2 +- dotnet-desktop-guide/winforms/forms/events.md | 5 +- ...tHandlers.cs => FormAsyncEventHandlers.cs} | 58 +++++---- ...dlers.resx => FormAsyncEventHandlers.resx} | 0 .../forms/snippets/events/cs/Program.cs | 2 +- .../forms/snippets/events/vb/Extensions.vb | 45 +++++++ ...tHandlers.vb => FormAsyncEventHandlers.vb} | 73 +++++++---- .../forms/snippets/events/vb/Program.vb | 2 +- 40 files changed, 913 insertions(+), 1101 deletions(-) delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.resx delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.resx create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.cs rename dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/{InvokeAsyncExamples.resx => FormInvokeAsync.resx} (100%) create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.Designer.cs create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.resx delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.resx delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs rename dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/{project.csproj => ThreadSafeCalls.csproj} (100%) create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.Designer.vb delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.resx delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.vb rename dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/{InvokeAsyncExamples.resx => FormInvokeAsync.resx} (100%) create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.vb create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.Designer.vb create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.resx create mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.Designer.vb delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.resx delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.vb delete mode 100644 dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb rename dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/{project.vbproj => ThreadSafeCalls.vbproj} (78%) rename dotnet-desktop-guide/winforms/forms/snippets/events/cs/{AsyncEventHandlers.cs => FormAsyncEventHandlers.cs} (79%) rename dotnet-desktop-guide/winforms/forms/snippets/events/cs/{AsyncEventHandlers.resx => FormAsyncEventHandlers.resx} (100%) create mode 100644 dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb rename dotnet-desktop-guide/winforms/forms/snippets/events/vb/{AsyncEventHandlers.vb => FormAsyncEventHandlers.vb} (73%) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 404f81d161..46b2431b0b 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -36,11 +36,13 @@ The Visual Studio debugger detects these unsafe thread calls by raising an method (.NET 9+), which provides async-friendly marshaling to the UI thread. - [Example: Use the Control.Invoke method](#example-use-the-controlinvoke-method): @@ -50,34 +52,6 @@ Your application’s most central responsibility is to keep that thread running A component, which offers an event-driven model. -- [Example: Use Control.InvokeAsync (.NET 9 and later)](#example-use-controlinvokeasync-net-9-and-later) - - The method (.NET 9+), which provides async-friendly marshaling to the UI thread. - -## Example: Use the Control.Invoke method - -The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. It queries the property, which compares the control's creating thread ID to the calling thread ID. If they're different, you should call the method. - -The `WriteTextSafe` enables setting the control's property to a new value. The method queries . If returns `true`, `WriteTextSafe` recursively calls itself, passing the method as a delegate to the method. If returns `false`, `WriteTextSafe` sets the directly. The `Button1_Click` event handler creates the new thread and runs the `WriteTextSafe` method. - -:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormThread.cs" id="Good"::: -:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormThread.vb" id="Good"::: - -For more information on how `Invoke` differs from `InvokeAsync`, see [Understanding the difference: Invoke vs InvokeAsync](#understanding-the-difference-invoke-vs-invokeasync). - -## Example: Use a BackgroundWorker - -An easy way to implement multi-threading scenarios while guarateeng the access to the control form only the UI-Thread is with the component, which uses an event-driven model. The background thread raises the event, which doesn't interact with the main thread. The main thread runs the and event handlers, which can call the main thread's controls. - -**Note:** Using this component is no longer the recommended way to address asynchronous scenarios in WinForms application, but we keep supporting this approach for backwards compatibility reasons and have no plans to phase out this component. The reason is that the background worker takes _just_ care of offloading processor workload of the UI-Thread to another thread, but does not address other asynchronous scenarios, like writing a long file to an SSD or retrieving a stream of data over the network. In both cases there is a good chance that the processor is not even doing the actual job. Using async methods with `await` makes sure that you are applying the preferred way to invoke a method asynchronously - and that is something that the background worker component just cannot do. If you need to explicitly offload processor workload, simply use `Task.Run` to create and start a new Task, which you then can await like any other asynchronous operation. - -To make a thread-safe call by using , handle the event. There are two events the background worker uses to report status: and . The `ProgressChanged` event is used to communicate status updates to the main thread, and the `RunWorkerCompleted` event is used to signal that the background worker has completed its work. To start the background thread, call . - -The example counts from 0 to 10 in the `DoWork` event, pausing for one second between counts. It uses the event handler to report the number back to the main thread and set the control's property. For the event to work, the property must be set to `true`. - -:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs" id="Background"::: -:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb" id="Background"::: - ## Example: Use Control.InvokeAsync (.NET 9 and later) Starting with .NET 9, Windows Forms includes the method, which provides async-friendly marshaling to the UI thread. This method is particularly useful for async event handlers and eliminates many common deadlock scenarios. @@ -112,7 +86,7 @@ Starting with .NET 9, Windows Forms includes the )`][invoke_func_value] | Async operation, no return value.* | Long-running UI updates. | | [`InvokeAsync(Func>)`][invoke1_func_value_return] | Async operation, with return value.* | Async data fetching with result. | -*Visual Basic doesn't support using awaiting a . +*Visual Basic doesn't support awaiting a . The following example demonstrates using `InvokeAsync` to safely update controls from a background thread: @@ -124,6 +98,9 @@ For async operations that need to run on the UI thread, use the async overload: :::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs" id="snippet_InvokeAsyncAdvanced"::: :::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb" id="snippet_InvokeAsyncAdvanced"::: +> [!NOTE] +> If you're using Visual Basic, the previous code snippet used an extension method to convert a to a . The extension method code is available on [GitHub](https://github.com/dotnet/docs-desktop/blob/main/dotnet-desktop-guide/winforms/forms/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb). + ### Advantages of InvokeAsync `Control.InvokeAsync` has several advantages over the older `Control.Invoke` method. It returns a `Task` that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike `Control.Invoke`, the `InvokeAsync` method doesn't block the calling thread, which keeps your apps responsive and avoids hangs. @@ -132,6 +109,33 @@ The method supports cancellation through `CancellationToken`, so you can cancel For comprehensive guidance on async event handlers and best practices, see [Events overview](../forms/events.md#async-event-handlers). +## Example: Use the Control.Invoke method + +The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. It queries the property, which compares the control's creating thread ID to the calling thread ID. If they're different, you should call the method. + +The `WriteTextSafe` enables setting the control's property to a new value. The method queries . If returns `true`, `WriteTextSafe` recursively calls itself, passing the method as a delegate to the method. If returns `false`, `WriteTextSafe` sets the directly. The `Button1_Click` event handler creates the new thread and runs the `WriteTextSafe` method. + +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormThread.cs" id="Good"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormThread.vb" id="Good"::: + +For more information on how `Invoke` differs from `InvokeAsync`, see [Understanding the difference: Invoke vs InvokeAsync](#understanding-the-difference-invoke-vs-invokeasync). + +## Example: Use a BackgroundWorker + +An easy way to implement multi-threading scenarios while guaranteeing that the access to a control or form is performed only on the main thread (UI thread), is with the component, which uses an event-driven model. The background thread raises the event, which doesn't interact with the main thread. The main thread runs the and event handlers, which can call the main thread's controls. + +> [!IMPORTANT] +> The `BackgroundWorker` component is no longer the recommended approach for asynchronous scenarios in Windows Forms applications. While we continue supporting this component for backwards compatibility, it only addresses offloading processor workload from the UI thread to another thread. It doesn't handle other asynchronous scenarios like file I/O or network operations where the processor might not be actively working. +> +> For modern asynchronous programming, use `async` methods with `await` instead. If you need to explicitly offload processor-intensive work, use `Task.Run` to create and start a new task, which you can then await like any other asynchronous operation. For more information, see [Example: Use Control.InvokeAsync (.NET 9 and later)](#example-use-controlinvokeasync-net-9-and-later) and [Cross-thread operations and events](../forms/events.md#cross-thread-operations). + +To make a thread-safe call by using , handle the event. There are two events the background worker uses to report status: and . The `ProgressChanged` event is used to communicate status updates to the main thread, and the `RunWorkerCompleted` event is used to signal that the background worker has completed its work. To start the background thread, call . + +The example counts from 0 to 10 in the `DoWork` event, pausing for one second between counts. It uses the event handler to report the number back to the main thread and set the control's property. For the event to work, the property must be set to `true`. + +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs" id="Background"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb" id="Background"::: + [invoke_action]: xref:System.Windows.Forms.Control.InvokeAsync(System.Action,System.Threading.CancellationToken) [invoke_func]: xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{``0},System.Threading.CancellationToken) [invoke_func_value]: xref:System.Windows.Forms.Control.InvokeAsync(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Threading.CancellationToken) diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs deleted file mode 100644 index 78d58eb2ea..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace project; - -partial class Form1 -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(46, 133); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(30, 37); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(333, 23); - this.textBox1.TabIndex = 1; - // - // Form1 - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "Form1"; - this.Text = "Form1"; - this.Load += new System.EventHandler(this.Form1_Load); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; -} - diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs deleted file mode 100644 index 7c6afb53e5..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace project; - -public partial class Form1 : Form -{ - public Form1() - { - InitializeComponent(); - } - - private void Form1_Load(object sender, EventArgs e) - { - - } - - private async void button1_Click(object sender, EventArgs e) - { - //var thread2 = new Thread(new ThreadStart(WriteTextUnsafe)); - //thread2.Start(); - await Task.Factory.StartNew(WriteTextUnsafe, new CancellationToken(false), TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); - await Task.Delay(6000); - Action de = delegate { WriteTextSafe(); }; - textBox1.Invoke(de); - } - - private async void WriteTextUnsafe() - { - textBox1.Text = "This text was set unsafely."; - await Task.Delay(4000); - textBox1.Text = "This text was set unsafely twice!"; - } - - private void WriteTextSafe() - { - textBox1.Text = "This text was set safely."; - } -} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.resx deleted file mode 100644 index f298a7be80..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.resx +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs deleted file mode 100644 index 502e74e9e2..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs +++ /dev/null @@ -1,71 +0,0 @@ - -namespace project; - -partial class FormBad -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(57, 165); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(57, 136); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(217, 23); - this.textBox1.TabIndex = 1; - // - // FormBad - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "FormBad"; - this.Text = "FormBad"; - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; -} \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs deleted file mode 100644 index 123869824f..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace project; - -public partial class FormBad : Form -{ - public FormBad() - { - InitializeComponent(); - } - - // - private void button1_Click(object sender, EventArgs e) - { - System.Threading.Thread thread2 = new(WriteTextUnsafe); - thread2.Start(); - } - - private void WriteTextUnsafe() => - textBox1.Text = "This text was set unsafely."; - // -} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.resx deleted file mode 100644 index f298a7be80..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.resx +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.cs new file mode 100644 index 0000000000..986e6330cc --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.cs @@ -0,0 +1,113 @@ +using System; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace project; + +public partial class FormInvokeAsync : Form +{ + private Button button1; + private Button button2; + private TextBox loggingTextBox; + + public FormInvokeAsync() + { + button1 = new Button { Text = "Invoke Async Basic", Location = new System.Drawing.Point(10, 10), Width = 200 }; + button2 = new Button { Text = "Invoke Async Advanced", Location = new System.Drawing.Point(10, 50), Width = 200 }; + loggingTextBox = new TextBox { Location = new System.Drawing.Point(10, 90), Width = 300, Multiline = true, Height =500 }; + button1.Click += button1_Click; + button2.Click += button2_Click; + Controls.Add(button1); + Controls.Add(button2); + Controls.Add(loggingTextBox); + } + + // + private async void button1_Click(object sender, EventArgs e) + { + button1.Enabled = false; + + try + { + // Perform background work + await Task.Run(async () => + { + for (int i = 0; i <= 100; i += 10) + { + // Simulate work + await Task.Delay(100); + + // Create local variable to avoid closure issues + int currentProgress = i; + + // Update UI safely from background thread + await loggingTextBox.InvokeAsync(() => + { + loggingTextBox.Text = $"Progress: {currentProgress}%"; + }); + } + }); + + loggingTextBox.Text = "Operation completed!"; + } + finally + { + button1.Enabled = true; + } + } + // + + // + private async void button2_Click(object sender, EventArgs e) + { + button2.Enabled = false; + try + { + loggingTextBox.Text = "Starting operation..."; + + // Dispatch and run on a new thread, but wait for tasks to finish + // Exceptions are rethrown here, because await is used + await Task.WhenAll(Task.Run(SomeApiCallAsync), + Task.Run(SomeApiCallAsync), + Task.Run(SomeApiCallAsync)); + + // Dispatch and run on a new thread, but don't wait for task to finish + // Exceptions are not rethrown here, because await is not used + _ = Task.Run(SomeApiCallAsync); + } + catch (OperationCanceledException) + { + loggingTextBox.Text += "Operation canceled."; + } + catch (Exception ex) + { + loggingTextBox.Text += $"Error: {ex.Message}"; + } + finally + { + button2.Enabled = true; + } + } + + private async Task SomeApiCallAsync() + { + using var client = new HttpClient(); + + // Simulate random network delay + await Task.Delay(Random.Shared.Next(500, 2500)); + + // Do I/O asynchronously + string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + + // Marshal back to UI thread + await this.InvokeAsync(async (cancelToken) => + { + loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"; + }); + + // Do more async I/O ... + } + // +} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.resx similarity index 100% rename from dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.resx rename to dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.resx diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.Designer.cs new file mode 100644 index 0000000000..7e6ae647f3 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.Designer.cs @@ -0,0 +1,85 @@ + +namespace project; + +partial class FormInvokeSync +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + button1 = new System.Windows.Forms.Button(); + textBox1 = new System.Windows.Forms.TextBox(); + button2 = new System.Windows.Forms.Button(); + SuspendLayout(); + // + // button1 + // + button1.Location = new System.Drawing.Point(21, 196); + button1.Name = "button1"; + button1.Size = new System.Drawing.Size(75, 23); + button1.TabIndex = 0; + button1.Text = "Good"; + button1.UseVisualStyleBackColor = true; + button1.Click += button1_Click; + // + // textBox1 + // + textBox1.Location = new System.Drawing.Point(21, 55); + textBox1.Multiline = true; + textBox1.Name = "textBox1"; + textBox1.Size = new System.Drawing.Size(267, 94); + textBox1.TabIndex = 1; + // + // button2 + // + button2.Location = new System.Drawing.Point(21, 225); + button2.Name = "button2"; + button2.Size = new System.Drawing.Size(75, 23); + button2.TabIndex = 2; + button2.Text = "Bad"; + button2.UseVisualStyleBackColor = true; + button2.Click += button2_Click; + // + // FormInvokeSync + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(800, 450); + Controls.Add(button2); + Controls.Add(textBox1); + Controls.Add(button1); + Name = "FormInvokeSync"; + Text = "FormThread"; + ResumeLayout(false); + PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button button1; + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.Button button2; +} \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs new file mode 100644 index 0000000000..7493d12e2c --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace project; + +public partial class FormInvokeSync : Form +{ + public FormInvokeSync() + { + InitializeComponent(); + } + + // + private void button1_Click(object sender, EventArgs e) + { + WriteTextSafe("Writing message #1"); + _ = Task.Run(() => WriteTextSafe("Writing message #2")); + } + + public void WriteTextSafe(string text) + { + if (textBox1.InvokeRequired) + textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)")); + + else + textBox1.Text += $"{Environment.NewLine}{text}"; + } + // + + // + private void button2_Click(object sender, EventArgs e) + { + WriteTextUnsafe("Writing message #1 (UI THREAD)"); + _ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)")); + } + + private void WriteTextUnsafe(string text) => + textBox1.Text += $"{Environment.NewLine}{text}"; + // +} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.resx new file mode 100644 index 0000000000..8b2ff64a11 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs deleted file mode 100644 index 29ac43a8bf..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ - -namespace project; - -partial class FormThread -{ - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.button1 = new System.Windows.Forms.Button(); - this.textBox1 = new System.Windows.Forms.TextBox(); - this.SuspendLayout(); - // - // button1 - // - this.button1.Location = new System.Drawing.Point(21, 84); - this.button1.Name = "button1"; - this.button1.Size = new System.Drawing.Size(75, 23); - this.button1.TabIndex = 0; - this.button1.Text = "button1"; - this.button1.UseVisualStyleBackColor = true; - this.button1.Click += new System.EventHandler(this.button1_Click); - // - // textBox1 - // - this.textBox1.Location = new System.Drawing.Point(21, 55); - this.textBox1.Name = "textBox1"; - this.textBox1.Size = new System.Drawing.Size(267, 23); - this.textBox1.TabIndex = 1; - this.textBox1.Enter += new System.EventHandler(this.textBox1_Enter); - // - // FormThread - // - this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); - this.Controls.Add(this.textBox1); - this.Controls.Add(this.button1); - this.Name = "FormThread"; - this.Text = "FormThread"; - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.Button button1; - private System.Windows.Forms.TextBox textBox1; -} \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs deleted file mode 100644 index d1ff1d0f4b..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace project; - -public partial class FormThread : Form -{ - public FormThread() - { - InitializeComponent(); - } - - // - private void button1_Click(object sender, EventArgs e) - { - var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); }); - var thread2 = new System.Threading.Thread(threadParameters); - thread2.Start(); - } - - public void WriteTextSafe(string text) - { - if (textBox1.InvokeRequired) - { - // Call this same method but append THREAD2 to the text - Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); }; - textBox1.Invoke(safeWrite); - } - else - textBox1.Text = text; - } - // - - private void textBox1_Enter(object sender, System.EventArgs e) - { - //if (!String.IsNullOrEmpty(textBox1.Text)) - //{ - // textBox1.SelectionStart = 0; - // textBox1.SelectionLength = textBox1.Text.Length; - //} - } -} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.resx deleted file mode 100644 index f298a7be80..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.resx +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs deleted file mode 100644 index a3c68f4dd9..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/InvokeAsyncExamples.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; // Added for CancellationToken -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace project; - -public partial class InvokeAsyncForm : Form -{ - private Button button1; - private Button button2; - private TextBox textBox1; - - public InvokeAsyncForm() - { - button1 = new Button { Text = "Invoke Async Basic", Location = new System.Drawing.Point(10, 10), Width = 200 }; - button2 = new Button { Text = "Invoke Async Advanced", Location = new System.Drawing.Point(10, 50), Width = 200 }; - textBox1 = new TextBox { Location = new System.Drawing.Point(10, 90), Width = 200, Multiline = true, Height =500 }; - button1.Click += button1_Click; - button2.Click += button2_Click; - Controls.Add(button1); - Controls.Add(button2); - Controls.Add(textBox1); - } - - // - private async void button1_Click(object sender, EventArgs e) - { - button1.Enabled = false; - - try - { - // Perform background work - await Task.Run(async () => - { - for (int i = 0; i <= 100; i += 10) - { - // Simulate work - await Task.Delay(100); - - // Create local variable to avoid closure issues - int currentProgress = i; - - // Update UI safely from background thread - await textBox1.InvokeAsync(() => - { - textBox1.Text = $"Progress: {currentProgress}%"; - }); - } - }); - - // Update UI after completion - await textBox1.InvokeAsync(() => - { - textBox1.Text = "Operation completed!"; - }); - } - finally - { - button1.Enabled = true; - } - } - // - - // - private async void button2_Click(object sender, EventArgs e) - { - button2.Enabled = false; - try - { - await this.InvokeAsync(async (cancellationToken) => - { - // This runs on UI thread but doesn't block it - textBox1.Text = "Starting operation..."; - - try - { - // Perform async work on UI thread - var result = await SomeAsyncApiCall(cancellationToken); - - // Update UI directly since we're on UI thread - textBox1.Text = $"Result: {result}"; - } - catch (OperationCanceledException) - { - textBox1.Text = "Operation canceled."; - } - catch (Exception ex) - { - textBox1.Text = $"Error: {ex.Message}"; - } - }); - } - finally - { - button2.Enabled = true; - } - } - - private async Task SomeAsyncApiCall(CancellationToken cancellationToken) - { - using var client = new HttpClient(); - await Task.Delay(2000, cancellationToken); // Simulate network delay - return await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md", cancellationToken); - } - // -} diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs index 4b4c699ac6..2fad38673e 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs @@ -17,6 +17,6 @@ static void Main() Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Form1()); + Application.Run(new FormInvokeSync()); } } diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/project.csproj b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/ThreadSafeCalls.csproj similarity index 100% rename from dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/project.csproj rename to dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/ThreadSafeCalls.csproj diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb new file mode 100644 index 0000000000..2df118c7f1 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb @@ -0,0 +1,45 @@ +Imports System.Runtime.CompilerServices +Imports System.Threading + +Module ValueTaskExtensions + + ''' + ''' Converts a Func(Of Task) into a Func(Of ValueTask). + ''' + + Public Function AsValueTask(work As Func(Of Task)) As Func(Of ValueTask) + Return Function() + Return New ValueTask(work()) + End Function + End Function + + ''' + ''' Converts a Func(Of Task(Of T)) into a Func(Of ValueTask(Of T)). + ''' + + Public Function AsValueTask(Of T)(work As Func(Of Task(Of T))) As Func(Of ValueTask(Of T)) + Return Function() + Return New ValueTask(Of T)(work()) + End Function + End Function + + ''' + ''' Converts a Func(Of CancellationToken, Task) into a Func(Of CancellationToken, ValueTask). + ''' + + Public Function AsValueTask(work As Func(Of CancellationToken, Task)) As Func(Of CancellationToken, ValueTask) + Return Function(ct As CancellationToken) + Return New ValueTask(work(ct)) + End Function + End Function + + ''' + ''' Converts a Func(Of CancellationToken, Task(Of T)) into a Func(Of CancellationToken, ValueTask(Of T)). + ''' + + Public Function AsValueTask(Of T)(work As Func(Of CancellationToken, Task(Of T))) As Func(Of CancellationToken, ValueTask(Of T)) + Return Function(ct As CancellationToken) + Return New ValueTask(Of T)(work(ct)) + End Function + End Function +End Module diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.Designer.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.Designer.vb deleted file mode 100644 index 773b239df8..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.Designer.vb +++ /dev/null @@ -1,61 +0,0 @@ - _ -Partial Class FormBad - Inherits System.Windows.Forms.Form - - 'Form overrides dispose to clean up the component list. - _ - Protected Overrides Sub Dispose(ByVal disposing As Boolean) - Try - If disposing AndAlso components IsNot Nothing Then - components.Dispose() - End If - Finally - MyBase.Dispose(disposing) - End Try - End Sub - - 'Required by the Windows Form Designer - Private components As System.ComponentModel.IContainer - - 'NOTE: The following procedure is required by the Windows Form Designer - 'It can be modified using the Windows Form Designer. - 'Do not modify it using the code editor. - _ - Private Sub InitializeComponent() - Me.TextBox1 = New System.Windows.Forms.TextBox() - Me.Button1 = New System.Windows.Forms.Button() - Me.SuspendLayout() - ' - 'TextBox1 - ' - Me.TextBox1.Location = New System.Drawing.Point(25, 43) - Me.TextBox1.Name = "TextBox1" - Me.TextBox1.Size = New System.Drawing.Size(249, 23) - Me.TextBox1.TabIndex = 0 - ' - 'Button1 - ' - Me.Button1.Location = New System.Drawing.Point(25, 72) - Me.Button1.Name = "Button1" - Me.Button1.Size = New System.Drawing.Size(75, 23) - Me.Button1.TabIndex = 1 - Me.Button1.Text = "Button1" - Me.Button1.UseVisualStyleBackColor = True - ' - 'FormBad - ' - Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!) - Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font - Me.ClientSize = New System.Drawing.Size(800, 450) - Me.Controls.Add(Me.Button1) - Me.Controls.Add(Me.TextBox1) - Me.Name = "FormBad" - Me.Text = "FormBad" - Me.ResumeLayout(False) - Me.PerformLayout() - - End Sub - - Friend WithEvents TextBox1 As System.Windows.Forms.TextBox - Friend WithEvents Button1 As System.Windows.Forms.Button -End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.resx deleted file mode 100644 index f298a7be80..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.resx +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.vb deleted file mode 100644 index 74bd714c99..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBad.vb +++ /dev/null @@ -1,12 +0,0 @@ -Public Class FormBad - ' - Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click - Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe) - thread2.Start() - End Sub - - Private Sub WriteTextUnsafe() - TextBox1.Text = "This text was set unsafely." - End Sub - ' -End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.resx similarity index 100% rename from dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.resx rename to dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.resx diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.vb new file mode 100644 index 0000000000..5c02b3d10f --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.vb @@ -0,0 +1,101 @@ +Imports System +Imports System.Net.Http +Imports System.Threading +Imports System.Threading.Tasks +Imports System.Windows.Forms + +Partial Public Class FormInvokeAsync + Inherits Form + + Private WithEvents button1 As Button + Private WithEvents button2 As Button + Private loggingTextBox As TextBox + + Public Sub New() + button1 = New Button With {.Text = "Invoke Async Basic", .Location = New System.Drawing.Point(10, 10), .Width = 200} + button2 = New Button With {.Text = "Invoke Async Advanced", .Location = New System.Drawing.Point(10, 50), .Width = 200} + loggingTextBox = New TextBox With {.Location = New System.Drawing.Point(10, 90), .Width = 200, .Multiline = True, .Height = 500} + Controls.Add(button1) + Controls.Add(button2) + Controls.Add(loggingTextBox) + End Sub + + ' + Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click + button1.Enabled = False + + Try + ' Perform background work + Await Task.Run(Async Function() + For i As Integer = 0 To 100 Step 10 + ' Simulate work + Await Task.Delay(100) + + ' Create local variable to avoid closure issues + Dim currentProgress As Integer = i + + ' Update UI safely from background thread + Await loggingTextBox.InvokeAsync(Sub() + loggingTextBox.Text = $"Progress: {currentProgress}%" + End Sub) + Next + End Function) + + ' Update UI after completion + Await loggingTextBox.InvokeAsync(Sub() + loggingTextBox.Text = "Operation completed!" + End Sub) + Finally + button1.Enabled = True + End Try + End Sub + ' + + ' + Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click + button2.Enabled = False + Try + loggingTextBox.Text = "Starting operation..." + + ' Dispatch and run on a new thread, but wait for tasks to finish + ' Exceptions are rethrown here, because await is used + Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync), + Task.Run(AddressOf SomeApiCallAsync), + Task.Run(AddressOf SomeApiCallAsync)) + + ' Dispatch and run on a new thread, but don't wait for task to finish + ' Exceptions are not rethrown here, because await is not used + Call Task.Run(AddressOf SomeApiCallAsync) + + Catch ex As OperationCanceledException + loggingTextBox.Text += "Operation canceled." + Catch ex As Exception + loggingTextBox.Text += $"Error: {ex.Message}" + Finally + button2.Enabled = True + End Try + End Sub + + Private Async Function SomeApiCallAsync() As Task + Using client As New HttpClient() + + ' Simulate random network delay + Await Task.Delay(Random.Shared.Next(500, 2500)) + + ' Do I/O asynchronously + Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + + ' Marshal back to UI thread + ' Extra work here in VB to handle ValueTask conversion + Await Me.InvokeAsync(DirectCast( + Async Function(cancelToken As CancellationToken) As Task + loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}" + End Function, + Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task + ) + + ' Do more Async I/O ... + End Using + End Function + ' +End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.Designer.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.Designer.vb new file mode 100644 index 0000000000..b97edd5f01 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.Designer.vb @@ -0,0 +1,74 @@ + _ +Partial Class FormInvokeSync + Inherits System.Windows.Forms.Form + + 'Form overrides dispose to clean up the component list. + _ + Protected Overrides Sub Dispose(ByVal disposing As Boolean) + Try + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + Finally + MyBase.Dispose(disposing) + End Try + End Sub + + 'Required by the Windows Form Designer + Private components As System.ComponentModel.IContainer + + 'NOTE: The following procedure is required by the Windows Form Designer + 'It can be modified using the Windows Form Designer. + 'Do not modify it using the code editor. + _ + Private Sub InitializeComponent() + Button1 = New System.Windows.Forms.Button() + TextBox1 = New System.Windows.Forms.TextBox() + Button2 = New System.Windows.Forms.Button() + SuspendLayout() + ' + ' Button1 + ' + Button1.Location = New System.Drawing.Point(27, 176) + Button1.Name = "Button1" + Button1.Size = New System.Drawing.Size(75, 23) + Button1.TabIndex = 0 + Button1.Text = "Good" + Button1.UseVisualStyleBackColor = True + ' + ' TextBox1 + ' + TextBox1.Location = New System.Drawing.Point(27, 53) + TextBox1.Multiline = True + TextBox1.Name = "TextBox1" + TextBox1.Size = New System.Drawing.Size(235, 90) + TextBox1.TabIndex = 1 + ' + ' Button2 + ' + Button2.Location = New System.Drawing.Point(27, 205) + Button2.Name = "Button2" + Button2.Size = New System.Drawing.Size(75, 23) + Button2.TabIndex = 2 + Button2.Text = "Bad" + Button2.UseVisualStyleBackColor = True + ' + ' FormInvokeSync + ' + AutoScaleDimensions = New System.Drawing.SizeF(7F, 15F) + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + ClientSize = New System.Drawing.Size(800, 450) + Controls.Add(Button2) + Controls.Add(TextBox1) + Controls.Add(Button1) + Name = "FormInvokeSync" + Text = "FormThread" + ResumeLayout(False) + PerformLayout() + + End Sub + + Friend WithEvents Button1 As System.Windows.Forms.Button + Friend WithEvents TextBox1 As System.Windows.Forms.TextBox + Friend WithEvents Button2 As System.Windows.Forms.Button +End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.resx new file mode 100644 index 0000000000..8b2ff64a11 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb new file mode 100644 index 0000000000..ed5f109207 --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb @@ -0,0 +1,35 @@ +Public Class FormInvokeSync + ' + Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click + + WriteTextSafe("Writing message #1") + Task.Run(Sub() WriteTextSafe("Writing message #2")) + + End Sub + + Private Sub WriteTextSafe(text As String) + + If (TextBox1.InvokeRequired) Then + + TextBox1.Invoke(Sub() + WriteTextSafe($"{text} (NON-UI THREAD)") + End Sub) + + Else + TextBox1.Text += $"{Environment.NewLine}{text}" + End If + + End Sub + ' + + ' + Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click + WriteTextUnsafe("Writing message #1 (UI THREAD)") + Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)")) + End Sub + + Private Sub WriteTextUnsafe(text As String) + TextBox1.Text += $"{Environment.NewLine}{text}" + End Sub + ' +End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.Designer.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.Designer.vb deleted file mode 100644 index 49a9ef4988..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.Designer.vb +++ /dev/null @@ -1,61 +0,0 @@ - _ -Partial Class FormThread - Inherits System.Windows.Forms.Form - - 'Form overrides dispose to clean up the component list. - _ - Protected Overrides Sub Dispose(ByVal disposing As Boolean) - Try - If disposing AndAlso components IsNot Nothing Then - components.Dispose() - End If - Finally - MyBase.Dispose(disposing) - End Try - End Sub - - 'Required by the Windows Form Designer - Private components As System.ComponentModel.IContainer - - 'NOTE: The following procedure is required by the Windows Form Designer - 'It can be modified using the Windows Form Designer. - 'Do not modify it using the code editor. - _ - Private Sub InitializeComponent() - Me.Button1 = New System.Windows.Forms.Button() - Me.TextBox1 = New System.Windows.Forms.TextBox() - Me.SuspendLayout() - ' - 'Button1 - ' - Me.Button1.Location = New System.Drawing.Point(27, 82) - Me.Button1.Name = "Button1" - Me.Button1.Size = New System.Drawing.Size(75, 23) - Me.Button1.TabIndex = 0 - Me.Button1.Text = "Button1" - Me.Button1.UseVisualStyleBackColor = True - ' - 'TextBox1 - ' - Me.TextBox1.Location = New System.Drawing.Point(27, 53) - Me.TextBox1.Name = "TextBox1" - Me.TextBox1.Size = New System.Drawing.Size(235, 23) - Me.TextBox1.TabIndex = 1 - ' - 'FormThread - ' - Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!) - Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font - Me.ClientSize = New System.Drawing.Size(800, 450) - Me.Controls.Add(Me.TextBox1) - Me.Controls.Add(Me.Button1) - Me.Name = "FormThread" - Me.Text = "FormThread" - Me.ResumeLayout(False) - Me.PerformLayout() - - End Sub - - Friend WithEvents Button1 As System.Windows.Forms.Button - Friend WithEvents TextBox1 As System.Windows.Forms.TextBox -End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.resx deleted file mode 100644 index f298a7be80..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.resx +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.vb deleted file mode 100644 index f4588e8a83..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormThread.vb +++ /dev/null @@ -1,28 +0,0 @@ -Public Class FormThread - ' - Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click - - Dim threadParameters As New System.Threading.ThreadStart(Sub() - WriteTextSafe("This text was set safely.") - End Sub) - - Dim thread2 As New System.Threading.Thread(threadParameters) - thread2.Start() - - End Sub - - Private Sub WriteTextSafe(text As String) - - If (TextBox1.InvokeRequired) Then - - TextBox1.Invoke(Sub() - WriteTextSafe($"{text} (THREAD2)") - End Sub) - - Else - TextBox1.Text = text - End If - - End Sub - ' -End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb deleted file mode 100644 index 2789fee876..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/InvokeAsyncExamples.vb +++ /dev/null @@ -1,104 +0,0 @@ -Imports System -Imports System.Net.Http -Imports System.Threading -Imports System.Threading.Tasks -Imports System.Windows.Forms - -Partial Public Class InvokeAsyncForm - Inherits Form - - Private WithEvents button1 As Button - Private WithEvents button2 As Button - Private textBox1 As TextBox - - Public Sub New() - button1 = New Button With {.Text = "Invoke Async Basic", .Location = New System.Drawing.Point(10, 10), .Width = 200} - button2 = New Button With {.Text = "Invoke Async Advanced", .Location = New System.Drawing.Point(10, 50), .Width = 200} - textBox1 = New TextBox With {.Location = New System.Drawing.Point(10, 90), .Width = 200, .Multiline = True, .Height = 500} - Controls.Add(button1) - Controls.Add(button2) - Controls.Add(textBox1) - End Sub - - ' - Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click - button1.Enabled = False - - Try - ' Perform background work - Await Task.Run(Async Function() - For i As Integer = 0 To 100 Step 10 - ' Simulate work - Await Task.Delay(100) - - ' Create local variable to avoid closure issues - Dim currentProgress As Integer = i - - ' Update UI safely from background thread - Await textBox1.InvokeAsync(Sub() - textBox1.Text = $"Progress: {currentProgress}%" - End Sub) - Next - End Function) - - ' Update UI after completion - Await textBox1.InvokeAsync(Sub() - textBox1.Text = "Operation completed!" - End Sub) - Finally - button1.Enabled = True - End Try - End Sub - ' - - ' - Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click - button2.Enabled = False - Try - ' Create a cancellation token source for this operation - Using cts As New CancellationTokenSource() - ' For VB.NET, use a separate method to handle the async operation with cancellation - Await AsyncOperationWithCancellation(cts.Token) - End Using - Finally - button2.Enabled = True - End Try - End Sub - - Private Async Function AsyncOperationWithCancellation(cancellationToken As CancellationToken) As Task - ' Update UI to show starting state - Await Me.InvokeAsync(Sub() - textBox1.Text = "Starting operation..." - End Sub, cancellationToken) - - Dim resultMessage As String = "" - - Try - ' Perform async work with cancellation support - Dim result As String = Await SomeAsyncApiCall(cancellationToken) - resultMessage = $"Result: {result}" - - Catch ex As OperationCanceledException - resultMessage = "Operation canceled." - - Catch ex As Exception - resultMessage = $"Error: {ex.Message}" - - End Try - - ' Update UI with final result - Await Me.InvokeAsync(Sub() - textBox1.Text = resultMessage - End Sub, cancellationToken) - End Function - - Private Async Function SomeAsyncApiCall(cancellationToken As CancellationToken) As Task(Of String) - - Using client As New HttpClient() - Await Task.Delay(2000, cancellationToken) ' Simulate network delay - Return Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md", cancellationToken) - End Using - - End Function - ' -End Class diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Program.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Program.vb index 5db4a1f468..223359c1e1 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Program.vb +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/Program.vb @@ -9,7 +9,7 @@ Module Program Application.SetHighDpiMode(HighDpiMode.SystemAware) Application.EnableVisualStyles() Application.SetCompatibleTextRenderingDefault(False) - Application.Run(New FormBackgroundWorker()) + Application.Run(New FormInvokeSync()) End Sub End Module diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/project.vbproj b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/ThreadSafeCalls.vbproj similarity index 78% rename from dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/project.vbproj rename to dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/ThreadSafeCalls.vbproj index 391311ae02..33f81f736d 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/project.vbproj +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/ThreadSafeCalls.vbproj @@ -3,6 +3,6 @@ WinExe net9.0-windows true - project.Program + ThreadSafeCalls.Program \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/events.md b/dotnet-desktop-guide/winforms/forms/events.md index 230dde2b1e..ef8c8d13af 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -79,7 +79,7 @@ Event handlers can be declared with the `async` (`Async` in Visual Basic) modifi :::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: > [!IMPORTANT] -> While `async void` is generally discouraged, it's necessary for event handlers since they cannot return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the example above. +> While `async void` is generally discouraged, it's necessary for event handlers (and event handler-like code, such as `Control.OnClick`) since they cannot return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the previous example. ### Common pitfalls and deadlocks @@ -125,6 +125,9 @@ For truly async operations that need to run on the UI thread: > [!TIP] > .NET 9 includes analyzer warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) to help detect when async methods are incorrectly passed to synchronous overloads of `InvokeAsync`. This helps prevent "fire-and-forget" behavior. +> [!NOTE] +> If you're using Visual Basic, the previous code snippet used an extension method to convert a to a . The extension method code is available on [GitHub](https://github.com/dotnet/docs-desktop/blob/main/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb). + # [.NET Framework](#tab/dotnetframework) For applications targeting .NET Framework, use traditional patterns with proper async handling: diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs similarity index 79% rename from dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs rename to dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs index 6344a9d4f4..35654731fd 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.cs +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs @@ -6,24 +6,24 @@ namespace AsyncEventHandlers; -public partial class ExampleForm : Form +public partial class FormAsyncEventHandlers : Form { private Button downloadButton; private Button processButton; private Button complexButton; private Button badButton; - private TextBox resultTextBox; + private TextBox loggingTextBox; private Label statusLabel; private ProgressBar progressBar; - public ExampleForm() + public FormAsyncEventHandlers() { Size = new System.Drawing.Size(400, 600); downloadButton = new Button { Text = "Download Data" }; processButton = new Button { Text = "Process Data" }; complexButton = new Button { Text = "Complex Operation" }; badButton = new Button { Text = "Bad Example (Deadlock)" }; - resultTextBox = new TextBox { Multiline = true, Width = 300, Height = 200 }; + loggingTextBox = new TextBox { Multiline = true, Width = 400, Height = 200 }; statusLabel = new Label { Text = "Status: Ready" }; progressBar = new ProgressBar { Width = 300 }; downloadButton.Click += downloadButton_Click; @@ -56,14 +56,14 @@ public ExampleForm() progressBar.Width = downloadButton.Width; // Result text box below progress bar. - resultTextBox.Location = new System.Drawing.Point(margin, progressBar.Bottom + spacing); + loggingTextBox.Location = new System.Drawing.Point(margin, progressBar.Bottom + spacing); Controls.Add(downloadButton); Controls.Add(processButton); Controls.Add(complexButton); Controls.Add(badButton); - Controls.Add(resultTextBox); + Controls.Add(loggingTextBox); Controls.Add(statusLabel); Controls.Add(progressBar); } @@ -80,7 +80,7 @@ private async void downloadButton_Click(object sender, EventArgs e) string content = await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); // Update UI with the result - resultTextBox.Text = content; + loggingTextBox.Text = content; statusLabel.Text = "Download complete"; } catch (Exception ex) @@ -102,7 +102,7 @@ private void badButton_Click(object sender, EventArgs e) { // This blocks the UI thread and causes a deadlock string content = DownloadPageContentAsync().GetAwaiter().GetResult(); - resultTextBox.Text = content; + loggingTextBox.Text = content; } catch (Exception ex) { @@ -150,23 +150,35 @@ await progressBar.InvokeAsync(() => // private async void complexButton_Click(object sender, EventArgs e) { - await this.InvokeAsync(async (cancellationToken) => - { - // This runs on UI thread but doesn't block it - statusLabel.Text = "Starting complex operation..."; - - var result = await SomeAsyncApiCall(); - - // Update UI directly since we're already on UI thread - resultTextBox.Text = result; - statusLabel.Text = "Operation completed"; - }); + // This runs on UI thread but doesn't block it + statusLabel.Text = "Starting complex operation..."; + + // Dispatch and run on a new thread + await Task.WhenAll(Task.Run(SomeApiCallAsync), + Task.Run(SomeApiCallAsync), + Task.Run(SomeApiCallAsync)); + + // Update UI directly since we're already on UI thread + statusLabel.Text = "Operation completed"; } - private async Task SomeAsyncApiCall() + private async Task SomeApiCallAsync() { - using var httpClient = new HttpClient(); - return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + using var client = new HttpClient(); + + // Simulate random network delay + await Task.Delay(Random.Shared.Next(500, 2500)); + + // Do I/O asynchronously + string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + + // Marshal back to UI thread + await this.InvokeAsync(async (cancelToken) => + { + loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"; + }); + + // Do more async I/O ... } // @@ -186,7 +198,7 @@ await Task.Run(async () => // Marshal back to UI thread this.Invoke(new Action(() => { - resultTextBox.Text = result; + loggingTextBox.Text = result; statusLabel.Text = "Complete"; })); }); diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.resx similarity index 100% rename from dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.resx rename to dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.resx diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs index a8260aefb3..ef1802e457 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs @@ -11,6 +11,6 @@ static void Main() // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); - Application.Run(new ExampleForm()); + Application.Run(new FormAsyncEventHandlers()); } } diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb new file mode 100644 index 0000000000..2df118c7f1 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb @@ -0,0 +1,45 @@ +Imports System.Runtime.CompilerServices +Imports System.Threading + +Module ValueTaskExtensions + + ''' + ''' Converts a Func(Of Task) into a Func(Of ValueTask). + ''' + + Public Function AsValueTask(work As Func(Of Task)) As Func(Of ValueTask) + Return Function() + Return New ValueTask(work()) + End Function + End Function + + ''' + ''' Converts a Func(Of Task(Of T)) into a Func(Of ValueTask(Of T)). + ''' + + Public Function AsValueTask(Of T)(work As Func(Of Task(Of T))) As Func(Of ValueTask(Of T)) + Return Function() + Return New ValueTask(Of T)(work()) + End Function + End Function + + ''' + ''' Converts a Func(Of CancellationToken, Task) into a Func(Of CancellationToken, ValueTask). + ''' + + Public Function AsValueTask(work As Func(Of CancellationToken, Task)) As Func(Of CancellationToken, ValueTask) + Return Function(ct As CancellationToken) + Return New ValueTask(work(ct)) + End Function + End Function + + ''' + ''' Converts a Func(Of CancellationToken, Task(Of T)) into a Func(Of CancellationToken, ValueTask(Of T)). + ''' + + Public Function AsValueTask(Of T)(work As Func(Of CancellationToken, Task(Of T))) As Func(Of CancellationToken, ValueTask(Of T)) + Return Function(ct As CancellationToken) + Return New ValueTask(Of T)(work(ct)) + End Function + End Function +End Module diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/FormAsyncEventHandlers.vb similarity index 73% rename from dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb rename to dotnet-desktop-guide/winforms/forms/snippets/events/vb/FormAsyncEventHandlers.vb index 6180cbf38f..090bded5ab 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vb +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/FormAsyncEventHandlers.vb @@ -4,7 +4,9 @@ Imports System.Threading Imports System.Threading.Tasks Imports System.Windows.Forms -Partial Public Class ExampleForm +Imports System.Runtime.CompilerServices + +Partial Public Class FormAsyncEventHandlers Inherits Form Private WithEvents downloadButton As Button @@ -12,7 +14,7 @@ Partial Public Class ExampleForm Private WithEvents complexButton As Button Private WithEvents badButton As Button Private WithEvents legacyButton As Button - Private resultTextBox As TextBox + Private loggingTextBox As TextBox Private statusLabel As Label Private progressBar As ProgressBar @@ -22,7 +24,7 @@ Partial Public Class ExampleForm processButton = New Button With {.Text = "Process Data"} complexButton = New Button With {.Text = "Complex Operation"} badButton = New Button With {.Text = "Bad Example (Deadlock)"} - resultTextBox = New TextBox With {.Multiline = True, .Width = 300, .Height = 200} + loggingTextBox = New TextBox With {.Multiline = True, .Width = 300, .Height = 200} statusLabel = New Label With {.Text = "Status: Ready"} progressBar = New ProgressBar With {.Width = 300} @@ -51,13 +53,13 @@ Partial Public Class ExampleForm progressBar.Width = downloadButton.Width 'Result text box below progress bar. - resultTextBox.Location = New System.Drawing.Point(Margin, progressBar.Bottom + spacing) + loggingTextBox.Location = New System.Drawing.Point(Margin, progressBar.Bottom + spacing) Controls.Add(downloadButton) Controls.Add(processButton) Controls.Add(complexButton) Controls.Add(badButton) - Controls.Add(resultTextBox) + Controls.Add(loggingTextBox) Controls.Add(statusLabel) Controls.Add(progressBar) End Sub @@ -72,7 +74,7 @@ Partial Public Class ExampleForm Dim content As String = Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") ' Update UI with the result - resultTextBox.Text = content + loggingTextBox.Text = content statusLabel.Text = "Download complete" End Using Catch ex As Exception @@ -89,7 +91,7 @@ Partial Public Class ExampleForm Try ' This blocks the UI thread and causes a deadlock Dim content As String = DownloadPageContentAsync().GetAwaiter().GetResult() - resultTextBox.Text = content + loggingTextBox.Text = content Catch ex As Exception MessageBox.Show($"Error: {ex.Message}") End Try @@ -129,25 +131,46 @@ Partial Public Class ExampleForm ' Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click - ' For VB.NET, we use a simpler approach since async lambdas with CancellationToken are more complex - Await Me.InvokeAsync(Sub() - ' This runs on UI thread - statusLabel.Text = "Starting complex operation..." - End Sub) - - ' Perform the async operation - Dim result As String = Await SomeAsyncApiCall() - - ' Update UI after completion - Await Me.InvokeAsync(Sub() - resultTextBox.Text = result - statusLabel.Text = "Operation completed" - End Sub) + 'Convert the method to enable the extension method on the type + Dim method = DirectCast(AddressOf ComplexButtonClickLogic, + Func(Of CancellationToken, Task)) + + 'Invoke the method asynchronously on the UI thread + Await Me.InvokeAsync(method.AsValueTask()) End Sub - Private Async Function SomeAsyncApiCall() As Task(Of String) - Using httpClient As New HttpClient() - Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + Private Async Function ComplexButtonClickLogic(token As CancellationToken) As Task + ' This runs on UI thread but doesn't block it + statusLabel.Text = "Starting complex operation..." + + ' Dispatch and run on a new thread + Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync), + Task.Run(AddressOf SomeApiCallAsync), + Task.Run(AddressOf SomeApiCallAsync)) + + ' Update UI directly since we're already on UI thread + statusLabel.Text = "Operation completed" + End Function + + Private Async Function SomeApiCallAsync() As Task + Using client As New HttpClient() + + ' Simulate random network delay + Await Task.Delay(Random.Shared.Next(500, 2500)) + + ' Do I/O asynchronously + Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + + ' Marshal back to UI thread + ' Extra work here in VB to handle ValueTask conversion + Await Me.InvokeAsync(DirectCast( + Async Function(cancelToken As CancellationToken) As Task + loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}" + End Function, + Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task + ) + + ' Do more Async I/O ... End Using End Function ' @@ -164,7 +187,7 @@ Partial Public Class ExampleForm ' Marshal back to UI thread Me.Invoke(New Action(Sub() - resultTextBox.Text = result + loggingTextBox.Text = result statusLabel.Text = "Complete" End Sub)) End Function) diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb index 5240214fd6..34c8142943 100644 --- a/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb @@ -5,7 +5,7 @@ Application.SetHighDpiMode(HighDpiMode.SystemAware) Application.EnableVisualStyles() Application.SetCompatibleTextRenderingDefault(False) - Application.Run(New ExampleForm) + Application.Run(New FormAsyncEventHandlers) End Sub End Module From 3e1628d2ab19247623e23ed71735aff56e1785ea Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 21 Aug 2025 10:56:50 -0700 Subject: [PATCH 14/17] Add comment to code --- .../how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs | 2 +- .../how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs index e166dcf60c..4439127806 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBackgroundWorker.cs @@ -21,7 +21,7 @@ public FormBackgroundWorker() private void button1_Click(object sender, EventArgs e) { if (!backgroundWorker1.IsBusy) - backgroundWorker1.RunWorkerAsync(); + backgroundWorker1.RunWorkerAsync(); // Not awaitable } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) diff --git a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb index dd19d32ebd..7f11d44316 100644 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormBackgroundWorker.vb @@ -3,7 +3,7 @@ Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click If (Not BackgroundWorker1.IsBusy) Then - BackgroundWorker1.RunWorkerAsync() + BackgroundWorker1.RunWorkerAsync() ' Not awaitable End If End Sub From 5cf135a2945a00b07323695e451a3973ce61b412 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 21 Aug 2025 10:57:00 -0700 Subject: [PATCH 15/17] Retitle article --- .../winforms/controls/how-to-make-thread-safe-calls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 46b2431b0b..1b5a7a31a8 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -1,5 +1,5 @@ --- -title: How to make thread-safe calls to controls +title: How to handle cross-thread operations with controls description: Learn how to implement multithreading in your app by calling cross-thread controls in a thread-safe way. If you encounter the 'cross-thread operation not valid' error, use the InvokeRequired property to detect this error. The BackgroundWorker component is also an alternative to creating new threads. ms.date: 08/18/2025 ms.service: dotnet-desktop @@ -19,7 +19,7 @@ helpviewer_keywords: - "controls [Windows Forms], multithreading" --- -# How to make thread-safe calls to controls +# How to handle cross-thread operations with controls Multithreading can improve the performance of Windows Forms apps, but access to Windows Forms controls isn't inherently thread-safe. Multithreading can expose your code to serious and complex bugs. Two or more threads manipulating a control can force the control into an inconsistent state and lead to race conditions, deadlocks, and freezes or hangs. If you implement multithreading in your app, be sure to call cross-thread controls in a thread-safe way. For more information, see [Managed threading best practices](/dotnet/standard/threading/managed-threading-best-practices). From ff0f78a7c4406225b01f382a10b45d4e8383d101 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 21 Aug 2025 11:03:39 -0700 Subject: [PATCH 16/17] Fix snippet paths to renamed files --- .../controls/how-to-make-thread-safe-calls.md | 16 +++++++------- dotnet-desktop-guide/winforms/forms/events.md | 22 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 1b5a7a31a8..8ad3579d55 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -29,8 +29,8 @@ There are two ways to safely call a Windows Forms control from a thread that did It's unsafe to call a control directly from a thread that didn't create it. The following code snippet illustrates an unsafe call to the control. The `Button1_Click` event handler creates a new `WriteTextUnsafe` thread, which sets the main thread's property directly. -:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormBad.cs" id="Bad"::: -:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormBad.vb" id="Bad"::: +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs" id="Bad"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb" id="Bad"::: The Visual Studio debugger detects these unsafe thread calls by raising an with the message, **Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on.** The always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. You should fix the issue, but you can disable the exception by setting the property to `false`. @@ -90,13 +90,13 @@ Starting with .NET 9, Windows Forms includes the [!NOTE] > If you're using Visual Basic, the previous code snippet used an extension method to convert a to a . The extension method code is available on [GitHub](https://github.com/dotnet/docs-desktop/blob/main/dotnet-desktop-guide/winforms/forms/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb). @@ -115,8 +115,8 @@ The following example demonstrates a pattern for ensuring thread-safe calls to a The `WriteTextSafe` enables setting the control's property to a new value. The method queries . If returns `true`, `WriteTextSafe` recursively calls itself, passing the method as a delegate to the method. If returns `false`, `WriteTextSafe` sets the directly. The `Button1_Click` event handler creates the new thread and runs the `WriteTextSafe` method. -:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormThread.cs" id="Good"::: -:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormThread.vb" id="Good"::: +:::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs" id="Good"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb" id="Good"::: For more information on how `Invoke` differs from `InvokeAsync`, see [Understanding the difference: Invoke vs InvokeAsync](#understanding-the-difference-invoke-vs-invokeasync). diff --git a/dotnet-desktop-guide/winforms/forms/events.md b/dotnet-desktop-guide/winforms/forms/events.md index ef8c8d13af..ddd9c828ac 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -75,8 +75,8 @@ Modern applications often need to perform asynchronous operations in response to Event handlers can be declared with the `async` (`Async` in Visual Basic) modifier and use `await` (`Await` in Visual Basic) for asynchronous operations. Since event handlers must return `void` (or be declared as a `Sub` in Visual Basic), they are one of the rare acceptable uses of `async void` (or `Async Sub` in Visual Basic): -:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_BasicAsyncEventHandler"::: -:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_BasicAsyncEventHandler"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: > [!IMPORTANT] > While `async void` is generally discouraged, it's necessary for event handlers (and event handler-like code, such as `Control.OnClick`) since they cannot return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the previous example. @@ -88,8 +88,8 @@ Event handlers can be declared with the `async` (`Async` in Visual Basic) modifi The following code demonstrates a common anti-pattern that causes deadlocks: -:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_DeadlockAntiPattern"::: -:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_DeadlockAntiPattern"::: +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_DeadlockAntiPattern"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_DeadlockAntiPattern"::: This causes a deadlock for the following reasons: @@ -114,16 +114,16 @@ When you need to update UI controls from background threads within async operati - **Exception propagation**: Properly propagates exceptions back to the calling code. - **Cancellation support**: Supports `CancellationToken` for operation cancellation. -:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_InvokeAsyncNet9"::: -:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_InvokeAsyncNet9"::: +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_InvokeAsyncNet9"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_InvokeAsyncNet9"::: For truly async operations that need to run on the UI thread: -:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_InvokeAsyncUIThread"::: -:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_InvokeAsyncUIThread"::: +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_InvokeAsyncUIThread"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_InvokeAsyncUIThread"::: > [!TIP] -> .NET 9 includes analyzer warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) to help detect when async methods are incorrectly passed to synchronous overloads of `InvokeAsync`. This helps prevent "fire-and-forget" behavior. +> .NET 9 includes analyzer warnings ([WFO2001](../compiler-messages/wfo2001.md)) to help detect when async methods are incorrectly passed to synchronous overloads of `InvokeAsync`. This helps prevent "fire-and-forget" behavior. > [!NOTE] > If you're using Visual Basic, the previous code snippet used an extension method to convert a to a . The extension method code is available on [GitHub](https://github.com/dotnet/docs-desktop/blob/main/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Extensions.vb). @@ -132,8 +132,8 @@ For truly async operations that need to run on the UI thread: For applications targeting .NET Framework, use traditional patterns with proper async handling: -:::code language="csharp" source="snippets/events/cs/AsyncEventHandlers.cs" id="snippet_LegacyNetFramework"::: -:::code language="vb" source="snippets/events/vb/AsyncEventHandlers.vb" id="snippet_LegacyNetFramework"::: +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_LegacyNetFramework"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_LegacyNetFramework"::: --- From 88e17acc86144f61d2b840a5414fcabb07918a86 Mon Sep 17 00:00:00 2001 From: "Andy De George (from Dev Box)" Date: Thu, 21 Aug 2025 13:57:58 -0700 Subject: [PATCH 17/17] Acro scores updated to 90+ --- .../controls/how-to-make-thread-safe-calls.md | 30 +++++++++---------- dotnet-desktop-guide/winforms/forms/events.md | 8 ++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md index 8ad3579d55..f325a5b81c 100644 --- a/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md +++ b/dotnet-desktop-guide/winforms/controls/how-to-make-thread-safe-calls.md @@ -32,11 +32,11 @@ It's unsafe to call a control directly from a thread that didn't create it. The :::code language="csharp" source="snippets/how-to-make-thread-safe-calls/cs/FormInvokeSync.cs" id="Bad"::: :::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormInvokeSync.vb" id="Bad"::: -The Visual Studio debugger detects these unsafe thread calls by raising an with the message, **Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on.** The always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. You should fix the issue, but you can disable the exception by setting the property to `false`. +The Visual Studio debugger detects these unsafe thread calls by raising an with the message, **Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on.** The always occurs for unsafe cross-thread calls during Visual Studio debugging, and might occur at app runtime. You should fix the issue, but you can disable the exception by setting the property to `false`. ## Safe cross-thread calls -Windows Forms applications follow a strict contract-like framework, similar all other Windows UI frameworks: all controls must be created and accessed from the same thread. Why is this important? Because Windows requires that an application provide a single dedicated thread to deliver system messages to. Whenever the Windows Window Manager detects an interaction to an application window, such as a key press, a mouse click, or resizing the window, it routes that information to the thread that created and manages the UI, and turns it into actionable events. This thread is known as the _UI thread_. +Windows Forms applications follow a strict contract-like framework, similar all other Windows UI frameworks: all controls must be created and accessed from the same thread. This is important because Windows requires applications to provide a single dedicated thread to deliver system messages to. Whenever the Windows Window Manager detects an interaction to an application window, such as a key press, a mouse click, or resizing the window, it routes that information to the thread that created and manages the UI, and turns it into actionable events. This thread is known as the _UI thread_. Because code running on another thread can't access controls created and managed by the UI thread, Windows Forms provides ways to safely work with these controls from another thread, as demonstrated in the following code examples: @@ -54,10 +54,10 @@ Because code running on another thread can't access controls created and managed ## Example: Use Control.InvokeAsync (.NET 9 and later) -Starting with .NET 9, Windows Forms includes the method, which provides async-friendly marshaling to the UI thread. This method is particularly useful for async event handlers and eliminates many common deadlock scenarios. +Starting with .NET 9, Windows Forms includes the method, which provides async-friendly marshaling to the UI thread. This method is useful for async event handlers and eliminates many common deadlock scenarios. > [!NOTE] -> `Control.InvokeAsync` is only available in .NET 9 and later. It is not supported in .NET Framework. +> `Control.InvokeAsync` is only available in .NET 9 and later. It isn't supported in .NET Framework. ### Understanding the difference: Invoke vs InvokeAsync @@ -65,16 +65,24 @@ Starting with .NET 9, Windows Forms includes the [!NOTE] > If you're using Visual Basic, the previous code snippet used an extension method to convert a to a . The extension method code is available on [GitHub](https://github.com/dotnet/docs-desktop/blob/main/dotnet-desktop-guide/winforms/forms/snippets/how-to-make-thread-safe-calls/vb/Extensions.vb). -### Advantages of InvokeAsync - -`Control.InvokeAsync` has several advantages over the older `Control.Invoke` method. It returns a `Task` that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike `Control.Invoke`, the `InvokeAsync` method doesn't block the calling thread, which keeps your apps responsive and avoids hangs. - -The method supports cancellation through `CancellationToken`, so you can cancel operations when needed. It also handles exceptions properly, passing them back to your code so you can deal with errors appropriately. .NET 9 includes compiler warnings ([WFO2001](/dotnet/desktop/winforms/compiler-messages/wfo2001)) that help you use the method correctly. - -For comprehensive guidance on async event handlers and best practices, see [Events overview](../forms/events.md#async-event-handlers). - ## Example: Use the Control.Invoke method The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. It queries the property, which compares the control's creating thread ID to the calling thread ID. If they're different, you should call the method. @@ -129,7 +129,7 @@ An easy way to implement multi-threading scenarios while guaranteeing that the a > > For modern asynchronous programming, use `async` methods with `await` instead. If you need to explicitly offload processor-intensive work, use `Task.Run` to create and start a new task, which you can then await like any other asynchronous operation. For more information, see [Example: Use Control.InvokeAsync (.NET 9 and later)](#example-use-controlinvokeasync-net-9-and-later) and [Cross-thread operations and events](../forms/events.md#cross-thread-operations). -To make a thread-safe call by using , handle the event. There are two events the background worker uses to report status: and . The `ProgressChanged` event is used to communicate status updates to the main thread, and the `RunWorkerCompleted` event is used to signal that the background worker has completed its work. To start the background thread, call . +To make a thread-safe call by using , handle the event. There are two events the background worker uses to report status: and . The `ProgressChanged` event is used to communicate status updates to the main thread, and the `RunWorkerCompleted` event is used to signal that the background worker has completed. To start the background thread, call . The example counts from 0 to 10 in the `DoWork` event, pausing for one second between counts. It uses the event handler to report the number back to the main thread and set the control's property. For the event to work, the property must be set to `true`. diff --git a/dotnet-desktop-guide/winforms/forms/events.md b/dotnet-desktop-guide/winforms/forms/events.md index ddd9c828ac..79c3eacad2 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -36,9 +36,9 @@ This event model uses *delegates* to bind events to the methods that are used to Delegates can be bound to a single method or to multiple methods, referred to as multicasting. When creating a delegate for an event, you typically create a multicast event. A rare exception might be an event that results in a specific procedure (such as displaying a dialog box) that wouldn't logically repeat multiple times per event. For information about how to create a multicast delegate, see [How to combine delegates (Multicast Delegates)](/dotnet/csharp/programming-guide/delegates/how-to-combine-delegates-multicast-delegates). -A multicast delegate maintains an invocation list of the methods it's bound to. The multicast delegate supports a method to add a method to the invocation list and a method to remove it. +A multicast delegate maintains an invocation list of the methods bound to it. The multicast delegate supports a method to add a method to the invocation list and a method to remove it. -When an event is recorded by the application, the control raises the event by invoking the delegate for that event. The delegate in turn calls the bound method. In the most common case (a multicast delegate), the delegate calls each bound method in the invocation list in turn, which provides a one-to-many notification. This strategy means that the control doesn't need to maintain a list of target objects for event notification—the delegate handles all registration and notification. +When an application records an event, the control raises the event by invoking the delegate for that event. The delegate in turn calls the bound method. In the most common case (a multicast delegate), the delegate calls each bound method in the invocation list in turn, which provides a one-to-many notification. This strategy means that the control doesn't need to maintain a list of target objects for event notification—the delegate handles all registration and notification. Delegates also enable multiple events to be bound to the same method, allowing a many-to-one notification. For example, a button-click event and a menu-command–click event can both invoke the same delegate, which then calls a single method to handle these separate events the same way. @@ -73,13 +73,13 @@ Modern applications often need to perform asynchronous operations in response to ### Basic async event handler pattern -Event handlers can be declared with the `async` (`Async` in Visual Basic) modifier and use `await` (`Await` in Visual Basic) for asynchronous operations. Since event handlers must return `void` (or be declared as a `Sub` in Visual Basic), they are one of the rare acceptable uses of `async void` (or `Async Sub` in Visual Basic): +Event handlers can be declared with the `async` (`Async` in Visual Basic) modifier and use `await` (`Await` in Visual Basic) for asynchronous operations. Since event handlers must return `void` (or be declared as a `Sub` in Visual Basic), they're one of the rare acceptable uses of `async void` (or `Async Sub` in Visual Basic): :::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_BasicAsyncEventHandler"::: :::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_BasicAsyncEventHandler"::: > [!IMPORTANT] -> While `async void` is generally discouraged, it's necessary for event handlers (and event handler-like code, such as `Control.OnClick`) since they cannot return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the previous example. +> While `async void` is discouraged, it's necessary for event handlers (and event handler-like code, such as `Control.OnClick`) since they can't return `Task`. Always wrap awaited operations in `try-catch` blocks to handle exceptions properly, as shown in the previous example. ### Common pitfalls and deadlocks