在 Form Shown 事件中启动任务 [英] Start a Task in the Form Shown event

查看:14
本文介绍了在 Form Shown 事件中启动任务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想将一个 ComboBox 绑定到一个 53k 行的 EF Core 实体.这需要一些时间,大约 10 秒.
我认为如果我将绑定过程放在 Form Shown 事件中,UI 将保持响应.但事实并非如此.

我尝试过的:

Private Sub frmCerere_Shown(sender As Object, e As EventArgs) 处理 Me.ShownTask.Factory.StartNew(Sub() GetProducts(cmbProduse), TaskCreationOptions.LongRunning)结束子公共共享子 GetProducts(ctrl As ComboBox)使用上下文作为 EnsightContext = 新的 EnsightContextcontext.Produse.Load()Dim idsap = context.Produse.Local.Select(Function(o) o.IdSap).ToListctrl.DataSource = idsap结束使用结束子

无济于事,因为什么都没有发生.显示了表单,但 ComboBox 为空.
如何将 ComboBox 返回到主线程?

解决方案

在不冻结容器 Form 的情况下在 ComboBox(或其他控件)中加载内容的四种方法.

注意:组合框列表不支持无限数量的项目.在将 65534 元素添加到 List 后,DropDown 实际上将停止工作.
DropDownList 和 ListBox 可以支持更多的项目,但这些也会在某个时候开始崩溃(~80,000 个项目),项目的滚动和呈现将明显受到影响.

在所有这些方法中(除了最后一个,请阅读那里的注释),一个

▶ 第二种使用 OleDb 异步方法查询数据库的方法.
所有方法都接受一个 CancellationToken,可用于在任何阶段取消操作.某些操作可能需要一些时间才能取消生效.无论如何,这一切都是异步发生的.
我们可以像以前一样捕获 OperationCanceledException 以通知或记录(或任何适合特定上下文的内容)取消.

私有 cts 作为 CancellationTokenSourcePrivate Async Sub Form1_Shown(sender As Object, e As EventArgs) 处理 MyBase.Showncts = New CancellationTokenSource() ' <= 可用于设置超时Dim connString As String = "<Some connection string>";Dim sql As String =<Some Query>";尝试ComboBox1.DisplayMember = "[列名]";ComboBox1.ValueMember = "[列名]";' 可选的ComboBox1.DataSource = Await GetProductsDataAsync(connString, sql, cts.Token)将 ocEx 捕获为 OperationCanceledExceptionConsole.WriteLine(GetProductsDataAsync 取消")Catch ex 作为例外' 捕获与数据访问相关的异常Console.WriteLine(ex.ToString())结束尝试'这里的代码在 GetProductsDataAsync() 返回后立即执行cts.Dispose()结束子公共异步函数 GetProductsDataAsync(connectionString As String, query As String, token As CancellationToken) As Task(Of DataTable)token.ThrowIfCancellationRequested()Dim dt As DataTable = 新数据表使用 conn 作为新的 OleDbConnection(connectionString),cmd 作为新的 OleDbCommand(query, conn)等待 conn.OpenAsync(令牌)dt.Load(Await cmd.ExecuteReaderAsync(token))结束使用返回dt结束函数


当您需要将一个或多个将来更新的控件传递给异步过程时可以使用的另外两种方法.
您需要确保这些控件在任务执行时可用,并且它们的句柄已经创建.

  • 具有 Visible = False 或从未显示过的 TabContol 的 TabPage 的子控件的控件,不要创建句柄.

▶ 第三种方法,Fire and Forget 风格.任务运行从某个源加载数据的方法.加载完成后,数据设置为 ComboBox.DataSource.

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?

解决方案

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 return, when CancellationTokenSource.Cancel() is called, inspecting the CancellationToken.IsCancellationRequested property, or throw, calling [CancellationToken].ThrowIfCancellationRequested().

.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.
Set the CancellationTokenSource to null (Nothing) when disposed: its IsDiposed property is internal and cannot be accessed directly.

▶ First method, using an IProgress 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 = False or 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 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.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.

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.

If the data loader procedure makes use of async methods, 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

这篇关于在 Form Shown 事件中启动任务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆