Start a Task in the Form Shown event

孤者浪人 提交于 2020-05-09 05:56:02

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!