When to expect a `Cross-thread Exception`? - vb.net

I'm having a hard time trying to understand what seems to be a random throwing of cross-thread exceptions.
Examples
When invoked in a different thread, why does this work:
Dim text As String = Me.Text
While this will throw an exception:
Me.Text = "str"
What makes it even stranger is that the following do work:
Dim text As String = Me.ctl.Margin.ToString() : Me.ctl.Margin = New Padding(1, 2, 3, 4)
Dim text As String = Me.ctl.MyProp : Me.MyProp = "str"
Note
Yes, I know that I could just invoke the property like this:
Me.Invoke(Sub() Me.Text = "str")
Question
So when can I expect a cross-thread exception?
Code
This is the code i used to test the Me.Text property:
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.ctl = New Control()
Me.ctl.Text = "test_control"
Me.Controls.Add(Me.ctl)
End Sub
Private Sub TestGet(sender As Object, e As EventArgs) Handles Button1.Click
Dim t As New Thread(AddressOf Me._Proc)
t.Start(TESTTYPE.GET)
End Sub
Private Sub TestSet(sender As Object, e As EventArgs) Handles Button2.Click
Dim t As New Thread(AddressOf Me._Proc)
t.Start(TESTTYPE.SET)
End Sub
Private Sub _Proc(tt As TESTTYPE)
Dim text As String = String.Empty
Dim [error] As Exception = Nothing
Try
If (tt = TESTTYPE.GET) Then
text = Me.ctl.Text
ElseIf (tt = TESTTYPE.SET) Then
Me.ctl.Text = "test"
End If
Catch ex As Exception
[error] = ex
End Try
Me.Invoke(Sub() Me._Completed(tt, text, [error]))
End Sub
Private Sub _Completed(tt As TESTTYPE, text As String, ByVal [error] As Exception)
If ([error] Is Nothing) Then
If (tt = TESTTYPE.GET) Then
MessageBox.Show(String.Concat("Success: '", text, "'"), tt.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information)
ElseIf (tt = TESTTYPE.SET) Then
MessageBox.Show("Success", tt.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
Else
MessageBox.Show([error].Message, tt.ToString(), MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Sub
Private ReadOnly ctl As Control
Private Enum TESTTYPE
[GET] = 0
[SET] = 1
End Enum
End Class
Edit
This will not throw an exception:
Public Event TestChanged As EventHandler
Public Property Test() As String
Get
Return Me.m_test
End Get
Set(value As String)
If (value <> Me.m_test) Then
Me.m_test = value
Me.Invalidate()
RaiseEvent TestChanged(Me, EventArgs.Empty)
End If
End Set
End Property

the main time a cross-thread exception occurs when you do something that would cause an event to fire from the non-UI thread that affects the UI thread; So reading a property can be fine, but writing the property of a control would cause it to repaint (at the very least), hence the exception.
Of course, other vendors may have used the exception for other scenarios where it is not safe to access from a different thread

So when can I expect a cross-thread exception?
Well, GUI in .Net are created in STA which means that only the thread that create the control can update it this has to do with Thread-safe concept. for this reasons when you start another thread and try to access the control which is owned by the main thread you will get an invalidOperationException

So when can I expect a cross-thread exception?
Really simple, when you access some function or property of a control, from a thread which do not have right to access it.
For example, in Window form application when you try to access the button placed on form from a non-ui thread, i.e. not the main thread, (and you have not set any flags manually to allow cross-thread operation)
EDIT As per comment, how can I know I can/can not access a getter/ setter of a property. Where are the access rights defined? you can always be on safe side by querying the control's InvokeRequired property in Windows

Okay, so I did some research myself, and it turns out that the exception originates in the .Handle property of the Control.
<Browsable(False), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DispId(-515), SRDescription("ControlHandleDescr")> _
Public ReadOnly Property Handle As IntPtr
Get
If ((Control.checkForIllegalCrossThreadCalls AndAlso Not Control.inCrossThreadSafeCall) AndAlso Me.InvokeRequired) Then
Throw New InvalidOperationException(SR.GetString("IllegalCrossThreadCall", New Object() {Me.Name}))
End If
If Not Me.IsHandleCreated Then
Me.CreateHandle()
End If
Return Me.HandleInternal
End Get
End Property
System.Windows.Forms.resources:
IllegalCrossThreadCall=Cross-thread operation not valid: Control '{0}' accessed from a thread other than the thread it was created on.
So I believe the answer would be something like this:
Whenever the handle of a control is invoked from a different thread.
(I'll give you all a vote up as all the answers are relevant towards cross-threading.)
The following code will throw an exception:
Public Class Form1
Private Sub Test(sender As Object, e As EventArgs) Handles Button1.Click
Dim t As New Thread(AddressOf Me._Proc)
t.Start()
End Sub
Private Sub _Proc(id As Integer)
Dim [error] As Exception = Nothing
Try
Dim p As IntPtr = Me.Handle
Catch ex As Exception
[error] = ex
End Try
Me.Invoke(Sub() Me._Completed([error]))
End Sub
Private Sub _Completed(ByVal [error] As Exception)
If ([error] Is Nothing) Then
MessageBox.Show("Success", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Information)
Else
MessageBox.Show([error].Message, Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
Me.tbDescription.Text = [error].Message
End If
End Sub
End Class

Related

using invoke in a class rather than in a form

i wrote a program reading a com port for a signal, everything was working fine, but they wanted to make the application a service so i swapped the application type to 'windows service' and created a class and put everything in the form in there and i called the class in my Main() in the startup module. the line,
Me.Invoke(New myDelegate(AddressOf UPdateVariable), New Object() {})
in the class has invoke in red saying that, "'Invoke, is not a member of Moisture.Moisture.'" and the "Me" part of that line is no longer greyed out as it was in the form. it worked before dont know what made the difference.
this is the whole code for that class
Imports System
Imports System.IO.Ports
Imports System.Net.Mime
Public Class Moisture
Dim WithEvents serialPort As New IO.Ports.SerialPort
Public Delegate Sub myDelegate()
Public RawString As New System.Text.StringBuilder
Public value As String
Public Sub StartListening()
If serialPort.IsOpen Then
serialPort.Close()
End If
Try
With serialPort
.PortName = "COM3"
.BaudRate = 9600
.Parity = Parity.None
.StopBits = StopBits.One
.DataBits = 8
.Handshake = Handshake.None
.RtsEnable = True
End With
serialPort.Open()
Catch ex As Exception
End Try
End Sub
Private Sub serialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort.DataReceived
Me.Invoke(New myDelegate(AddressOf UPdateVariable), New Object() {})
End Sub
Public Sub UPdateVariable()
With RawString
.Append(serialPort.ReadLine())
End With
If RawString.ToString().Count(Function(x As Char) x = "%"c) = 2 Then
PiTagUpdater(StringParser(RawString.ToString()))
RawString.Clear()
End If
End Sub
Public Function StringParser(RawString As String) As String
Dim Moisture = RawString
Dim value As String
Dim values As String() = Moisture.Split(New Char() {":"c})
value = values(values.Length - 1).Trim({" "c, "%"c})
Return value
End Function
Private Sub PiTagUpdater(Value As Decimal)
Try
Dim piserver As New PCA.Core.PI.PIServer(PCA.Core.Globals.Applications.Application("GENERAL").ConfigValues.ConfigValue("PI_SERVER_NAME").StringValue, PCA.Core.Globals.Applications.Application("GENERAL").ConfigValues.ConfigValue("PI_SERVER_UID").GetDeCryptedStringValue, PCA.Core.Globals.Applications.Application("GENERAL").ConfigValues.ConfigValue("PI_SERVER_PASSWD").GetDeCryptedStringValue, True)
Dim TimeStamp As DateTime = FormatDateTime(Now)
Dim RapidRingCrush = "M1:RapidRingCrush.T"
Try
piserver.WriteValue(RapidRingCrush, Value, TimeStamp)
Catch ex As Exception
MessageBox.Show("Error occured locating Pi Tag", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Application.Exit()
End Try
Catch ex As Exception
MessageBox.Show("Cannot connect to Pi Server")
End Try
End Sub
End Class
Me refers to the current object, in this case the instance of your class. Your class doesn't have an Invoke() method (like the error says), though anything that derives from System.Windows.Forms.Control does (for instance a Form).
Control.Invoke() is used to move the execution of a method to the same thread that the control was created on. This is used to achieve thread-safety.
Since you switched to a service it is fair to assume that you do not have a user interface, thus there's no need to invoke. Removing the Invoke() call and calling the method manually should be enough:
Private Sub serialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort.DataReceived
UPdateVariable()
End Sub
EDIT:
As LarsTech says, you also have to switch the message boxes to some kind of logging methods. A Windows Service usually runs under a different account, which means that it cannot display a user interface to the current user.

global variable defined in winforms not working as expected

I have a project in VB.Net that requires a global variable (a list of event log objects) throughout. This seemed simple enough, just define the variable and initialize in the Application's StartUp event, then access it throughout the application. Unfortunately, this only seems to work when there are no child processes (they have no access to the global variable) - so I'm way over my head as how to access the global variable from the child worker processes (if that is even possible).
The program starts several Test worker processes (that check multiple DB connections from different sources, remote web services from several sources, network checks, etc) w/ progress bars for each. If an error occurs during any of these tests, the error needs to be logged.
The problem is that, the program cannot log events to the Windows Event system because it won't be running under an administrator account (so logging there is not possible thanks to MS's decision to prevent logging under normal user accounts w/Vista,7,8,10), the program also can't log to a text file due to it being asynchronous and the file access contention problems (immediately logging an event to a text file won't work), so I wish to log any events/errors in memory (global variable), THEN dump it to a log file AFTER all child processes complete. Make any sense?
I created a class called AppEvent
Public Class AppEvent
Sub New()
EventTime_ = Date.Now
Level_ = EventLevel.Information
Description_ = String.Empty
Source_ = String.Empty
End Sub
Private EventTime_ As Date
Public Property EventTime() As Date
Get
Return EventTime_
End Get
Set(ByVal value As Date)
EventTime_ = value
End Set
End Property
Private Level_ As EventLevel
Public Property Level() As EventLevel
Get
Return Level_
End Get
Set(ByVal value As EventLevel)
Level_ = value
End Set
End Property
Private Description_ As String
Public Property Description() As String
Get
Return Description_
End Get
Set(ByVal value As String)
Description_ = value
End Set
End Property
Private Source_ As String
Public Property Source() As String
Get
Return Source_
End Get
Set(ByVal value As String)
Source_ = value
End Set
End Property
End Class
Public Enum EventLevel
[Information]
[Warning]
[Error]
[Critical]
[Fatal]
End Enum
And create a public variable just for this (and add an initial event to the AppEvents list)
Namespace My
Partial Friend Class MyApplication
'global variable here (using this for logging asynch call errors, then dumping this into a log file when all asynch calls are complete (due to file contention of log file)
Public AppEvents As New List(Of AppEvent)
Private Sub MyApplication_Startup(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) Handles Me.Startup
'create first event and add it to the global variable declared above
AppEvents.Add(New AppEvent With {.EventTime = Now, .Description = "Program Started", .Level = EventLevel.Information, .Source = "QBI"})
End Sub
End Class
End Namespace
Next, in my logging class, I have some methods for logging, flushing/writing the event(s)
Public Class AppLogging
Public Shared Sub WriteEventToAppLog(evt As AppEvent)
LogDataToFile(FormatLineItem(evt.EventTime, evt.Level, evt.Description, evt.Source))
End Sub
Public Shared Sub WriteEventsToAppLog(AppEvents As List(Of AppEvent))
Dim sbEvents As New StringBuilder
If AppEvents.Count > 0 Then
For Each evt In AppEvents
sbEvents.AppendLine(FormatLineItem(evt.EventTime, evt.Level, evt.Description, evt.Source))
Next
LogDataToFile(sbEvents.ToString.TrimEnd(Environment.NewLine))
End If
End Sub
Private Shared Function FormatLineItem(eventTime As Date, eventLevel As EventLevel, eventDescr As String, eventSource As String) As String
Return String.Format("Logged On: {0} | Level: {1} | Details: {2} | Source: {3}", eventTime, System.Enum.GetName(GetType(EventLevel), eventLevel).Replace("[", "").Replace("]", ""), eventDescr, eventSource)
End Function
Private Shared Sub LogDataToFile(eventLogText As String, Optional ByVal LogFileName As String = "Error.log", Optional ByVal HeaderLine As String = "****** Application Log ******")
'log file operations
Dim LogPath As String = System.AppDomain.CurrentDomain.BaseDirectory()
If Not LogPath.EndsWith("\") Then LogPath &= "\"
LogPath &= LogFileName
Dim fm As FileMode
Try
If System.IO.File.Exists(LogPath) Then
fm = FileMode.Append
Else
fm = FileMode.Create
eventLogText = HeaderLine & Environment.NewLine & eventLogText
End If
Using fs As New FileStream(LogPath, fm, FileAccess.Write)
Using sw As New StreamWriter(fs)
sw.WriteLine(eventLogText)
End Using
End Using
My.Application.AppEvents.Clear() 'clears the global var
Catch ex As Exception
'handle this
End Try
End Sub
Public Shared Sub WriteEventToMemory(eventLevel As EventLevel, eventDescription As String, Optional eventSource As String = "")
Dim evt As New AppEvent
evt.Description = eventDescription
evt.Level = eventLevel
evt.EventTime = Now
evt.Source = eventSource
Try
My.Application.AppEvents.Add(evt)
Catch ex As Exception
Throw
End Try
End Sub
Public Shared Sub FlushEventsToLogFile()
WriteEventsToAppLog(My.Application.AppEvents)
End Sub
End Class
There's a few methods in here, but the method called in every exception handler is WriteEventToMemory (it merely adds an AppEvent to the AppEvents list).
An example test routine/worker process (to the local database) looks like:
#Region "local db test"
Private Sub TestLocalDBWorkerProcess_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestLocalDBWorkerProcess.DoWork
Me.TestLocalDBStatusMessage = TestLocalDB()
End Sub
Private Sub TestLocalDBWorkerProcess_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles TestLocalDBWorkerProcess.RunWorkerCompleted
Me.prgLocalDatabase.Style = ProgressBarStyle.Blocks
Me.prgLocalDatabase.Value = 100
If Me.ForcedCancelTestLocalDB Then
Me.lbStatus.Items.Add("Local DB Test Cancelled.")
Else
If TestLocalDBStatusMessage.Length > 0 Then
Me.lblLocalDatabaseStatus.Text = "Fail"
Me.lbStatus.Items.Add(TestLocalDBStatusMessage)
SendMessage(Me.prgLocalDatabase.Handle, 1040, 2, 0) 'changes color to red
Else
Me.lblLocalDatabaseStatus.Text = "OK"
Me.lbStatus.Items.Add("Connection to local database is good.")
Me.prgLocalDatabase.ForeColor = Color.Green
End If
End If
Me.ForcedCancelTestLocalDB = False
Me.TestLocalDBProcessing = False
ProcessesFinished()
End Sub
Private Sub StartTestLocalDB()
Me.prgLocalDatabase.Value = 0
Me.prgLocalDatabase.ForeColor = Color.FromKnownColor(KnownColor.Highlight)
Me.prgLocalDatabase.Style = ProgressBarStyle.Marquee
Me.TestLocalDBProcessing = True
Me.TestLocalDBStatusMessage = String.Empty
Me.lblLocalDatabaseStatus.Text = String.Empty
TestLocalDBWorkerProcess = New System.ComponentModel.BackgroundWorker
With TestLocalDBWorkerProcess
.WorkerReportsProgress = True
.WorkerSupportsCancellation = True
.RunWorkerAsync()
End With
End Sub
Private Function TestLocalDB() As String
Dim Statusmessage As String = String.Empty
Try
If Me.TestLocalDBWorkerProcess.CancellationPending Then
Exit Try
End If
If Not QBData.DB.TestConnection(My.Settings.DBConnStr3) Then
Throw New Exception("Unable to connect to local database!")
End If
Catch ex As Exception
Statusmessage = ex.Message
AppLogging.WriteEventToMemory(EventLevel.Fatal, ex.Message, "TestLocalDB")
End Try
Return Statusmessage
End Function
#End Region
The try-catch block simply catches the exception and writes it to memory (I just wrapped it in the WriteEventToMemory method, but it's just adding it to the AppEvents list: My.Application.AppEvents.Add(evt)
Everything appeared to be working peachy, until I noticed that the count for AppEvents was (1) after the Startup event, then it's count was (0) from any of the child processes, finally, the count was (1) when the list was dumped to the error log file (only the first event added was there). It is clearly acting like there are multiple versions of the AppEvents variable.
****** Application Log ******
Logged On: 10/7/2016 6:01:45 PM | Level: Information | Details: Program Started | Source: QBI
Only the first event shows up, the other events not (they are added, there's no null ref exceptions or any exceptions - like phantoms). Any event added to the global variable on the MAIN thread stays (and gets logged, ultimately). So this is clearly a multithreaded issue (never tried this before in a Windows app).
Any ideas on how to remedy?
As mentioned above, I had to pass the events back to the calling workerprocess, so, in the main form I put in:
Private AppEvent_TestLocalDB As New AppEvent
In the DoWork (for each process), I changed it to:
Private Sub TestLocalDBWorkerProcess_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestLocalDBWorkerProcess.DoWork
Me.TestLocalDBStatusMessage = TestLocalDB(AppEvent_TestLocalDB)
End Sub
The TestLocalDB sub now looks like:
Private Function TestLocalDB(ByRef aEvent As AppEvent) As String
Dim Statusmessage As String = String.Empty
Try
If Me.TestLocalDBWorkerProcess.CancellationPending Then
Exit Try
End If
If Not QBData.DB.TestConnection(My.Settings.DBConnStr3) Then
Throw New Exception("Unable to connect to local database!")
End If
Catch ex As Exception
Statusmessage = ex.Message
With aEvent
.Description = ex.Message
.Level = EventLevel.Fatal
.Source = "TestLocalDB"
End With
End Try
Return Statusmessage
End Function
Note there is no error logging, just the event variable (ByRef to pass it back, the equivalent to C# out).
When the worker process completes, I add the following:
With AppEvent_TestLocalDB
AppLogging.WriteEventToMemory(.Level, .Description, .Source)
End With
(the other worker processes work the same way)
When ALL the processes are complete, then I flush it to the log file.
AppLogging.FlushEventsToLogFile()
Now the custom event/error log entries look like so (with a freshly made file):
****** Application Log ******
Logged On: 10/7/2016 10:14:36 PM | Level: Information | Details: Program Started | Source: QBI
Logged On: 10/7/2016 10:14:53 PM | Level: Fatal | Details: Unable to connect to local database! | Source: TestLocalDB
That was it - just pass the variable back to the caller

RaiseEvent didn't raise when called by Deserialized function in vb.net

i have a serializable class
<Serializable()> Public Class SACCVar
Private _ConsigneCompression As Integer
Public Event VariableChanged(ByVal Val As Object, ByVal Old_Val As Object, desc As String)
Public Property ConsigneCompression As Integer
Get
Return _ConsigneCompression
End Get
Set(value As Integer)
Tmp_Val = _ConsigneCompression
_ConsigneCompression = value
RaiseEvent VariableChanged(_ConsigneCompression, Tmp_Val, "ConsigneCompression")
End Set
End Property End Class
In a module i declare my variable as class and implement my raise function
Public WithEvents MesuresVal As New MesuresVar
Public Sub SaccVarChanged(ByVal Val, ByVal Old_Value, DictKeyDesc) Handles SaccData.VariableChanged
For Each item As CtrlItem In SaccData.DicOfControl(DictKeyDesc)
Dim pinstance As PropertyInfo = item.Ctrl.GetType.GetProperty(item.prop)
pinstance.SetValue(item.Ctrl, Val)
Next End Sub
In the code when i do
SaccData.ConsigneCompression = 1234
it call SaccVarChanged
but when i call my subroutines that Deserialize my class it pass on the RaiseEvent VariableChanged part of the code in my "public property". But it didn't raise the SaccVarChanged sub.
Is there anything i can do for that?
Thank you
EDIT :
here is my serialize /deserialize code :
Dim fichier As String
fichier = Fichier_SACC
' Déclaration
Dim XSSACC As New XmlSerializer(SaccData.GetType)
Dim streamSACC As FileStream
If Not File.Exists(fichier) Then
'Exit Sub
'TODO gestion erreur
Else
streamSACC = New FileStream(fichier, FileMode.Open)
Try
SaccData = CType(XSSACC.Deserialize(streamSACC), SACCVar)
Catch ex As Exception
' Propagrer l'exception
Throw ex
Finally
' En cas d'erreur, n'oublier pas de fermer le flux en lecture si ce dernier est toujours ouvert
streamSACC.Close()
End Try
End If
Dim StreamSACC As New StreamWriter(Fichier_SACC)
Dim serialiseSACC As New XmlSerializer(SaccData.GetType)
Try
serialiseSACC.Serialize(StreamSACC, SaccData)
Catch ex As Exception
Throw ex
Finally
StreamSACC.Close()
End Try
Ok i finally get it working...
thanks to here : Events not working with after Deserialization
i found that Deserialization remove the handler of the event. It seems that deserialization create a new instance of my object.
The solution give in the link is something like :
<OnDeserialized()>
Private Sub OnDeserializedMethod(ByVal Context As StreamingContext)
AddHandler Child.Changed AddressOf Me.Child_Changed
End Sub
But that didn't work for me as i am using xmlserialiser wich seems not implement with call back (a question of compatibility with framework 1.1 i read...)
Instead i am using a flag that i test in my "new" method.
everything is working as i expect.
Thanks once again #plutonix..

VB.NET accessing class objects from launched thread

I am writing an application in VB.NET that allows users to schedule submissions (emails) to be sent at a later date. I use threads to wait until the time is right to send a particular submission, but for some reason I can't access one of the class objects from the listener threads (or something else is happening, that's what I'm trying to figure out). Here is the relevant code:
Public Class AppContext
Inherits ApplicationContext
Private submsnMngr As SubmissionManager
Public Sub New()
submsnMgr = New SubmissionManager()
menuAddEdit = New ToolStripMenuItem("Add/Edit Submissions")
...
End Sub
Private Sub menuAddEdit_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Handles menuAddEdit.Click
' The user clicking this tray button is the ONLY way that the form can be shown
submsnMngr.ShowWelcome()
End Sub
...
End Class
Class SubmissionManager
Public currentSubmissions As SubmissionList
Public WelcomeForm As Welcome
Public Sub ShowWelcome()
If WelcomeForm Is Nothing Then
' Welcome is the form that needs to be refreshed down in the MailSender subroutine
WelcomeForm = New Welcome(Me)
End If
WelcomeForm.Show()
End Sub
Public Sub CheckDates()
For Each submsn In currentSubmissions.Submissions
SyncLock submsn
If Today.Date >= submsn.EffDate.AddDays(-90).Date And Not submsn.Sent90 And Not submsn.Denied90 And submsn.Thread Is Nothing Then
submsn.Send(1)
submsn.Sent90 = True
currentSubmissions.Save()
ElseIf Today.Date = submsn.EffDate.AddDays(-91).Date And submsn.Thread Is Nothing Then
Dim thd As New Thread(AddressOf MailSender)
thd.IsBackground = True
submsn.Thread = thd
Dim args As New ThreadArgs(submsn.Insured, 1)
thd.Start(args)
End If
If Today.Date >= submsn.EffDate.AddDays(-60).Date And submsn.Thread Is Nothing Then
submsn.Send(2)
currentSubmissions.RemoveSubmission(submsn)
If WelcomeForm IsNot Nothing Then
WelcomeForm.RefreshSubmissions()
End If
ElseIf Today.Date = submsn.EffDate.AddDays(-61).Date And submsn.Thread Is Nothing Then
Dim thd As New Thread(AddressOf MailSender)
thd.IsBackground = True
submsn.Thread = thd
Dim args As New ThreadArgs(submsn.Insured, 2)
thd.Start(args)
End If
End SyncLock
Next
End Sub
Private Sub DateListener()
Do
CheckDates()
Thread.Sleep(3600000)
Loop
End Sub
Private Sub MailSender(args As ThreadArgs)
Dim wait As New TimeSpan(14 - DateTime.Now.Hour, 23 - DateTime.Now.Minute, 0)
Thread.Sleep(wait.TotalMilliseconds)
Dim submsn As Submission = currentSubmissions.GetSubmission(args.insured)
SyncLock submsn
submsn.Send(args.mode)
If args.mode = 1 Then
submsn.Sent90 = True
submsn.Thread = Nothing
currentSubmissions.Save()
Else
currentSubmissions.RemoveSubmission(submsn)
End If
End SyncLock
If WelcomeForm IsNot Nothing Then
' Here is the issue, this code is not being run, even though WelcomeForm is set
' in New() above
WelcomeForm.RefreshSubmissions()
End If
End Sub
End Class
Paying special attention to the few comment lines in the code above, why is WelcomeForm Nothing when I clearly set it to reference the form created in the New() subroutine? I tried alternatively sending the reference to the MailSender thread as an argument, but the same thing happened. Note that I need the If statement there because the user may have closed the form before the thread gets to that point. But it is essential that RefreshSubmissions() be called on it if it is still open.
Sorry guys, realized my thread was being aborted elsewhere in my application's code. No problems with the actual code I posted above.

Form is not updating, after custom class event is fired

I'm having an issue where my main form isn't updating even though I see the event fire off. Let me explain the situation and share some of my code which I'm sure will be horrible since I'm an amateur.
I created a class to take in the settings for running a process in the background. I add some custom events in that class so I could use that in my form instead of a timer.
I put a break on the two subs for that handle those events and I see them get kicked off as soon as an install starts.
I look at the data and it's coming across and no exceptions are thrown.
At first I thought it was because the datagridview had some latency issues. I set that to be double buffered through some tricks I found but it didn't matter. There was still a roughly 10 second delay before the data showed up in the datagrid.
I thought about it and decided I really didn't need a datagridview and replaced the control with a multiline textbox, but it didn't make a difference. It's still taking 10 seconds or longer to show updates to the form/textbox.
I've included some of my code below.
Public Shared WithEvents np As NewProcess
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Try
np = New NewProcess
AddHandler np.InstallFinished, AddressOf np_InstallFinished
AddHandler np.InstallStarted, AddressOf np_InstallStarted
Catch ex As Exception
End Try
End Sub
Protected Sub np_InstallFinished(ByVal Description As String, ByVal ExitCode As Integer)
InstallInProcess = False
If Not Description = Nothing Then
If Not ExitCode = Nothing Then
AddLog(String.Format("Completed install of {0} ({1}).", Description, ExitCode))
Else
AddLog(String.Format("Completed install of {0}.", Description))
End If
End If
RefreshButtons()
UpdateListofApps()
np.Dispose()
End Sub
Protected Sub np_InstallStarted(ByVal Description As String)
InstallInProcess = True
If Not Description = Nothing Then AddLog(String.Format("Started the install of {0}.", Description))
End Sub
Public Class NewProcess
Dim ProcessName As String
Dim ProcessVisibile As Boolean
Dim Arguments As String
Dim WaitforExit As Boolean
Dim Description As String
Dim ShellExecute As Boolean
Dim EC As Integer = Nothing 'Exit Code
Private IsBusy As Boolean = Nothing
Dim th As Threading.Thread
Public Event InstallFinished(ByVal Description As String, ByVal ExitCode As Integer)
Public Event InstallStarted(ByVal Description As String)
Public Function Busy() As Boolean
If IsBusy = Nothing Then Return False
Return IsBusy
End Function
Public Function ExitCode() As Integer
Return EC
End Function
Public Function ProcessDescription() As String
Return Description
End Function
''' <summary>
''' Starts a new multithreaded process.
''' </summary>
''' <param name="path">Path of the File to run</param>
''' <param name="Visible">Should application be visible?</param>
''' <param name="Arg">Arguments</param>
''' <param name="WaitforExit">Wait for application to exit?</param>
''' <param name="Description">Description that will show up in logs</param>
''' <remarks>Starts a new multithreaded process.</remarks>
Public Sub StartProcess(ByVal path As String, ByVal Visible As Boolean, Optional ByVal Arg As String = Nothing, Optional ByVal WaitforExit As Boolean = False, Optional ByVal Description As String = Nothing)
Try
Me.ProcessName = path
Me.ProcessVisibile = Visible
If Arguments = Nothing Then Me.Arguments = Arg
Me.Description = Description
Me.WaitforExit = WaitforExit
If IsBusy And WaitforExit Then
MessageBox.Show("Another install is already in process, please wait for previous install to finish.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Exit Sub
End If
If Not fn_FileExists(ProcessName) Then
MessageBox.Show("Could not find file " & ProcessName & ".", "Could not start process because file is missing.", MessageBoxButtons.OK, MessageBoxIcon.Error)
Exit Sub
End If
th = New Threading.Thread(AddressOf NewThread)
With th
.IsBackground = True
If Not Description Is Nothing Then .Name = Description
.Start()
End With
Catch ex As Exception
End Try
End Sub
Private Sub NewThread()
Dim p As Process
Try
p = New Process
With p
.EnableRaisingEvents = True
.StartInfo.Arguments = Arguments
.StartInfo.FileName = ProcessName
.StartInfo.CreateNoWindow = ProcessVisibile
End With
If ProcessVisibile Then
p.StartInfo.WindowStyle = ProcessWindowStyle.Normal
Else
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden
End If
p.Start()
IsBusy = True
RaiseEvent InstallStarted(Description)
If WaitforExit Then
Do While p.HasExited = False
Threading.Thread.Sleep(500)
Loop
IsBusy = False
RaiseEvent InstallFinished(Description, p.ExitCode)
End If
EC = p.ExitCode
Catch ex As Exception
End Try
End Sub
Public Sub Dispose()
ProcessName = Nothing
ProcessVisibile = Nothing
Arguments = Nothing
WaitforExit = Nothing
Description = Nothing
EC = Nothing
InstallInProcess = Nothing
th.Join()
MemoryManagement.FlushMemory()
End Sub
End Class
Sub AddLog(ByVal s As String)
Try
s = String.Format("[{0}] {1}", TimeOfDay.ToShortTimeString, s)
Form1.tbLogs.AppendText(s & vbCrLf)
Using st As New StreamWriter(LogFilePath, True)
st.WriteLine(s)
st.Flush()
End Using
Catch ex As Exception
End Try
End Sub
Any idea's? I'm at a complete loss.
I've tried adding application.doevents, me.refresh and quite a few other things :(
Form1.tbLogs.AppendText(s & vbCrLf)
Standard VB.NET trap. Form1 is a class name, not a reference to the form. Unfortunately, VB.NET implemented an anachronism from VB6 where that was legal. It however falls apart when you use threads. You'll get another form object automatically created, one that isn't visible because its Show() method was never called. Otherwise dead as a doornail since the thread is not pumping a message loop.
You'll need to pass a reference to the actual form object that the user is looking at to the worker class. The value of Me in the Form1 code. You will also have to use Control.Invoke since it isn't legal to update controls from another thread. I recommend you fire an event instead, one that Form1 can subscribe to, so that your worker class isn't infected with implementation details of the UI.
Some suggestions:
First make it work without threads. (Ironical you call yourself an amateur, It so happened I only learned to do that after I was past the 'amateur' stage)
Don't try to update the GUI Controls from a background thread. That's forbidden in windows. I'm not sure that's what you do (no VB guru), but it sure looks like it.
Use the .net BackgroundWorker class. It has builtin functionality to talk back to the main thread from a background thread. It's not perfect but a good start.
You got me pointed in the right direction. Thanks Hans. This was my solution:
Private Sub SetText(ByVal [text] As String)
If Me.tbLogs.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, New Object() {[text]})
Else
Me.tbLogs.Text = [text]
End If
End Sub
Private Sub np_InstallStarted(ByVal Description As String)
InstallInProcess = True
If Me.tbLogs.Text = "" Then
SetText(String.Format("[{0}] Started the install of {1}.{2}", TimeOfDay.ToShortTimeString, Description, vbCrLf))
Else
SetText(tbLogs.Text & vbCrLf & String.Format("[{0}] Started the install of {1}.{2}", TimeOfDay.ToShortTimeString, Description, vbCrLf))
End If
End Sub
Private Sub np_InstallFinished(ByVal [Description] As String, ByVal [ExitCode] As Integer)
InstallInProcess = False
If Not Description = Nothing Then
If Not ExitCode = Nothing Then
SetText(tbLogs.Text & vbCrLf & String.Format("[{0}] Completed install of {1} ({2}).{3}", TimeOfDay.ToShortTimeString, Description, ExitCode, vbCrLf))
Else
SetText(tbLogs.Text & vbCrLf & String.Format("[{0}] Completed install of {1}.{3}", TimeOfDay.ToShortTimeString, Description, vbCrLf))
End If
End If
RefreshButtons()
UpdateListofApps()
np.Dispose()
End Sub
So when the event kicks off that the install has started or finished, I use the SetText to update the log on the original form.
Problem is I posted that original post as an "unregistered user" so now I'm trying to figure out a way to say the question was answered. Thanks again for your help!