Four methods to load content in a ComboBox (or other controls) without freezing the container Form.
A note: the ComboBox List doesn’t support an infinite number of Items. The DropDown will actually stop working after
65534 elements are added to the List.
The DropDownList and ListBox can support more items, but these will also begin to crumble at some point (
~80,000 items), the scrolling and the rendering of the Items will be visibly compromised.
In all these methods (except the last, read the note there), a CancellationTokenSource is used to pass a CancellationToken to the method, to signal – if needed – that a cancellation has been requested.
A method can
CancellationTokenSource.Cancel() is called, inspecting the
CancellationToken.IsCancellationRequested property, or throw, calling
.Net methods that accept a CancellationToken always throw. We can try/catch the OperationCanceledException or TaskCanceledException in the calling method to be notified when the cancellation request has been executed.
CancellationTokenSource.Cancel() is also called when the form is closing, in case the data loading is still running.
Nothing) when disposed: its
IsDiposed property is internal and cannot be accessed directly.
▶ First method, using an IProgress<T> delegate, created in the UI Thread, used to update UI Controls when called from a worker thread.
Private cts As CancellationTokenSource Private progress As Progress(Of String()) Private Async Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown cts = New CancellationTokenSource() progress = New Progress(Of String())(Sub(data) OnProgress(data)) Try Await GetProductsProgressAsync(progress, cts.Token) Catch ex As OperationCanceledException ' This exception is raised if cts.Cancel() is called. ' It can be ignored, logged, the User can be notified etc. Console.WriteLine("GetProductsProgressAsync canceled") End Try 'Code here is executed right after GetProductsProgressAsync() returns End Sub Private Sub OnProgress(data As String()) ComboBox1.BeginUpdate() ComboBox1.Items.AddRange(data) ComboBox1.EndUpdate() End Sub Private Async Function GetProductsProgressAsync(progress As IProgress(Of String()), token As CancellationToken) As Task token.ThrowIfCancellationRequested() ' Begin loading data, asynchronous only ' The CancellationToken (token) can be passed to other procedures or ' methods that accept a CancellationToken ' (...) ' If the methods used allow to partition the data, report progress here ' progress.Report([array of strings]) ' End loading data ' Otherwise, generate an IEnumerable collection that can be converted to an array of strings ' (or any other collection compatible with the Control that receives it) progress.Report([array of strings]) End Function Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing CancelTask() End Sub Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click CancelTask() End Sub Private Sub CancelTask() If cts IsNot Nothing Then cts.Cancel() cts.Dispose() cts = Nothing End If End Sub
Note: the Form’s
FormClosing event is subscribed to only here, but the same applies to all other methods, of course
Progress<T> uses a method delegate,
OnProgress(data As String()).
It can be replaced by a Lambda:
' [...] ' Progress<T> can be declared in place Dim progress = New Progress(Of String())( Sub(data) ComboBox1.BeginUpdate() ComboBox1.Items.AddRange(data) ComboBox1.EndUpdate() End Sub) Await GetProductsProgressAsync(progress, cts.Token) ' [...]
▶ A second method that queries a database using OleDb async methods.
All methods accept a CancellationToken that can be used to cancel the operation in any stage. Some operations may take some time before the cancellation takes effect. This all happens asynchronously anyway.
We can catch, as before, the
OperationCanceledException to notify or log (or anything that fits in a specific context) the cancellation.
Private cts As CancellationTokenSource Private Async Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown cts = New CancellationTokenSource() ' <= Can be used to set a Timeout Dim connString As String = "<Some connection string>" Dim sql As String = "<Some Query>" Try ComboBox1.DisplayMember = "[A Column Name]" ComboBox1.ValueMember = "[A Column Name]" ' Optional ComboBox1.DataSource = Await GetProductsDataAsync(connString, sql, cts.Token) Catch ocEx As OperationCanceledException Console.WriteLine("GetProductsDataAsync canceled") Catch ex As Exception ' Catch exceptions related to data access Console.WriteLine(ex.ToString()) End Try 'Code here is executed right after GetProductsDataAsync() returns cts.Dispose() End Sub Public Async Function GetProductsDataAsync(connectionString As String, query As String, token As CancellationToken) As Task(Of DataTable) token.ThrowIfCancellationRequested() Dim dt As DataTable = New DataTable Using conn As New OleDbConnection(connectionString), cmd As New OleDbCommand(query, conn) Await conn.OpenAsync(token) dt.Load(Await cmd.ExecuteReaderAsync(token)) End Using Return dt End Function
Two other methods that can be used when you need to pass to the async procedure one or more Controls that will be updated in the future.
You need to make sure that these Controls are available when the Task executes and that their handle is already created.
- Controls that have
Visible = Falseor are child of a TabContol’s TabPage that has never been shown, don’t create the handle.
▶ The third method, Fire and Forget style. A Task runs a method that loads data from some source. When the loading is finished, the data is set as a ComboBox.DataSource.
BeginInvoke() is used to execute this operation in the UI Thread. Without it, a System.InvalidOperationException with reason
Illegal Cross-thread Operation would be raised.
Before setting the DataSource,
BeginUpdate() is called, to prevent the ComboBox from repainting while the controls load the data.
BeginUpdate is usually called when Items are added one at a time, to both avoid flickering and improve performace, but it’s also useful in this occasion. It’s more evident in the second method.
Private cts As CancellationTokenSource Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown cts = New CancellationTokenSource() Task.Run(Function() GetProducts(Me.ComboBox1, cts.Token)) 'Code here is executed right after Task.Run() End Sub Private Function GetProducts(ctrl As ComboBox, token As CancellationToken) As Task If token.IsCancellationRequested Then Return Nothing ' Begin loading data, synchronous or asynchrnonous ' The CancellationToken (token) can be passed to other procedures or ' methods that accept a CancellationToken ' Async methods will throw is the Task is canceled ' (...) ' End loading data, synchronous or asynchrnonous ' Synchronous methods don't accept a CancellationToken ' In this case, check again now if we've been canceled in the meanwhile If token.IsCancellationRequested Then Return Nothing ctrl.BeginInvoke(New MethodInvoker( Sub() ctrl.BeginUpdate() ctrl.DataSource = [The DataSource] ctrl.EndUpdate() End Sub )) Return Nothing End Function
▶ The fourth method uses the async / await pattern
The Async modifier is added to
Form.Shown event handler.
The Await Operator is applied to
Task.Run(), suspending the execution of other code in the method until the task returns, while control is returned to the current Thread for other operations.
GetProducts() is an
Async method that returns a Task, is in this case.
Code that follows the
Await Task.Run() call is executed after
This procedure works in a different way than the previous one:
here, it’s assumed that the data is loaded in a collection – an
IEnumerable<T> of some sort – maybe a
List<T> as shown in the question.
The data, when available, is added to the
ComboBox.Items collection in chunks of
120 elements (not a magic number, it can be tuned to any other value in relation to the complexity of the data) in a loop.
Await Task.Yield() is called at the end, to comply with the
async/await requirements. It will resume back into the SynchronizationContext captured when
Await is reached.
CancellationTokenSource here. Not because it’s not needed using this pattern, just because I think it could be a good exercise to try to add a
CancellationToken to the method call, as shown in the previous example, to get acquainted. Since this method uses a loop, a cancellation request check can be added to the loop, making the cancellation even more effective.
If the data loader procedure makes use of
Await Task.Yield() can be removed.
Private Async Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown Await Task.Run(Function() GetProductsAsync(Me.ComboBox1)) ' Code here is executed after the GetProducts() method returns End Sub Private Async Function GetProductsAsync(ctrl As ComboBox) As Task ' Begin loading data, synchronous or asynchrnonous ' (...) ' Generates [The List] Enumerable object ' End loading data, synchronous or asynchrnonous Dim position As Integer = 0 For i As Integer = 0 To ([The List].Count \ 120) ' BeginInvoke() will post to the same Thread here. ' It's used to update the Control in a non-synchronous way ctrl.BeginInvoke(New MethodInvoker( Sub() ctrl.BeginUpdate() ctrl.Items.AddRange([The List].Skip(position).Take(120).ToArray()) ctrl.EndUpdate() position += 120 End Sub )) Next Await Task.Yield() End Function