Link to home
Start Free TrialLog in
Avatar of sidwelle
sidwelleFlag for United States of America

asked on

UI controls don't update via Delegate

Please tell me what was wrong with the following snip, Thanks  

 Private Delegate Sub StatusDelegate(ByVal txt As String)
    Public Function Status(ByVal txt As String) As Integer

        'If Me.InvokeRequired Then
        If Me.lblStatus.InvokeRequired Then
            'Me.lblStatus.Invoke(New StatusDelegate(AddressOf Status))
            'Status`.Invoke(New SetText(AddressOf SetLabelText), Text)
            Me.Invoke(New StatusDelegate(AddressOf Status), Text)

        Else
            Me.lblStatus.Text = txt
            'Me.lblStatus.Text = "test ..."
            'Me.Text = "test ..."
            Me.lblStatus.Update()
            Application.DoEvents()
        End If

    End Function

Open in new window

Avatar of Fernando Soto
Fernando Soto
Flag of United States of America image

Hi sidwelle;

The Delegate type should match  the type you are creating. So try it this way. Also when you execute the Status function and the Invoke is executed pass the same parameter as txt not Text.

Private Delegate Function StatusDelegate(ByVal txt As String) As Integer

' This should be a function and not a Sub and should return an Integer
Public Function Status(ByVal txt As String) As Integer

    'If Me.InvokeRequired Then
    If Me.lblStatus.InvokeRequired Then
        'Me.lblStatus.Invoke(New StatusDelegate(AddressOf Status))
        'Status`.Invoke(New SetText(AddressOf SetLabelText), Text)
        Me.Invoke(New StatusDelegate(AddressOf Status), txt)
    Else
        Me.lblStatus.Text = txt
        'Me.lblStatus.Text = "test ..."
        'Me.Text = "test ..."
        Me.lblStatus.Update()
        'Not needed
        'Application.DoEvents()
    End If

End Function

Open in new window

Avatar of sidwelle

ASKER

I made those changes, but I still don't get an update.

I can trace the call in to the 'Status' function and see the value of what was passed and assigne to the text property, but the never does update.

One Time I dragged the exection into the Invoke side of the If statment just to see the Invoke work, but it throws an error: Invoke or BeginInvoke cannot be called on a control until the window handle has been created.

Whats with that ?
Is it not call the main thread ?
From where are you calling this function from? If you want to see the Invoke work it should be called from a different thread. Please show the code where you are calling it from.
its just a simple thread call:

        Dim listener As New Thread(AddressOf main)
        listener.Start(port)
If I set a breakpoint in the Status function, can hover over lblStatus.text and it show the value as assigned.

But the control does not paint !?
If the project is not to big and if you are allowed to zip up the complete project into a zip file and post to the web such as SkyDrive or some other location so that I can download it and try it.

If that is not possible please put together a small test project which show the issue you are having. Then post to the web. Do not post to EE because it will not allow some of the files types in the project.
I tried the below code and it is working as expected, DoEvents is not at all required:
Imports System.Threading

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim t As Thread
        t = New Thread(AddressOf ThreadTask)
        t.IsBackground = True
        t.Start()
    End Sub

    Private Delegate Function StatusDelegate(ByVal txt As String) As Integer

    Public Function Status(ByVal txt As String) As Integer

        'If Me.InvokeRequired Then
        If Me.lblStatus.InvokeRequired Then
            'Me.lblStatus.Invoke(New StatusDelegate(AddressOf Status))
            'Status`.Invoke(New SetText(AddressOf SetLabelText), Text)
            Me.Invoke(New StatusDelegate(AddressOf Status), txt)

        Else
            Me.lblStatus.Text = txt


            'Me.lblStatus.Text = "test ..."
            'Me.Text = "test ..."
        End If
        Return 0
    End Function

    Private Sub ThreadTask()
        Dim stp As Integer
        While True
            stp = stp + 1
            Thread.Sleep(1000)
            Status(stp.ToString())
        End While

        
    End Sub


End Class

Open in new window

Here is a simple app that does not see to work.
Where is it?
Try Again
3rd Try
Its not going to let me upload, so I just pasted it. Looks alot like yours.

Imports System.Threading

Public Class Form1

    'Private WithEvents MyClass1 As New SomeClass

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        'MyClass1.Starter()
        'Starter()

        Dim t As New Thread(AddressOf Main)
        t.IsBackground = True
        t.Start()


    End Sub

    Private Delegate Function StatusDelegate(ByVal txt As String) As Integer
    Public Function Status(ByVal txt As String) As Integer

        'If Me.InvokeRequired Then
        If Me.Label1.InvokeRequired Then
            'Me.lblStatus.Invoke(New StatusDelegate(AddressOf Status))
            'Status`.Invoke(New SetText(AddressOf SetLabelText), Text)
            Me.Invoke(New StatusDelegate(AddressOf Status), txt)

        Else
            Me.Label1.Text = txt
            Me.Text = txt

            Me.Label1.Update()
            'Application.DoEvents()

            'MsgBox(txt)
        End If

    End Function

    Public Function Starter() As Integer

        'Dim t As New Thread(AddressOf Main)
        't.IsBackground = True
        't.Start()

    End Function

    Public Shared Function Main() As Integer

        Form1.Status("This is a test ...")

    End Function



End Class

Public Class SomeClass

    'Public Function Starter() As Integer

    '    Dim t As New Thread(AddressOf Main)
    '    t.IsBackground = True
    '    t.Start()

    'End Function

    'Public Shared Function Main() As Integer

    '    Form1.Status("This is a test ...")

    'End Function

End Class
Are you trying to post to EE?
As I stated in a previous post, "Do not post to EE because it will not allow some of the files types in the project.", That is Why I suggested to use A SkyDrive account which comes with windows live account which cost nothing to have. Or you could use some other website.
If you can suggest a specific site, I will load it there.
Don't have a skyDive account ?
My code is almost identical to what is posted. Is there some setting in the project that I don't have ?
ASKER CERTIFIED SOLUTION
Avatar of Fernando Soto
Fernando Soto
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Removing the keyword "Shared" fixed the small app.

2 Conclusions.
1.) Main (threaded) function can't be "Shared".  You explanation explains the err msg I was receiving (Invoke or BeginInvoke cannot be called on a control until the window handle has been created.)
2.) You must make the call back from the same FORM class ? (Not another class ?)
Thanks
Did that answer this question?
Thanks for the help.

Was not really  sure what 'Shared' meant, I just copied it from another example.
Can I get your opinion on #2 ?   I would like to be able to build another class or module and call back and update the form, if its possible.

Thanks
Hi sidwelle;

In order to create and run the thread in another class and have it access Form1 you will need to pass it a reference to Form1 when you create the other class. Below is a code snippet to show how it can be done. Please note that I turned all your Function's to Sub's because non of them were returning any values.

Imports System.Threading

Public Class Form1


    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        ' Create an instance of the class that will be running the thread. Note that I am passing a reference to this Form in the constructor.
        Dim sClass As New SomeClass(Me)
        ' Start the thread in the SomeClass instance
        sClass.Starter()

    End Sub

    Private Delegate Sub StatusDelegate(ByVal txt As String)

    Public Sub Status(ByVal txt As String)

        If Me.Label1.InvokeRequired Then
            Me.Invoke(New StatusDelegate(AddressOf Status), txt)
        Else
            Me.Label1.Text = txt
            Me.Text = txt
            Me.Label1.Update()
        End If

    End Sub

End Class

Public Class SomeClass

    ' Variable that holds a reference to the Form that will be called to write to the label on.
    Private callingForm As Form1
 
    ' Constructor being passed a reference to the Form that will be modified
    Public Sub New(ByVal f As Form1)
        callingForm = f
    End Sub

    Public Sub Starter()
        Dim t As New Thread(AddressOf Main)
        t.IsBackground = True
        t.Start()
    End Sub

    ' Note that I am using the reference to the other Form when calling Status. 
    Public Sub Main()
        callingForm.Status("This is a test ...")
    End Sub

End Class

Open in new window

Thank you.

I will work with it tomorrow and post back.
I can get it to work, few points ...
1.)  I don't exactly know why it wants "f as form1" and not "f as form" in my head it should accept either ?
I know that from1 will tie you back to a copy that exact form, but why is it so exclusive and not accept any form name ?

2.)  When the Delegate is called it seems to release the second thread, it acts like it waits for a value to be returned, but when the execution gets back to the line in the second thread that called it, the value returned is "nothing" ?
I need to know how to get a value back to the line in the 2nd class that called the function in the IU class.

Again:
I need to know how to get the "Status" function to return a value to the 2nd thread.

I noticed that I can assign return values if the function is not delegated, I can even read form controls ?
Do I only need to use the invoke mechanism when updating a control ?


Thanks
To your questions:

1. Because you need to access the form you will be interacting with or an instance of it and Form is not the right form. Form doest not have a Status Sub and a StatusDelegate definition and if it does it's not the right one .

2. Not sure what you are saying here can you provide more details on this question.

To this question, "I need to know how to get the "Status" function to return a value to the 2nd thread.", Change the delegate as follows, where ReturnDataType is the datatype being returned:

Private Delegate Function StatusDelegate(ByVal txt As String) As ReturnDataType

and change the Status Sub to a Function as follows, where ReturnDataType is the same type as the delegate.:

Public Function Status(ByVal txt As String) As ReturnDataType

Inside the Status function place a Return statement with the variable being returned

To your question, "Do I only need to use the invoke mechanism when updating a control ?", According to the documentation when accessing a controls property there are only four functions that are thread safe and there are Invoke, BeginInvoke, EndInvoke, and CreateGraphics anything else can cause issues.
Tried using the "Return" statement, but the result is the same.
I had seen that before in other examples, just thought someone typed in some java ?
You are not using the Return statement to replace the txt variable are you?
In the following example, a value of 1 is returned, I never did see a value of 2.
Even when I commented each of them out.

    Private Delegate Function StatusDelegate(ByVal txt As String) As Integer
    Public Function Status(ByVal txt As String) As Integer

        If Me.Label1.InvokeRequired Then
            Me.Invoke(New StatusDelegate(AddressOf Status), txt)
            Return 1
        Else
            Me.Label1.Text = txt
            Return 2
        End If

    End Function

Open in new window

Did the value in txt show up in the label?
Yes.

I get the value of 1 with either:

return 1
or
Status = 1

When placed in the If statement before the else.
Can you post the code that you are using so that I can drop it into a project to see what you are trying to do.
The following example lets me pass any form name that I want: (it has to 'Me' if passing the form that you calling from)

Public Class Form2
    Private Sub cmdShowFrm3_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles cmdShowFrm3.Click
        Form3.ShowMe(Me)
    End Sub
End Class

Public Class Form3
    Private cf As Form

    Public Function ShowMe(ByVal f As Form) As Integer
        cf = f
        Me.Show()
    End Function

    Private Sub Form3_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        'Center me on top of calling form ...
        Dim iMidWidth As Integer = cf.Left + cf.Width / 2
        Dim iMidHeight As Integer = cf.Top + cf.Height / 2

        Me.Left = iMidWidth - (Me.Width / 2)
        Me.Top = iMidHeight - (Me.Height / 2)

    End Sub
End Class

Open in new window

Here is the other example:

Imports System.Threading

Public Class Form1
    Private WithEvents MyClass1 As New SomeClass(Me)

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        MyClass1.Starter(Me)
        'Starter()

        'Dim t As New Thread(AddressOf Main)
        't.IsBackground = True
        't.Start()

    End Sub

    Private Delegate Function StatusDelegate(ByVal txt As String) As Integer
    Public Function Status(ByVal txt As String) As Integer

        If Me.Label1.InvokeRequired Then
            Me.Invoke(New StatusDelegate(AddressOf Status), txt)
            'Return 1
            Status = 1
        Else
            Me.Label1.Text = txt
            Me.Text = txt
            Beep()

            'Return 2
            Status = 2
        End If

    End Function

    'Public Function Starter() As Integer

    '    Dim t As New Thread(AddressOf Main)
    '    t.IsBackground = True
    '    t.Start()

    'End Function

    'Public Function Main() As Integer

    '    Thread.Sleep(2000)
    '    Me.Status("This is a test ...")

    'End Function

    Public Class SomeClass
        Private frm As Form1

        Public Function Starter(ByVal f As Form1) As Integer

            Dim t As New Thread(AddressOf Main)
            t.IsBackground = True
            t.Start()

        End Function

        Public Function Main() As Integer
            Dim iRtn As Integer

            Thread.Sleep(500)
            iRtn = frm.Status("This is a test ...")

            MsgBox("Value Returned: " & iRtn.ToString)
        End Function

        Public Sub New(ByVal f As Form1)
            frm = f
        End Sub
    End Class

End Class

Open in new window

Which Form is your startup form?
The Center example: Form2
OK using Form2 as the start up form. Click on the button and it opens Form3 on top of Form2. Form1 never gets created and no thread is called no label gets fill.

Do you have other code that creates Form1 and runs the thread?
No, I meant them to ack as two seperate projects.

With the Center example, I was trying to demonstrate that IDE does not force me to use the name of a specific form in the defination of the function:

Public Function ShowMe(ByVal f As Form) As Integer

Open in new window

To your statement, "With the Center example, I was trying to demonstrate that IDE does not force me to use the name of a specific form in the defination of the function:"; this is true but that is because all Form's inherits from Windows Form control they all have the same basic properties. So Form1, Form2 and Form3 can all be cast to a Form type. But what you can NOT do is have a function in Form3 let say and have that function called PrintDisplay. If you try the same thing then it will NOT work because a object of type Form does not have a function called PrintDisplay you would need to cast it to a type Form3.

The reason you was seeing only return of 1 you was only displaying the return value of the last call only. Make the below changes and you will see both.

Public Function Status(ByVal txt As String) As Integer

    If Me.Label1.InvokeRequired Then
        ' When the next line is executed this thread waits until the else is done
        Dim iRetMainThread As Integer = Me.Invoke(New StatusDelegate(AddressOf Status), txt)
        ' Comming back from GUI thread will print 2 here
        MsgBox("Return value from main thread" & iRetMainThread)
        ' Now return 1 to the original caller of this function.
        'Return 1
        Status = 1
    Else
        Me.Label1.Text = txt
        Me.Text = txt
        Beep()

        'Return 2
        Status = 2
    End If 
                                                       
End Function

Public Function Main() As Integer
    Dim iRtn As Integer

    Thread.Sleep(500)
    iRtn = frm.Status("This is a test ...")

    MsgBox("Value Returned Non GUI thread: " & iRtn.ToString)
End Function

Open in new window

This is the line that I wanted to see, Simple now that you see it.

Status = Me.Invoke(New StatusDelegate(AddressOf Status), txt)

I want to thank you for your help. You have gone above and beyond most of the other experts that answer with a one liner.

If I have any more questions, I will post a new question.

Thanks
Thank you. The way I see it if I am to be of help then do it the right way by making sure what I post is clear and not just a one liner.

Have a great day.