从另一个线程中的模块更新标签 [英] Updating label from module in another thread

查看:24
本文介绍了从另一个线程中的模块更新标签的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试从模块更新在 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

现在我们有了更好的关注点分离:数据库"只知道细粒度的数据库操作,而表单知道如何将这些数据库操作放在一起并在必要时更新自身.由于 ThreadBeginInvoke 的使用,它仍然很难看..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 表示进度百分比,但您也可以使用完全任意的东西例如Strings.

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 Strings, 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屋!

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