在显示表单事件中启动任务 [英] Start a Task in the Form Shown event
问题描述
我想将ComboBox绑定到53k行的EF Core实体。这大约需要10秒钟的时间。
我以为,如果将绑定过程放在Form Shown
事件中,则UI将保持响应状态。但是事实并非如此。
我尝试过的事情:
私人子目录frmCerere_Shown(sender显示为
Task.Factory.StartNew(Sub()GetProducts(cmbProduse),TaskCreationOptions.LongRunning)
End Sub
Public Shared Sub GetProducts(ctrl As) ComboBox)
将上下文用作EnsightContext =新的EnsightContext
context.Produse.Load()
Dim idsap = context.Produse.Local.Select(Function(o)o.IdSap).ToList
ctrl.DataSource = idsap
使用
结束使用End Sub
To无济于事,因为什么也没有发生。显示了Form,但是ComboBox为空。
如何将ComboBox返回到主线程?
四种在不冻结容器Form的情况下将内容加载到ComboBox(或其他控件)中的方法。
注意:组合框列表不支持无限个项目。将 65534
元素添加到列表后,DropDown实际上将停止工作。
DropDownList和ListBox可以支持更多功能项目,但这些项目也将在某些时候开始崩溃(〜80,000
项目),这些项目的滚动和渲染将明显受到损害。
在所有这些方法中(最后一个方法除外,请在此处阅读说明),
►使用OleDb异步方法查询数据库的第二种方法。
所有方法都接受CancellationToken,可在任何阶段取消该操作。某些操作可能需要一些时间才能生效。无论如何,这一切都是异步发生的。
和以前一样,我们可以捕获 OperationCanceledException
进行通知或记录(或适合特定上下文的任何内容)
私有cts作为CancellationTokenSource
私有异步子Form1_Shown(作为对象发送,作为EventArgs发送)处理MyBase 。显示的
cts =新的CancellationTokenSource()
Dim connString As String =< Some connection string>;
Dim sql As String =< Some Query>;
尝试
ComboBox1.DisplayMember = [[列名]
ComboBox1.ValueMember = [[列名] '可选
ComboBox1.DataSource =等待GetProductsDataAsync(connString,sql,cts.Token)
将ocEx捕获为OperationCanceledException
Console.WriteLine( GetProductsDataAsync canceled)
捕获为异常
'捕获与数据访问相关的异常
Console.WriteLine(ex.ToString())
End Try
'这里的代码在GetProductsDataAsync()之后立即执行返回
结束子
公共异步函数GetProductsDataAsync(connectionString作为String,查询作为String,令牌作为CancellationToken)作为Task(Of DataTable)
token.ThrowIfCancellationRequested()
Dim dt作为数据表=新数据表
使用conn作为新OleDbConnection(连接字符串),
命令作为新OleDbCommand(查询,conn)
等待conn.OpenAsync(令牌)
dt.Load(等待命令.ExecuteReaderAsync(CommandBehavior.CloseConnection,令牌))
结束使用
Retur n dt
结束函数
另外两种可以在您使用时使用的方法需要将一个或多个控件传递给异步过程,这些控件将在以后进行更新。
您需要确保在执行任务时这些控件可用并且它们的句柄已经创建。 / p>
- 具有
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)
token.ThrowIfCancellationRequested()
progress.Report([array of strings])
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
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()
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
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),
command As New OleDbCommand(query, conn)
Await conn.OpenAsync(token)
dt.Load(Await command.ExecuteReaderAsync(CommandBehavior.CloseConnection, 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
' (...)
' 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
► 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.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.
If the data loader procedure makes use of async
methods, Await Task.Delay(1)
can be removed.
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
Private Async Function GetProducts(ctrl As ComboBox) As Task
Await Task.Delay(1)
' 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
这篇关于在显示表单事件中启动任务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!