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..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 @@ -1,8 +1,9 @@ --- -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: 06/20/2021 +ms.date: 08/18/2025 ms.service: dotnet-desktop +ai-usage: ai-assisted dev_langs: - "csharp" - "vb" @@ -18,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). @@ -28,36 +29,114 @@ 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`. +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 -The following code examples demonstrate two ways to safely call a Windows Forms control from a thread that didn't create it: +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_. -1. The method, which calls a delegate from the main thread to call the control. -2. A component, which offers an event-driven model. +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: -In both examples, the background thread sleeps for one second to simulate work being done in that thread. +- [Example: Use Control.InvokeAsync (.NET 9 and later)](#example-use-controlinvokeasync-net-9-and-later) -## Example: Use the Invoke method + The method (.NET 9+), which provides async-friendly marshaling to the UI thread. + +- [Example: Use the Control.Invoke method](#example-use-the-controlinvoke-method): + + The method, which calls a delegate from the main thread to call the control. + +- [Example: Use a BackgroundWorker](#example-use-a-backgroundworker) + + A component, which offers an event-driven model. + +## 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 useful for async event handlers and eliminates many common deadlock scenarios. + +> [!NOTE] +> `Control.InvokeAsync` is only available in .NET 9 and later. It isn't 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 when the delegate marshaled to the message queue is itself waiting for a message to arrive (deadlock). +- Useful when you have results ready to display on the UI thread, for example: disabling a button or setting the text of a control. + +**Control.InvokeAsync (Posting - Non-blocking):** + +- Asynchronously posts the delegate to the UI thread's message queue instead of waiting for the invoke to finish. +- Returns immediately without blocking the calling thread. +- Returns a `Task` that can be awaited for completion. +- Ideal for async scenarios and prevents UI thread bottlenecks. + +#### 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. + +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). + +### Choosing the right InvokeAsync overload + +`Control.InvokeAsync` provides four overloads for different scenarios: + +| 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. | + +*Visual Basic doesn't support 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/FormInvokeAsync.cs" id="snippet_InvokeAsyncBasic"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.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/FormInvokeAsync.cs" id="snippet_InvokeAsyncAdvanced"::: +:::code language="vb" source="snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.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). + +## 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"::: +:::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). ## 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. +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 . +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`. :::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) +[invoke1_func_value_return]: xref:System.Windows.Forms.Control.InvokeAsync``1(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``0}},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 48a1b183b9..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.Designer.cs +++ /dev/null @@ -1,73 +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 27de7e3db7..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Form1.cs +++ /dev/null @@ -1,48 +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/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..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 @@ -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(); // Not awaitable + } - 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 deleted file mode 100644 index 9fbf81a92d..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.Designer.cs +++ /dev/null @@ -1,72 +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 9a854be92c..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormBad.cs +++ /dev/null @@ -1,31 +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) - { - var thread2 = new System.Threading.Thread(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/FormInvokeAsync.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormInvokeAsync.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/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 a3f5ee942e..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.Designer.cs +++ /dev/null @@ -1,73 +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 bdde2acee9..0000000000 --- a/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/FormThread.cs +++ /dev/null @@ -1,50 +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/Program.cs b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/cs/Program.cs index 745b07829e..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 @@ -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 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/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 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/FormInvokeAsync.resx b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/controls/snippets/how-to-make-thread-safe-calls/vb/FormInvokeAsync.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/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/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 ea443e7c28..79c3eacad2 100644 --- a/dotnet-desktop-guide/winforms/forms/events.md +++ b/dotnet-desktop-guide/winforms/forms/events.md @@ -1,9 +1,10 @@ --- 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 dev_langs: ["csharp", "vb"] helpviewer_keywords: - "Windows Forms, event handling" @@ -35,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. @@ -66,6 +67,85 @@ 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` (`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 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 + +> [!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/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: + +- 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 + +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), `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. + +:::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/FormAsyncEventHandlers.cs" id="snippet_InvokeAsyncUIThread"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.vb" id="snippet_InvokeAsyncUIThread"::: + +> [!TIP] +> .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). + +# [.NET Framework](#tab/dotnetframework) + +For applications targeting .NET Framework, use traditional patterns with proper async handling: + +:::code language="csharp" source="snippets/events/cs/FormAsyncEventHandlers.cs" id="snippet_LegacyNetFramework"::: +:::code language="vb" source="snippets/events/vb/FormAsyncEventHandlers.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. + ## Related content - [Handling and raising events in .NET](/dotnet/standard/events/index) 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..c27cd77f22 --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/AsyncEventHandlers.csproj @@ -0,0 +1,11 @@ + + + + WinExe + net9.0-windows + enable + true + enable + + + \ No newline at end of file diff --git a/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs new file mode 100644 index 0000000000..35654731fd --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.cs @@ -0,0 +1,220 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace AsyncEventHandlers; + +public partial class FormAsyncEventHandlers : Form +{ + private Button downloadButton; + private Button processButton; + private Button complexButton; + private Button badButton; + private TextBox loggingTextBox; + private Label statusLabel; + private ProgressBar progressBar; + + 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)" }; + loggingTextBox = new TextBox { Multiline = true, Width = 400, 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. + loggingTextBox.Location = new System.Drawing.Point(margin, progressBar.Bottom + spacing); + + + Controls.Add(downloadButton); + Controls.Add(processButton); + Controls.Add(complexButton); + Controls.Add(badButton); + Controls.Add(loggingTextBox); + Controls.Add(statusLabel); + Controls.Add(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://github.com/dotnet/docs/raw/refs/heads/main/README.md"); + + // Update UI with the result + loggingTextBox.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 + { + // This blocks the UI thread and causes a deadlock + string content = DownloadPageContentAsync().GetAwaiter().GetResult(); + loggingTextBox.Text = content; + } + catch (Exception ex) + { + 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 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); + + // 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; + } + // + + // + private async void complexButton_Click(object sender, EventArgs e) + { + // 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 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 ... + } + // + + // + 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(() => + { + loggingTextBox.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(); + 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/FormAsyncEventHandlers.resx b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/FormAsyncEventHandlers.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/Program.cs b/dotnet-desktop-guide/winforms/forms/snippets/events/cs/Program.cs new file mode 100644 index 0000000000..ef1802e457 --- /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 FormAsyncEventHandlers()); + } +} 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..8f023ab8cb --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/AsyncEventHandlers.vbproj @@ -0,0 +1,17 @@ + + + + WinExe + net9.0-windows + AsyncEventHandlers + Sub Main + true + + + + + + + + + \ No newline at end of file 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/FormAsyncEventHandlers.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/FormAsyncEventHandlers.vb new file mode 100644 index 0000000000..090bded5ab --- /dev/null +++ b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/FormAsyncEventHandlers.vb @@ -0,0 +1,206 @@ +Imports System +Imports System.Net.Http +Imports System.Threading +Imports System.Threading.Tasks +Imports System.Windows.Forms + +Imports System.Runtime.CompilerServices + +Partial Public Class FormAsyncEventHandlers + 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 loggingTextBox 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)"} + loggingTextBox = 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. + loggingTextBox.Location = New System.Drawing.Point(Margin, progressBar.Bottom + spacing) + + Controls.Add(downloadButton) + Controls.Add(processButton) + Controls.Add(complexButton) + Controls.Add(badButton) + Controls.Add(loggingTextBox) + 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() + Dim content As String = Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md") + + ' Update UI with the result + loggingTextBox.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 + ' This blocks the UI thread and causes a deadlock + Dim content As String = DownloadPageContentAsync().GetAwaiter().GetResult() + loggingTextBox.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 + '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 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 + ' + + ' + 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() + loggingTextBox.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/Program.vb b/dotnet-desktop-guide/winforms/forms/snippets/events/vb/Program.vb new file mode 100644 index 0000000000..34c8142943 --- /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 FormAsyncEventHandlers) + End Sub + +End Module