need help with cross-thread calling to form

tonll

Member
Joined
Oct 10, 2006
Messages
7
Location
SG
Programming Experience
1-3
okay.. so i know how to do this with text from the documentation example. thing is... i need to call a saveFileDialog to do saveFileDialog.showDialog() from a thread other than it was created on. so how do i invoke the control to do this???
the only thing close that i can find is this: http://www.vbdotnetforums.com/showthread.php?t=13763

which i dont really understand.

if i just call savefiledialog1.showdialog() on the thread, it'll show this(even tho i did add that STA thingy):
System.Threading.ThreadStateException was unhandled
Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it. This exception is only raised if a debugger is attached to the process."

please help. thanks.
 
Im not exactly sure what your problem is there.. but why don't you create a new SaveFileDialog object on your other thread? That way its always "used on the thread that created it". You can pass the Filename result back via a cross-thread call.
 
hmm

maybe this will help you understand what i mean:

VB.NET:
    <STAThread()> Public Sub handlerThread()

        Dim handlerSocket As Socket
        handlerSocket = alSockets(alSockets.Count - 1)
        Dim networkStream As NetworkStream = New NetworkStream(handlerSocket)
        Dim blocksize As Int16 = 1024
        Dim thisRead As Int16
        Dim databyte(blocksize) As Byte
        Dim sw As StreamWriter

        sw = Nothing

        SaveFileDialog1.ShowDialog()
        savePath = SaveFileDialog1.FileName
        SyncLock Me
            'creating a streamwriter to write data read from thisRead into file.
            sw = New StreamWriter(savePath)
            'reading data
            thisRead = networkStream.Read(databyte, 0, blocksize)
            'storing data read into a string
            textData = Encoding.ASCII.GetString(databyte)
            'write data into file
            sw.Write(textData)
            'close streamwriter
            sw.Close()

        End SyncLock
        'calling to another thread to update listbox
        Me.txtThread = New Thread(New ThreadStart(AddressOf Me.ThreadProcSafe))
        Me.txtThread.Start()

        handlerSocket = Nothing


    End Sub

see that? im trying to get savefiledialog to show when it reaches the handler thread(which is called from another thread, which is called from the main form). and as far as i know, there isnt "savefiledialog.invokerequired".
 
As mafrosis asked, can you create a new SaveFileDialog in that thread and use that instead? (Dim SFD As New SaveFileDialog)

Also, don't understand why you SyncLock the class instance there.
 
Thanks John for providing the sample code. i didnt really get what mafrosis said in creating a saveFileDialog object. actually, i didnt know you could do this "(Dim SFD As New SaveFileDialog)". Never used that before..

so i tried this:
VB.NET:
[SIZE=2]<STAThread()> [/SIZE][SIZE=2][COLOR=#0000ff]Public[/COLOR][/SIZE][SIZE=2] [/SIZE][SIZE=2][COLOR=#0000ff]Sub[/COLOR][/SIZE][SIZE=2] handlerThread()
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] handlerSocket [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Socket
handlerSocket = alSockets(alSockets.Count - 1)
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] networkStream [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] NetworkStream = [/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] NetworkStream(handlerSocket)
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] blocksize [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Int16 = 1024
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] thisRead [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Int16
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] databyte(blocksize) [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] [/SIZE][SIZE=2][COLOR=#0000ff]Byte
[/COLOR][/SIZE][SIZE=2][/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] sw [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] StreamWriter
[/SIZE][SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] sfd [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] SaveFileDialog
sfd = [/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] SaveFileDialog
sw = [/SIZE][SIZE=2][COLOR=#0000ff]Nothing
 
 
[/COLOR][/SIZE][SIZE=2]sfd.ShowDialog()
savePath = sfd.FileName

is that correct? i still get a ThreadStateException. sorry about this im really a noob in objects and its my first time dealing with multithreading. thanks so much for any help.
[/SIZE]
 
First, your 'Stathread' attribute has no effect on other methods than Sub Main. If you wish you must set this apartment state on the thread with Thread.SetApartmentState method before starting it.

There is also the possibility to ask for filename before you start the worker thread and just pass that filename to it, either running the show with a 'threaded' class instance or by locking your filename instance variable that 'belongs' to main thread.
 
tonll, what I mean by "creating the savefiledialog object" is when you use New. And to clarify for you, these 2 example are exactly the same - one is just blending the variable declaration with actually creating the object:

VB.NET:
Dim sfd As SaveFileDialog
sfd = New SaveFileDialog

VB.NET:
Dim sfd As New SaveFileDialog

If you post up all of your code (ie, both threads) ill run it at work and see what this ThreadStateException is about, then post you something working. Threading can be a bitch on your first go. :)
 
oooh.. okay. i thought it was something else. erm. its actually a client server application. so there's a need to get a connection from the client side. (2 seperate programs) but here it is:

client side:
VB.NET:
Imports System.Threading
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.IO

Public Class Form1

    Private Sub btnFile_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnFile.Click
        OpenFileDialog1.ShowDialog()
        txtFile.Text = OpenFileDialog1.FileName

    End Sub

    Private Sub btnSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSend.Click
        Dim filebuffer As Byte()
        Dim filestream As Stream
        Try

            filestream = File.OpenRead(txtFile.Text)
            ReDim filebuffer(filestream.Length)
            filestream.Read(filebuffer, 0, filestream.Length)

            Dim clientSocket As New TcpClient(txtIPadd.Text, 8080)
            Dim networkStream As NetworkStream
            networkStream = clientSocket.GetStream()
            networkStream.Write(filebuffer, 0, filestream.Length)

        Catch exc As SocketException
            MessageBox.Show("Please enter a valid IP address", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        Catch exc As ArgumentException
            MessageBox.Show("Please open a valid file for sending", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        Catch exc As FileNotFoundException
            MessageBox.Show("Please open a valid file for sending", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)

        End Try





    End Sub


End Class

server side:
VB.NET:
Imports System.IO
Imports System.Threading
Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.ComponentModel
Imports System.Windows.Forms

Public Class Form1
    Private alSockets As ArrayList
    Public textData As String
    Delegate Sub SetTextCallback(ByVal [text] As String)
    Private showSave As Thread
    Private txtThread As Thread = Nothing
    Public conThread As Thread = Nothing
    Public info As String
    Public savePath As String


    <STAThread()> Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        textData = ""
        savePath = ""
        Dim IPHost As IPHostEntry
        IPHost = Dns.GetHostEntry(Dns.GetHostName())
        lblStatus.Text = "IP address: " + IPHost.AddressList(0).ToString()
        alSockets = New ArrayList()
        Dim thdListener As New Thread(New ThreadStart(AddressOf listenerThread))
        thdListener.Start()
        lbConnections.Items.Add("Waiting for connection..")


    End Sub
    Public Sub displayConnection()

        Me.SetText(info + vbTab + "connected.")

    End Sub
    Public Sub listenerThread()

        Dim tcpListener As New TcpListener(IPAddress.Any, 8080)
        Dim handlerSocket As Socket
        Dim thdstHandler As ThreadStart
        Dim thdHandler As Thread

        tcpListener.Start()
        Do
            handlerSocket = tcpListener.AcceptSocket()
            If handlerSocket.Connected Then
                'storing handlersocket info into string for easy access
                info = handlerSocket.RemoteEndPoint.ToString()
                'calling another thread to update listbox
                Me.conThread = New Thread(New ThreadStart(AddressOf Me.displayConnection))
                Me.conThread.Start()
                SyncLock (Me)
                    alSockets.Add(handlerSocket)
                End SyncLock
                thdstHandler = New ThreadStart(AddressOf handlerThread)
                thdHandler = New Thread(thdstHandler)
                thdHandler.Start()
            End If
        Loop
    End Sub

    Public Sub handlerThread()

        Dim handlerSocket As Socket
        handlerSocket = alSockets(alSockets.Count - 1)
        Dim networkStream As NetworkStream = New NetworkStream(handlerSocket)
        Dim blocksize As Int16 = 1024
        Dim thisRead As Int16
        Dim databyte(blocksize) As Byte
        Dim sw As StreamWriter

        sw = Nothing

        Dim saveFileDialog1 As New SaveFileDialog()
        saveFileDialog1.Filter = "textfile|*.txt"
        saveFileDialog1.Title = "Save File"
        saveFileDialog1.ShowDialog()
        savePath = saveFileDialog1.FileName

        SyncLock Me
            'creating a streamwriter to write data read from thisRead into file.
            sw = New StreamWriter(savePath)
            'reading data
            thisRead = networkStream.Read(databyte, 0, blocksize)
            'storing data read into a string
            textData = Encoding.ASCII.GetString(databyte)
            'write data into file
            sw.Write(textData)
            'close streamwriter
            sw.Close()

        End SyncLock
        'calling to another thread to update listbox
        Me.txtThread = New Thread(New ThreadStart(AddressOf Me.ThreadProcSafe))
        Me.txtThread.Start()

        handlerSocket = Nothing


    End Sub
    Private Sub ThreadProcSafe()
        Me.SetText("File Written.")
    End Sub

    Private Sub SetText(ByVal [text] As String)

        If Me.lbConnections.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf SetText)
            Me.Invoke(d, New Object() {[text]})
        Else
            Me.lbConnections.Items.Add(text)
        End If
    End Sub

End Class

thanks so much mafrosis. that'd be a really great help. =)
 
Solution

Ok tonll, I played around with your Server code and got it working - but I changed some stuff a lot and have a few comments:

1) We fix the exception you were receiving by calling SetApartmentState() on the new thread when we create it. ;)

2) When a connection comes in you are gonna pop up a save box on the server - do you really want to do that everytime?!!?

3) You dont need all those threads you were creating. Really, you just have 2 core threads. One thread listens for incoming connections and one handles the main form window. All other threads are spawned to deal with incoming socket connections.

For that reason I renamed the Delegate you had at the top, and used the Me.Invoke mechanism to call cross-thread. If you look at the SetText method you can see it invokes our delegate. This points at our method AddToConnections, which executes on the Form1 thread.

This means you can call SetText from any thread in your code and it will correctly update the main form.

4) When the Server application ends, you arent closing your listening socket. If you look how I wrapped your Do..While in a Try..Catch, we have a mechanism there to handle SocketExceptions - and then in FormClosing we can force the connection to Close(). As I commented, you should also close any open sockets at that time as well.

I think thats everything, but post again if missed something or really dont make sense. :D

mafro



VB.NET:
Imports System.IO
Imports System.Threading
Imports System.Text
Imports System.Net
Imports System.Net.Sockets
Imports System.ComponentModel
Imports System.Windows.Forms

Public Class Form1
    Private alSockets As ArrayList
    
    Dim tcpListener As TcpListener
    
    Public textData As String
    Private showSave As Thread
    Public savePath As String
    Private info As String
    
    'declare our delegate
    Public Delegate Sub SetTextDelegate(ByVal text As String)
    'create an instance of the delegate pointing at our update method
    Public SetTextInstance As New SetTextDelegate(AddressOf AddToConnections)
    
    
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        textData = ""
        savePath = ""
        Dim IPHost As IPHostEntry
        IPHost = Dns.GetHostEntry(Dns.GetHostName())
        lblStatus.Text = "IP address: " + IPHost.AddressList(0).ToString()
        alSockets = New ArrayList()
        
        'start listener thread
        Dim thdListener As New Thread(New ThreadStart(AddressOf listenerThread))
        thdListener.Start()
        
        lbConnections.Items.Add("Waiting for connection..")
    End Sub
    
    
    Private Sub Form1_FormClosing(ByVal sender As System.Object, ByVal e As FormClosingEventArgs) Handles MyBase.FormClosing
        'interrupt the socket listener here!
        tcpListener.Stop()
    End Sub
    
    
    Public Sub listenerThread()
        tcpListener = New TcpListener(IPAddress.Any, 8080)
        Dim handlerSocket As Socket
        Dim thdHandler As Thread

        tcpListener.Start()
        
        Try
            Do
                handlerSocket = tcpListener.AcceptSocket()
                If handlerSocket.Connected Then
                    'storing handlersocket info into string for easy access
                    info = handlerSocket.RemoteEndPoint.ToString()
                    
                    'calling another thread to update listbox
                    Me.SetText("Received connection")
                    
                    'there is no need to SyncLock here as we only have 
                    'one thread accessing alSockets
                    alSockets.Add(handlerSocket)
                    
                    'spawn another thread to handle the connection
                    thdHandler = New Thread(AddressOf handlerThread)
                    'set the apartment state of the new thread!!!
                    thdHandler.SetApartmentState(ApartmentState.STA)
                    thdHandler.Start()
                End If
            Loop

        Catch ex As SocketException
            'do something
        Finally
            'on error close all sockets and listeners
            tcpListener.Stop()
            
            'you need to loop through alSockets and close all connections
        End Try
    End Sub

    Public Sub handlerThread()
        Dim handlerSocket As Socket
        handlerSocket = alSockets(alSockets.Count - 1)
        
        Dim networkStream As NetworkStream = New NetworkStream(handlerSocket)
        Dim blocksize As Int16 = 1024
        Dim thisRead As Int16
        Dim databyte(blocksize) As Byte
        Dim sw As StreamWriter

        Dim saveFileDialog1 As New SaveFileDialog()
        saveFileDialog1.Filter = "textfile|*.txt"
        saveFileDialog1.Title = "Save File"
        saveFileDialog1.ShowDialog()
        savePath = saveFileDialog1.FileName

        SyncLock Me
            'creating a streamwriter to write data read from thisRead into file.
            sw = New StreamWriter(savePath)
            'reading data
            thisRead = networkStream.Read(databyte, 0, blocksize)
            'storing data read into a string
            textData = Encoding.ASCII.GetString(databyte)
            'write data into file
            sw.Write(textData)
            'close streamwriter
            sw.Close()
        End SyncLock
        
        'call cross-thread to update listbox
        Me.SetText("File Written.")

        handlerSocket.Close()
    End Sub
    
    
    ''' <summary>Call this method to update listbox cross-thread</summary>
    ''' <param name="text">Text to update</param>
    Private Sub SetText(ByVal text As String)
        Me.Invoke(SetTextInstance, New Object() {text})
    End Sub
    
    Private Sub AddToConnections(ByVal text As String)
        'when this method executes it will be on the Form1 thread
        'because we use Me.Invoke and the delegate to call cross-thread
        Me.lbConnections.Items.Add(text)
    End Sub
        

End Class
 
Yay.

whoa mafro, thats SWEEET!!! you totally cleaned up my code. looks so much better. i did think i was overdoing the delegates declaration here and there but since i didnt really know how it worked i thought it better to leave it as it is since it was working until i had that STAthread exception. i actually did the savefiledialog.showdialog() at the start of the program but i wasnt happy with it. the first version was a fixed file path in which the file was overwritten everytime i sent something over. so i thought maybe if i let the user decide where he wanted the file to be saved and the name of the file.. it'll be better.
since you raised this,
2) When a connection comes in you are gonna pop up a save box on the server - do you really want to do that everytime?!!?
, good point. i'll probably improve it to something like a log file.. so i add stuff into the file rather than overwriting it. and save the popping of savefiledialog which the user will.. probably get annoyed of after a while. heh.:D but now that i know how to do that from another thread, it'll help me out a whole lot when i write other applications. thanks. you taught me more here than i could find in msdn or anywhere else. ;) and this is one PERFECT example
thdHandler.SetApartmentState(ApartmentState.STA)
they didnt have this ANYWHERE. the only thing i could find was that <STAThread()>. so thankyouthankyou.
4) When the Server application ends, you arent closing your listening socket. If you look how I wrapped your Do..While in a Try..Catch, we have a mechanism there to handle SocketExceptions - and then in FormClosing we can force the connection to Close(). As I commented, you should also close any open sockets at that time as well.
no wonder the debugger was still running even though i closed the application. so thats what was missing...
all in all.. you've been a great help. i cant thank you enough. i'll do some major detailed comparisons on where i went wrong.
God bless you. Have a nice day at work.;)
 
they didnt have this ANYWHERE. the only thing i could find was that <STAThread()>
They have it everywhere, and I told you in post 6. Sadly sometimes it doesn't help to tell people the answer, because they don't possess the knowledge to understand it.
First, your 'Stathread' attribute has no effect on other methods than Sub Main. If you wish you must set this apartment state on the thread with Thread.SetApartmentState method before starting it.
 
i guess i got a lesson on life as well. thanks John. I'll keep that in mind. I'll make sure nobody gets to say that to me again. Thanks for your help. =) have a nice day.
 
Back
Top