다음을 통해 공유


VB.NET: Invoke Method to update UI from secondary threads

https://msdnshared.blob.core.windows.net/media/2016/05/0640_NinjaAwardTinyGold.pngGold Award Winner



Introduction

As many developers well knows, it's not possible - using .NET Framework - to realize a direct interaction between processes which run on threads different from the one on which the UI resides (typically, the main thread of the program). We are in a situation from which we can exploit the many advantages of multi-threading programming to execute parallel operations, but if those tasks must return an immediate graphical result, we won't be able to access the user controls from those processes.

In this brief article, we'll see how can be possible, through Invoke method, which is available to all controls through the System.Windows.Form namespace, to realize such functionality, in order to execute a graphic refresh and update through delegates.

 


Delegates

The MSDN documentation states delegates are construct which could be compared to the pointer of functions in languages like C or C++. Delegates encapsulate a method inside an object. The delegate object could then be passed to code which will execute the referenced method, o method that could be unknown during the compilation phase of the program itself. Delegates could be EventHandler instances, MethodInvoker-type objects, or any other form of object which ask for a void list of parameters.

Here follows a pretty trivial, though effective, example of their use

 


Basic example

Let's consider a WinForm on which will reside a Label, Label2. That label must be use to show an increasing numeric counter. Since we desire to execute the value increase on a separated thread, we will incur into the named problem. Let's see why. First of all, we will write the code that will execute the increment of our numerical value on a secondary task from the main one, trying to update Label2, to observe the result of the operation.

Private Sub  Form1_Load(sender As  Object, e As  EventArgs) Handles MyBase.Load
  Dim n As Integer  = 0
  Dim t As New  Task(New  Action(Sub()
                              n += 1
                Label2.Text = n.ToString
     End Sub))
 t.Start()
End Sub

At runtime, the raised exception will attest what we saw up to here: it's not possible to modify an object properties (in reality, some of them), if the object itself is managed from a different thread other than the main one.

Yet, the Label control has - like any other control - a method named Invoke, through which we can call delegates toward the main thread. We can rewrite our method like the following. This time, for the sake of completeness, inserting our increment in a loop, to show how Invoke can work inside loops too.

Private Sub  Form1_Load(sender As  Object, e As  EventArgs) Handles MyBase.Load
  Dim n As Integer  = 0
  Dim t As New  Task(New  Action(Sub()
  For n = 1 To 60000
  Label2.Invoke(Sub()
           Label2.Text = n.ToString
           End Sub)
   Next
 End Sub))
  t.Start()
End Sub

Running the program we can see how the graphical data update will be correctly executed, simultaneously with the development of the numerical variable.

That's - aforementioned - a very basic and trivial example, but in a delegate context it's possible to execute an arbitrary numer of operations of different complexity, making it possible to realize any feature in regarding of cross-threading operations.

 


Update UI from different tasks

To explore further the subject, we'll see now a more demanding example, in terms of resources needed. In this case, we'll make a program that will process simultaneously many text files, to update the UI to show what a single thread is doing in a given moment.

Example definition

We have 26 different text files, each of which contains a variable number of words, mainly Italian but not exclusively. Those words begin with a particular letter: for example, the file "dizionario_a.txt" will contain only words which stars with "a", "dizionario_b.txt" only those which starts with "b", and so on. We desire to write a program which possess 26 labels and, starting a task for each letter, will proceed in inserting every read word in a variable of type List(Of String). Each task must show the processed word, so that every thread will execute - through Invoke() method - the refreshing of the content of the Label on which it works.

Source code

Let's take a look to the code, to make some considerations after it.

Imports System.IO
 
Public Class  Form1
 
    Dim wordList As New  List(Of String)
 
    Public Sub  AddWords(letter As Char, lbl As  Label)
        Using sR As  New StreamReader(Application.StartupPath &  "\text\dizionario_" & letter & ".txt")
            While Not  sR.EndOfStream
                Dim word As String  = sR.ReadLine
 
                wordList.Add(word)
 
                lbl.Invoke(Sub()
                      lbl.Text = word
                      counter.Text = wordList.Count.ToString
                      Me.Refresh()
                    End Sub)
            End While
        End Using
 
        lbl.ForeColor = Color.Green
    End Sub
 
    Private Sub  Form1_Load(sender As Object, e As  EventArgs) Handles MyBase.Load
        Me.DoubleBuffered = True
 
        Dim _x As Integer  = 8
        Dim _y As Integer  = 40
 
        For ii As Integer  = Asc("a")  To  Asc("z")
 
            Dim c As New  Label
            c.Name = "Label" & ii.ToString("000")
            c.Text = "---"
            c.Top = _y
            c.Left = _x
            Me.Controls.Add(c)
 
            _y += 20
            If _y > 180 Then
                _y = 40
                _x += 120
            End If
 
            Dim j As Integer  = ii
            Dim t As New  Task(Sub()
                  AddWords(Chr(j), CType(Me.Controls("Label" & j.ToString("000")), Label))
                 End Sub)
 
            t.Start()
        Next
 
    End Sub
 
End Class

The project is a simple WinForms program. During Load() event, we'll create all the Labels we need, starting the tasks that will use, each of them on a different word, the AddWords() subroutine, to process a single dictionary (dictionary files will be found in the downloadable project, at the end of the article). We will see, taking a look at the loop in the Load() event, that each created task is launched immediately, letting the OS the worries of queuing those processes which aren't immediately manageable.

Each task, calling the AddWords() subroutine, will provide in: 1) opening the file having a given letter, 2) read each line, 3) save that value in the List wordList. We will also note a calling to the method Invoke() on the Label passed as argument to the subroutine. An interesting particular is that inside a single Invoke() a developer can manage more than a single UI update. In our specific case, we can see how the Label is updated, updating also the one named "counter", which we will use to show the global number of words read to a given moment. Moreover, to help the graphical rendering of our controls, we will call upon the Refresh() method of the Form itself, though - doing so - we will impair the overall performances, since the program will dedicate part of its cycles to refresh the Form and all its children, at every iteration.

As we can see running the code, or through the following video, the update of the content of our controls will happen in a concerted way, allowing to each task to modify the value of controls which constitutes the UI of our program.

Demonstrative video

View


Download

The source code of the example above, and its auxiliary files, can be freely downloaded from the following URL: 

https://code.msdn.microsoft.com/Invoke-Method-to-update-UI-5970a859

 


Other languages

This article is also available in the following localizations: