The C# Task asynchronous programming model (TAP) provides an easy way for developer to write asynchronous code. However, it requires you put async
modifier in the method signature, so the caller can use await
keyword to provide a non-blocking way to wait for the underlining task to complete. Then you have to put the async
modifier in the caller method signature and await in its caller, and so on. If you have a legacy application, this kind of change would end up changing lots of your code in order to use the async/await
.
Think about a traditional windows form app, you click a button on the screen, it triggers button click event. In the event handler, it needs call several web services one by one. During this time, you don’t want to block the UI thread, but you also don’t want to user to click other controls on screen. How would we achieve this goal?
AsyncInvoker
The idea is we use another invisible Windows Form and overlay on the existing screen. Therefore any user clicks would be sent to this invisible Windows Form. In this invisible Windows Form class provides InvokeAction(Action syncCall) and InvokeFunc<TResult>(Func<TResult> syncCall) methods. When they are called, this form would display as a modal popup on top of the existing app screen. Then it would execute the action or func using the BackgroundWorker object. Once the BackgroundWorker completes, it would close itself and return the execution result in AsyncCompletedArgs public read only property.
public partial class AsyncInvoker : Form
{
private Form _host;
private BackgroundWorker? _worker;
public RunWorkerCompletedEventArgs AsyncCompletedArgs { get; private set; }
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// BusyForm
//
AutoScaleDimensions = new SizeF(13F, 32F);
AutoScaleMode = AutoScaleMode.Font;
CausesValidation = false;
ClientSize = new Size(1300, 720);
ControlBox = false;
Cursor = Cursors.WaitCursor;
FormBorderStyle = FormBorderStyle.None;
Margin = new Padding(5);
Name = "AsyncInvoker";
Opacity = 0.5D;
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.Manual;
Text = "AsyncInvoker";
Activated += AsyncInvoker_Activated;
ResumeLayout(false);
}
#endregion
public AsyncInvoker(Form hostForm)
{
_host = hostForm;
InitializeComponent();
ResizeToHost();
}
private void WorkerCompleted(object? sender, RunWorkerCompletedEventArgs e)
{
this.Close();
this.AsyncCompletedArgs = e;
_worker?.Dispose();
}
private void ResizeToHost()
{
Rectangle rect;
Point location;
var parentForm = _host.FindForm().Parent;
if (parentForm != null)
{
rect = parentForm.FindForm().ClientRectangle;
location = parentForm.FindForm().PointToScreen(new Point(0, 0));
}
else
{
rect = _host.ClientRectangle;
location = _host.PointToScreen(new Point(0, 0));
}
this.Top = location.Y;
this.Left = location.X;
this.Width = rect.Width;
this.Height = rect.Height;
}
public void InvokeAction(Action syncCall)
{
_worker = new BackgroundWorker()
{
WorkerReportsProgress = false,
WorkerSupportsCancellation = false,
};
_worker.DoWork += (_, args) =>
{
syncCall();
};
_worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(WorkerCompleted);
_worker.RunWorkerAsync();
this.ShowDialog();
}
public void InvokeFunc<TResult>(Func<TResult> syncCall)
{
_worker = new BackgroundWorker()
{
WorkerReportsProgress = false,
WorkerSupportsCancellation = false,
};
_worker.DoWork += (_, e) =>
{
e.Result = syncCall();
};
_worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(WorkerCompleted);
_worker.RunWorkerAsync();
this.ShowDialog();
}
private void AsyncInvoker_Activated(object? sender, EventArgs e)
{
_host.Activate();
this.Cursor = Cursors.WaitCursor;
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_worker != null)
{
_worker.Dispose();
}
}
base.Dispose(disposing);
}
}
Using AsyncInvoker
Here is the example how to use the AsyncInvoker, also the difference between using regular synchronous call and using AsyncInvoker.
In the following screen, we have 3buttons: Sync Call, Async Call and Async Call with Return Value. Behind scene, they all call Thread.Sleep(10000). The label under the “Sync Call” button would be updated by a timer every second.
Clicking the “Sync Call” button, the label stops updating for 10 seconds, which means the UI is blocked; Clicking the “Async Call” button, the label continues to update, which means the UI is NOT blocked. Meanwhile, the cursor becomes busy cursor and the user is not able to click any buttons on the screen. “Async Call with Return Value” button has similar behavior as “Async Call” button, but the call will return a value to the caller.
public partial class Form1 : Form
{
private System.Timers.Timer timer = new System.Timers.Timer(200);
public Form1()
{
InitializeComponent();
}
private void btnSyncCall_Click(object sender, EventArgs e)
{
Thread.Sleep(10000);
}
private void btnAsyncCall_Click(object sender, EventArgs e)
{
AsyncInvoker invoker = new AsyncInvoker(this);
invoker.InvokeAction(() => Thread.Sleep(10000));
}
private void btnAsyncFuncCall_Click(object sender, EventArgs e)
{
AsyncInvoker invoker = new AsyncInvoker(this);
invoker.InvokeFunc<string>(() => { Thread.Sleep(10000); return "test result"; });
MessageBox.Show(invoker.AsyncCompletedArgs.Result?.ToString());
}
private void Form1_Load(object sender, EventArgs e)
{
timer.Elapsed += (timerSender, timerEvent) => UpdateTime(timerSender, timerEvent);
timer.AutoReset = true;
timer.Enabled = true;
}
public void UpdateTime(object source, System.Timers.ElapsedEventArgs e)
{
this.lblCurrentTime.Invoke(() => this.lblCurrentTime.Text = e.SignalTime.ToString("yyyy/MM/dd HH:mm:ss"));
}
}
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
btnSyncCall = new Button();
btnAsyncCall = new Button();
lblCurrentTime = new Label();
btnAsyncFuncCall = new Button();
SuspendLayout();
//
// btnSyncCall
//
btnSyncCall.Location = new Point(42, 32);
btnSyncCall.Name = "btnSyncCall";
btnSyncCall.Size = new Size(141, 29);
btnSyncCall.TabIndex = 0;
btnSyncCall.Text = "Sync Call";
btnSyncCall.UseVisualStyleBackColor = true;
btnSyncCall.Click += btnSyncCall_Click;
//
// btnAsyncCall
//
btnAsyncCall.Location = new Point(243, 32);
btnAsyncCall.Name = "btnAsyncCall";
btnAsyncCall.Size = new Size(141, 29);
btnAsyncCall.TabIndex = 1;
btnAsyncCall.Text = "Async Call";
btnAsyncCall.UseVisualStyleBackColor = true;
btnAsyncCall.Click += btnAsyncCall_Click;
//
// lblCurrentTime
//
lblCurrentTime.AutoSize = true;
lblCurrentTime.Location = new Point(42, 98);
lblCurrentTime.Name = "lblCurrentTime";
lblCurrentTime.Size = new Size(0, 20);
lblCurrentTime.TabIndex = 3;
//
// btnAsyncFuncCall
//
btnAsyncFuncCall.Location = new Point(451, 32);
btnAsyncFuncCall.Name = "btnAsyncFuncCall";
btnAsyncFuncCall.Size = new Size(216, 29);
btnAsyncFuncCall.TabIndex = 4;
btnAsyncFuncCall.Text = "Async Call with Return Value";
btnAsyncFuncCall.UseVisualStyleBackColor = true;
btnAsyncFuncCall.Click += btnAsyncFuncCall_Click;
//
// Form1
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Controls.Add(btnAsyncFuncCall);
Controls.Add(lblCurrentTime);
Controls.Add(btnAsyncCall);
Controls.Add(btnSyncCall);
Name = "Form1";
Text = "Form1";
Load += Form1_Load;
ResumeLayout(false);
PerformLayout();
}
#endregion
private Button btnSyncCall;
private Button btnAsyncCall;
private Label lblCurrentTime;
private Button btnAsyncFuncCall;
}
Happy coding!