从另一个线程中的模块更新标签 [英] Updating label from module in another thread
问题描述
我正在尝试从模块更新在 UI 线程中运行的标签......如果它在主窗体中,我的代码工作正常,但我想尝试通过不使用它来保持我的代码整洁全部在主窗体中,并将其拆分为模块.
I'm trying to update a label which is running in the UI thread from a Module... my code works fine if its within the Main Form but I'd like to try and keep my code tidy by not having it all within the Main Form and splitting it out into Modules.
所以如果我的主表单中有以下内容,它就可以工作:
So if I have the below in my Main Form it works:
Private threadingExecuteManualScan As Thread
Public Sub toolStripItem_Run_Manual_Scan_Click(sender As Object, e As EventArgs) Handles toolStripItem_Run_Manual_Scan.Click
threadingExecuteManualScan = New Thread(AddressOf executeManualScanThread)
threadingExecuteManualScan.IsBackground = True
threadingExecuteManualScan.Start()
End Sub
Delegate Sub SetTextDelegate(ByVal textString As String)
Private Sub updateTextBox(ByVal stringValue As String)
Dim textDelegate As New SetTextDelegate(AddressOf updateTextBox)
form_Main.BeginInvoke(textDelegate, stringValue)
End Sub
Public Sub executeManualScanThread()
updateTextBox("Update Label With This String")
End Sub
我想将所有内容移到一个模块中,除了:
I'd like to move all of it to a Module except for:
Public Sub toolStripItem_Run_Manual_Scan_Click(sender As Object, e As EventArgs) Handles toolStripItem_Run_Manual_Scan.Click
threadingExecuteManualScan = New Thread(AddressOf executeManualScanThread)
threadingExecuteManualScan.IsBackground = True
threadingExecuteManualScan.Start()
End Sub
但是当我执行 Invoke.Required 时,它永远不会返回一个真值,该值不会更新我的主表单上的标签.
But when I do the Invoke.Required never returns a true value which then doesn't update my label on my Main Form.
我做错了什么?
谢谢
本
更新
我的主表单包含这个:
Public Class form_Main
Delegate Sub SetTextDelegate(ByVal args As String)
Private threadingExecuteManualScan As Thread
Public Sub toolStripItem_Run_Manual_Scan_Click(sender As Object, e As EventArgs) Handles toolStripItem_Run_Manual_Scan.Click
threadingExecuteManualScan = New Thread(AddressOf executeManualScanThread)
threadingExecuteManualScan.IsBackground = True
threadingExecuteManualScan.Start()
End Sub
Public Sub updateTextBox(ByVal stringValue As String)
Dim textDelegate As New SetTextDelegate(AddressOf updateTextBox)
me.BeginInvoke(textDelegate, stringValue)
End Sub
End Class
还有我的模块:
Module module_Helper_Threading
Public Sub executeManualScanThread()
'Some Database Work
form_Main.SetTextBoxInfo("Report Back - Step 1")
'Some More Database Work
form_Main.SetTextBoxInfo("Report Back - Step 2")
'etc
End Sub
End Module
然而这会导致错误:
Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
推荐答案
有趣的是,您发布的代码的第一个版本也是最好的.
It's amusing that the very first version of the code you posted was also the best.
与其就如何改进事情进行冗长的讨论,不如让我介绍一下您当前代码的重写,这将实现主要目标并让您快速启动和运行:
Rather than going into a lengthy discussion about how things could be improved let me present a rewrite of your current code which would achieve the main goal and get you up and running, quickly:
Imports System.Threading
Public Class form_Main
Private Sub toolStripItem_Run_Manual_Scan_Click() Handles toolStripItem_Run_Manual_Scan.Click
Dim t As New Thread(Sub() module_Helper_Threading.executeManualScanThread(Me))
t.IsBackground = True
t.Start()
End Sub
Public Sub SetTextBoxInfo(stringValue As String)
Me.BeginInvoke(Sub() Me.TextBox.Text = stringValue)
End Sub
End Class
Module module_Helper_Threading
Public Sub executeManualScanThread(form_Main As form_Main)
'Some Database Work
Thread.Sleep(1000)
form_Main.SetTextBoxInfo("Report Back - Step 1")
'Some More Database Work
Thread.Sleep(1000)
form_Main.SetTextBoxInfo("Report Back - Step 2")
'etc
End Sub
End Module
这会起作用,因为现在您正在传递对 form_Main
实例的具体引用.然而,我对这种方法的问题是,你的模块首先应该没有 form_Main
的概念.我最初的建议是通过 Progress/IProgress
报告进度,但它只适用于处理某种类型的集合,而您正在处理不同的数据库操作的情况,因此更好的设计如下:
This will work because now you're passing around a concrete reference to an instance of form_Main
. My problem with this approach, however, is that your module should really have no notion of form_Main
in the first place. My initial recommendation was going to be progress reporting via Progress/IProgress
, but it's only appropriate in cases where you're processing a collection of some kind, whereas you're working with disparate database operations, so a better design would be as follows:
Imports System.Threading
Public Class form_Main
Private Sub toolStripItem_Run_Manual_Scan_Click() Handles toolStripItem_Run_Manual_Scan.Click
Dim t As New Thread(AddressOf Me.RunManualScan)
t.IsBackground = True
t.Start()
End Sub
Private Sub RunManualScan()
' We know this will be running on a background thread.
Dim workResult1 = DatabaseWork.SomeWork()
Me.SetTextBoxInfo("Report Back - Step " & workResult1)
Dim workResult2 = DatabaseWork.OtherWork()
Me.SetTextBoxInfo("Report Back - Step " & workResult2)
End Sub
Public Sub SetTextBoxInfo(stringValue As String)
Me.BeginInvoke(Sub() Me.TextBox.Text = stringValue)
End Sub
End Class
' You could use a Module, but it
' pollutes IntelliSense more than Class.
Public NotInheritable Class DatabaseWork
Public Shared Function SomeWork() As Int32
Thread.Sleep(1000)
Return 1
End Function
Public Shared Function OtherWork() As Int32
Thread.Sleep(1000)
Return 2
End Function
End Class
现在我们有了更好的关注点分离:数据库"只知道细粒度的数据库操作,而表单知道如何将这些数据库操作放在一起并在必要时更新自身.由于 Thread
和 BeginInvoke
的使用,它仍然很难看..NET 4.5 提供了更好的组合异步操作的机制,允许我们将上述内容重写如下:
Now we have better separation of concerns: the "database" only knows about the fine-grained database operations, and the form knows how to put those database operations together and update itself when necessary. It's still ugly though due to Thread
and BeginInvoke
use. .NET 4.5 provides better mechanisms of composing asynchronous operations which allow us to rewrite the above as follows:
Imports System.Threading
Imports System.Threading.Tasks
Public Class form_Main
Private Sub toolStripItem_Run_Manual_Scan_Click() Handles toolStripItem_Run_Manual_Scan.Click
Me.ExecuteManualScan()
End Sub
' Note the Async modifier.
Private Async Sub ExecuteManualScan()
' The delegate passed to Task.Run executes on
' a thread pool (background) thread. Await'ing
' a task transitions us back to the original thread.
' Note that it is good practice to use Task.Run for
' CPU-bound work, but since we're stuck with blocking
' database operations, it will have to do in this case.
Dim workResult1 = Await Task.Run(AddressOf DatabaseWork.SomeWork)
' Note the lack of BeginInvoke - we're already on the UI thread.
Me.TextBox.Text = "Report Back - Step " & workResult1
' Note that the delegate is declared slightly differently.
' While functionally similar to the first call, this version
' allows you to pass arguments to the method if necessary.
Dim workResult2 = Await Task.Run(Function() DatabaseWork.OtherWork())
Me.TextBox.Text = "Report Back - Step " & workResult2
End Sub
End Class
编辑
如果您绝对必须报告长时间运行的操作的进度,因为 .NET 4.0 System.Progress(Of T)/IProgress(Of T)
是在调用者中执行此操作的推荐方式-不可知的时尚.请注意,它是一种通用类型,因此最终取决于您想要在整个处理过程中报告的确切内容 - 虽然约定是 Int32
表示进度百分比,但您也可以使用完全任意的东西例如String
s.
If you absolutely must report progress from a long-running operation, since .NET 4.0 System.Progress(Of T)/IProgress(Of T)
is the recommended way of doing so in a caller-agnostic fashion. Note that it's a generic type so it is ultimately up to you what it is exactly that you want to report throughout the processing - and while the convention is Int32
denoting progress percentage, you could also use something completely arbitrary like String
s, for example.
Imports System.Threading
Imports System.Threading.Tasks
Public Class form_Main
Private Sub toolStripItem_Run_Manual_Scan_Click() Handles toolStripItem_Run_Manual_Scan.Click
Me.ExecuteManualScan()
End Sub
Private Async Sub ExecuteManualScan()
' Ensure that the next scan operation cannot
' be started until this one is complete by
' disabling the relevant UI elements.
Me.toolStripItem_Run_Manual_Scan.Enabled = False
Try
Me.TextBox.Text = "Starting ..."
' When you create an instance of Progress, it captures
' the current SynchronizationContext, and will raise
' the ProgressChanged event on that context, meaning
' that if it's created on the UI thread, the progress
' handler callback will automatically be marshalled back
' to the UI thread for you, so you no longer need Invoke.
Dim progress As New Progress(Of Int32)
' Update the UI when progress is reported.
AddHandler progress.ProgressChanged,
Sub(s, progressPercentage) Me.TextBox.Text = String.Format("Progress: {0}%.", progressPercentage)
Dim workResult = Await Task.Run(Function() DatabaseWork.LongWork(progress))
Me.TextBox.Text = "Result: " & workResult
Finally
Me.toolStripItem_Run_Manual_Scan.Enabled = True
End Try
End Sub
End Class
Public NotInheritable Class DatabaseWork
Public Shared Function LongWork(progress As IProgress(Of Int32)) As Int32
Dim progressPercentage = 0
For i = 0 To 100 - 1
' Simulate some work.
Thread.Sleep(10)
progressPercentage += 1
progress.Report(progressPercentage)
Next
Return 42
End Function
End Class
这篇关于从另一个线程中的模块更新标签的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!