问题
I want to bind a ComboBox to an EF Core entity of 53k rows. This takes some time, around 10 seconds.
I thought that if I put the binding process in the Form Shown
event, the UI will stay responsive. But this was not the case.
What I've tried:
Private Sub frmCerere_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Task.Factory.StartNew(Sub() GetProducts(cmbProduse), TaskCreationOptions.LongRunning)
End Sub
Public Shared Sub GetProducts(ctrl As ComboBox)
Using context As EnsightContext = New EnsightContext
context.Produse.Load()
Dim idsap = context.Produse.Local.Select(Function(o) o.IdSap).ToList
ctrl.DataSource = idsap
End Using
End Sub
To no avail, as nothing happens. The Form is shown, but the ComboBox is empty.
How can I return the ComboBox back to the the main thread?
回答1:
Two 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.
► The first method is in 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.
A CancellationTokenSource is used to pass a CancellationToken to the method, to signal that a cancellation is requested, if needed. The method return if it detects that CancellationTokenSource.Cancel() has been called, inspecting (when it can) the CancellationToken.IsCancellationRequested property.
CancellationTokenSource.Cancel()
is also called when the form is closing, in case the data loading is still running.
Set the CancellationTokenSource
to null
(Nothing
) when disposed: its IsDiposed
property is internal and cannot be accessed directly.
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
Public 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
' (...)
' End loading data, synchronous or asynchrnonous
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
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
If cts IsNot Nothing Then
cts.Cancel()
cts.Dispose()
End If
End Sub
Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
If cts IsNot Nothing Then
cts.Cancel()
cts.Dispose()
cts = Nothing
End If
End Sub
► The second 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 GetProducts()
returns.
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.Delay() is called at the beginning, to comply with the async/await
requirements. It's not really necessary, it could be removed, but a warning about the missing Await
operator will appear.
There's no 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.
Private Async Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
Await Task.Run(Function() GetProducts(Me.ComboBox1))
' Code here is executed after the GetProducts() method returns
End Sub
Public Async Function GetProducts(ctrl As ComboBox) As Task
Await Task.Delay(10)
' 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)
ctrl.BeginInvoke(New MethodInvoker(
Sub()
ctrl.BeginUpdate()
ctrl.Items.AddRange([The List].Skip(position).Take(120).ToArray())
ctrl.EndUpdate()
position += 120
End Sub
))
Next
End Function
来源:https://stackoverflow.com/questions/60561044/start-a-task-in-the-form-shown-event