I'm running the below function and it's a bit hit and miss, sometimes it loops back through and runs Call ShowProgressDialog(Nothing, Nothing) other times it just does the If Me.Dispatcher.Thread Is System.Threading.Thread.CurrentThread Then and jumps straight to the Else...
Private Function CloseDownTheApplication() As Task
'-----------------------------------------------------------------------------------------------------------
' Test whether the thread that owns the control of interest is the current thread...
' If it's not, you invoke a delegate referring to the same method via the control of interest.
'-----------------------------------------------------------------------------------------------------------
If Me.Dispatcher.Thread Is System.Threading.Thread.CurrentThread Then
Call ShowProgressDialog(Nothing, Nothing)
Else
Me.Dispatcher.Invoke(New Action(AddressOf CloseDownTheApplication))
End If
End Function
Some additional info - It's initialised from a Background Worker which is initiated from worker.RunWorkerAsync() which works absolutely fine, but it is the reason I need to cross over the threads to run my code.
The ShowProgressDialog runs this code block if it matters:
Private Async Sub ShowProgressDialog(sender As Object, e As RoutedEventArgs)
Dim controller = Await Me.ShowProgressAsync("Please wait...", "We are baking some cupcakes!")
Await Task.Delay(5000)
controller.SetCancelable(False)
Dim i As Double = 0.0
While i < 6.0
Dim val As Double = (i / 100.0) * 20.0
controller.SetProgress(val)
controller.SetMessage("Baking cupcake: " & i & "...")
If controller.IsCanceled Then
Exit While
End If
'canceled progressdialog auto closes.
i += 1.0
Await Task.Delay(2000)
End While
Await controller.CloseAsync()
Me.Close()
End Sub
I've googled it to death and all I can find really is a different way to check using Dispatcher.CheckAccess but this makes no difference. Also I'm on VS Express For Windows Desktop and using VB.Net.
Related
I have a VB function which starts a program as a process and waits for it to complete. It passes a return code of zero if okay or 8 if not. The problem is that it blocks the WPF UI thread and can lead to crashes of Not Enough Quota because the thread has been blocked for too long.
So I’m trying to make it run async so the UI thread isn’t blocked. Unfortunately I have many lines of VB code but all the examples on various web sites these days are C# which I don’t program in and I have far too much code to try and learn C# to convert it all.
I’ve tried Await Task.Run which doesn’t accept parameters. I temporarily removed the Pgm parameter and hardcoded the program name and it will then compile and work. I realise I could use global variables instead but that doesn’t seem good practice.
TaskFactory seems to allow parameters but when I await on StartNew control returns immediately because StartNew creates an outer task and an inner task and the Await only waits for the initial outer task. A C# solution I’ve found suggests using Await Await Task but I can’t seem to convert this to a syntax that VB will accept.
Any help would be appreciated on how I can Await for Startit to complete. I'm using .Net 6 and VS 2022 under Windows 10.
Please excuse any formatting errors. This is my first day on Stack Overflow
The code
Class MainWindow
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
Call StartitAsync()
MsgBox("Returned from StartitAsync")
End Sub
Private Async Function StartitAsync() As Task(Of Integer)
Dim Startup As Func(Of String, Integer) = AddressOf Startit
Dim tf As New TaskFactory
Dim Rc As Integer = Await tf.StartNew(Startup, "notepad.exe")
MsgBox("Returned from await of Startit, RC is " & Rc)
Return Rc
End Function
Private Function Startit(Pgm As String) As Integer
Dim RC As Integer
Dim Startinfo As New ProcessStartInfo
MsgBox("Pgm is " & Pgm)
Startinfo.WindowStyle = ProcessWindowStyle.Maximized ' Display in a maximised window
Startinfo.FileName = Pgm
Startinfo.Arguments = ""
Using PgmProcess As Process = Process.Start(startInfo:=Startinfo) ' Start the program
PgmProcess.WaitForExit() ' Wait until it ends
If PgmProcess.HasExited = True Then ' If the process has exited
RC = PgmProcess.ExitCode ' Save the exit code
Else
RC = 8
End If
End Using
Return RC
End Function
You should use Task.Run instead of Task.Factory.StartNew (or (new TaskFactory()).StartNew). This is true for C# as well as VB.NET.
My VB is extremely rusty, but I believe this is what you're looking for:
Private Async Function StartitAsync() As Task(Of Integer)
Dim Startup = Function() Startit("notepad.exe")
Dim Rc As Integer = Await Task.Run(Startup)
MsgBox("Returned from await of Startit, RC is " & Rc)
Return Rc
End Function
This uses lambda expressions, which are very useful when using APIs like Task.Run.
Side note: You shouldn't call MsgBox from Startit. Since Task.Run executes Startit on the thread pool, it shouldn't access any UI elements (or do things like show message boxes).
I have an application which has a form with a DataGridView bound to a BindingSource, which is bound to a DataTable:
bsList.DataSource = dsData
bsList.DataMember = "List"
dgvList.DataSource = bsList
The underlying data which populates dsData.Tables("List") can change whilst the user is working so to combat this I have a background thread which routinely checks the database for changes and updates dsData.Tables("List"). It also changes the colour of any row where another user is currently working.
However, users report that when this background updating functionality is enabled the application routinely CTDs with no application error message. I have been unable to reproduce this and my attempt to log the crashes via writing to a log file in Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException hasn't worked as the log file is never written to, suggesting this event is never triggered.
The thread is instantiated like this:
LiveUpdating = New Thread(AddressOf UpdateUserLocation) With {.IsBackground = True}
LiveUpdating.Start()
This is the UpdateUserLocation sub:
Public Sub UpdateUserLocation()
Do While My.Settings.customBackgroundUpdating = True And formLoaded = True
UserLocations.Clear()
dtUsers = CLS_USERS.GetUsersSequence(winUser)
dtProgress = DAC.GetProgress()
For Each CandRow As DataRow In dsHHData.Tables("List").Rows
Dim CandReadDate As Date
Dim CandRowNextRead As String = DBNull.Value.ToString
If Not (CandRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(CandRow("NEXT READ").ToString, CandReadDate) Then
CandRowNextRead = CandReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim CandRowSending As String = TryCast(CandRow("SENDING"), String)
Dim CandRowNotes As String = TryCast(CandRow("NOTES"), String)
For Each NewRow As DataRow In dtUsers.Rows
If CandRow("SQ").ToString = NewRow("SQ").ToString Then
UserLocations.Add(NewRow("SQ").ToString)
End If
Next
For Each ProgressRow As DataRow In dtProgress.Rows
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
Dim NextReadDate As Date
Dim ProgressRowNextRead As String = DBNull.Value.ToString
If Not (ProgressRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(ProgressRow("NEXT READ").ToString, NextReadDate) Then
ProgressRowNextRead = NextReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim ProgressRowSending As String = TryCast(ProgressRow("SENDING"), String)
Dim ProgressRowNotes As String = TryCast(ProgressRow("NOTES"), String)
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
If CandRowSending <> ProgressRowSending Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableSending), CandRow, ProgressRowSending)
End If
If CandRowNextRead <> ProgressRowNextRead Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNextRead), CandRow, ProgressRowNextRead)
End If
If CandRowNotes <> ProgressRowNotes Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNotes), CandRow, ProgressRowNotes)
End If
End If
End If
Next
Next
dgv.BeginInvoke(
New MethodInvoker(
Sub()
For Each dgv_row As DataGridViewRow In dgv.Rows
If UserLocations.Contains(dgv_row.Cells("SQ").Value.ToString) Then
dgv.DefaultCellStyle.BackColor = My.Settings.customRowHighlight
Else
dgv.DefaultCellStyle.BackColor = Nothing
End If
Next
End Sub))
Thread.Sleep(My.Settings.customRefreshRate * 1000)
Loop
End Sub
The subs that do the DataTable update are like this:
Private Delegate Sub UpdateDataTableDelegate(ByVal CandRow As DataRow, ByVal ProgressRow As String)
Private Sub UpdateDataTableSending(ByVal CandRow As DataRow, ByVal ProgressRowSending As String)
CandRow("SENDING") = ProgressRowSending
End Sub
I know this is not the best way to handle a multi-user environment but the nature of this work requires that all people can access and see the same data. I could force them to refresh regularly but that seems very intrusive.
The crashes only occur when this thread is running and the crashes are regular (and not instant) but I cannot seem to reproduce them and the application is very stable otherwise.
There must be some cross-threading issue but I can't work how when all of the updates to the DataTable or DataGridView are done via a BeginInvoke on the main UI thread.
EDIT: I've just realised that even though I am doing the queries and most of the heavy lifting in the background thread, the updates are stilled called on the main UI thread which would lock the thread. This would be particularly noticeable if there were a lot of updates... Because each one is called individually.
If the UI lock up was long enough, and the user was clicking on stuff, would this cause Windows to treat the application as unresponsive and crash it? If so, is there a better way I could handle these updates?
Any help with resolving this would be enormously appreciated.
So there's an application at my work that installs several Windows services to a server. As a side project, I've been asked to make a simple GUI that will list these services with a "light" (a picture box with a red or green dot) next to the name of each service. The idea is that in the event these services were to stop running, the "light" would change from green to red.
I have the GUI part built, and I can query a remote server's services, then compare it to an array of the ones I'm interested and set the "light" next to each service to green/red depending on the service state. The part I'm hung up on is how to monitor these services in real time? Currently, I just have the following code in the Form_Load event:
Dim myConnectionOptions As New System.Management.ConnectionOptions
With myConnectionOptions
.Impersonation = System.Management.ImpersonationLevel.Impersonate
.Authentication = System.Management.AuthenticationLevel.Packet
End With
Try
Dim myManagementScope As System.Management.ManagementScope
myManagementScope = New System.Management.ManagementScope("\\" & SERVERNAME & "\root\cimv2", myConnectionOptions)
myManagementScope.Connect()
Dim query As New Management.ObjectQuery("SELECT * FROM Win32_Service")
Dim searcher As New Management.ManagementObjectSearcher(myManagementScope, query)
Dim i As Integer = 0
For Each queryObj As Management.ManagementObject In searcher.Get()
For Each service As String In arrServices
If queryObj("DisplayName").Equals(service) Then
If queryObj("State").Equals("Stopped") Then
arrLights(i).Image = My.Resources.redlight
End If
i += 1
End If
Next
Next
Catch err As Management.ManagementException
MessageBox.Show("WMI query failed with the following error: " & err.Message)
Catch unauthorizedErr As System.UnauthorizedAccessException
MessageBox.Show("Authentication error: " & unauthorizedErr.Message)
End Try
Would a simple timer that executes this code repeatedly be the best approach, or is there a more elegant solution? I have a little experience in VB.NET and WMI, but none in any type of real-time monitoring activity like this.
First of all i would put it into a thread, that way even if your connection times out you dont freeze your UI, then i would use a custom wait timer not the built in one as cross threading can be a pain.
wait timer:
Public Sub Wait(ByVal wait_time As Integer)
Dim time As Date
time = Now.AddMilliseconds(wait_time)
Do While time > Now
Application.DoEvents()
Loop
End Sub
example of threading:
Private services_check As Thread
private sub form1_load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
services_check = new thread(AddressOf 'Current code in a public sub')
services_cheack.IsBackground = True
Services_check.start()
It may not be the most elegant solution but its how i would do it, as for your current code im sorry i dont know enough about remote connections to help you.
I have a weird exception that I can't seem to debug. For anyone about to suggest worker threads I am prohibited from using them at my work... I asked why not myself and received a vague answer and clear directions to avoid it... anyways...
I decided I wanted to have a progress bar on a separate form that is initialized and shown directly from my library class (independent of the main form and executing on a different thread). Even though this class initializes the form itself, I am using control.InvokeRequired and control.Invoke anyway so I can reuse the form elsewhere.
The following is for initializing the progress bar:
Public Sub InitializePB(ByVal _Maximum As Integer, ByVal _Step As Integer, ByVal _StartValue As Integer, ByVal _Style As Windows.Forms.ProgressBarStyle)
If Me.InvokeRequired Then
'Different thread - invoke delegate
pb_Progress.Invoke(Sub() InitializePB(_Maximum, _Step, _StartValue, _Style))
Else
'Same thread - set values
pb_Progress.Minimum = 1
pb_Progress.Maximum = _Maximum
pb_Progress.Step = _Step
pb_Progress.Value = _StartValue
pb_Progress.Style = _Style
Me.Refresh()
End If
End Sub
Now for the step function:
Public Sub PerformStepPB()
If Me.InvokeRequired Then
'Different thread - invoke delegate
pb_Progress.Invoke(Sub() PerformStepPB())
Else
'Same thread - perform step
pb_Progress.PerformStep()
Me.Refresh()
End If
End Sub
Now the following is test code from where I launch form containing the progress bar and then send step calls:
pbForm = New frmProgress(_OwnerDesktopLoc)
pbForm.Show()
pbForm.InitializePB(100, 1, 1, Windows.Forms.ProgressBarStyle.Blocks)
Dim msElapsedVals As Integer = 0
While msElapsedVals <= 100
pbForm.PerformStepPB()
Thread.Sleep(100)
msElapsedVals += 1
End While
pbForm.Close()
pbForm.Dispose()
While Not pbForm.IsDisposed
Thread.Sleep(100)
End While
Exit Sub
So far so good... you will notice that I call the Form.Refresh method for the progress bar form every time the step method is called. I ended up doing this because when the form launched it would crash after several progress bar steps. At this point in time the form does show without crashing (because of the refresh) except that if I click the form it crashes (and no exception is caught by Visual Studio). The weird thing is that the code from the library class for updating the progress bar continues without any problems as if the problem is simply with the Win32 window. Here is the Windows message associated with the crash:
Description:
A problem caused this program to stop interacting with Windows.
Problem signature:
Problem Event Name: AppHangB1
Application Name: DL_RDS_Sort.vshost.exe
I tried launching the form from the main form thread but with the same results. I have no clue what's going on... any help would be greatly appreciated =D
Update on exactly how things are being called:
From MainForm a thread is started with the address of a subroutine defined within a library class :
Private Sub but_Sort_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles but_Sort.Click
Me.Sorter.OwnerDesktopLoc = New Drawing.Point(Me.DesktopLocation.X + CInt(Me.DesktopBounds.Width / 6), Me.DesktopLocation.Y + CInt(Me.DesktopBounds.Height / 2))
'Start sorting thread
Me.sortThread = New Threading.Thread(AddressOf Me.Sorter.Sort)
Me.sortThread.Start()
Then in this thread's routine I am testing the initialization of the form and calling the methods (shown earlier) I wrote for updating the performance bar properties:
Public Sub Sort()
Try
IIf(_OwnerDesktopLoc = Nothing, useProgressBar = False, useProgressBar = True)
'TESTING//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
pbForm = New frmProgress(_OwnerDesktopLoc)
pbForm.Show()
pbForm.InitializePB(100, 1, 1, Windows.Forms.ProgressBarStyle.Blocks)
Dim msElapsedVals As Integer = 0
While msElapsedVals <= 100
pbForm.PerformStepPB()
Thread.Sleep(100)
msElapsedVals += 1
End While
pbForm.Close()
pbForm.Dispose()
While Not pbForm.IsDisposed
Thread.Sleep(100)
End While
Exit Sub
'TESTING//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Note that there is a lot more code following the testing area but it never gets called and does not play a part in this issue because I exit the subroutine. I normally use ShowDialog but this halts the execution which is not what I want.
Some things worth mentioning:
-the frmProgress has the ControlBox property set to false.
-the frmProgress has the FormBorderStyle set to FixedDialog
Also: When I use the Show() method, the Windows "busy circle" cursor appears when I hover my mouse over the shown form. ShowDialog() does not have this behaviour... It's as if the form is waiting for something... Hope this clarifies my problem.
I'm running in to a problem that I was able to fix with Application.DoEvents, but don't want to leave that in because it might introduce all sorts of nasty problems.
Background:
Our app is primarily a desktop app that makes many calls to a web service. We control everything but changes to the overall system design are not going to be seriously considered. One of those calls, Calculate, is used very often, and occasionally can take a few minutes to process all the data to return valid results.
Previously this call to Calculate was done synchronously and thus would block the UI leaving the user to wonder if the app had frozen or not, etc. I've successfully moved all the long wait calls to a BackgroundWorker and then made a simple Waiting screen that would cycle through a "Calculating..." animated message.
Now the problem arises when our UI code tries to call the calculate routine again prior to the first one finishing. I would get a "This BackgroundWorker is currently busy and cannot run multiple instances..." message. Which I thought should be controlled by the resetEvent.WaitOne() calls. It did not so I thought maybe another event controlling access to the entire routine would help, so I added the calcDoneEvent. This still did not fix the problem, but would cause it to block indefinitely on the 2nd call to Calculate's calcDoneEvent.WaitOne() call. Then on a whim I added the Application.DoEvents to the bottom of Calculate and viola, problem solved.
I don't want to leave that .DoEvents in there because I've read it can cause problems that later are very difficult to track down. Is there a better way to handle this situation?
Thanks in advance..
Private WithEvents CalculateBGW As New System.ComponentModel.BackgroundWorker
Dim resetEvent As New Threading.AutoResetEvent(False)
Dim calcDoneEvent As New Threading.AutoResetEvent(True)
Public Sub Calculate()
calcDoneEvent.WaitOne() ' will wait if there is already a calculate running.'
calcDoneEvent.Reset()
' setup variables for the background worker'
CalculateBGW.RunWorkerAsync() ' Start the call to calculate'
Dim nMsgState As Integer = 0
' will block until the backgorundWorker is done'
Do While Not resetEvent.WaitOne(200) ' sleep for 200 miliseconds, then update the status window'
Select Case nMsgState
Case 1
PleaseWait(True, vbNull, "Calculating. ")
Case 2
PleaseWait(True, vbNull, "Calculating.. ")
Case 3
PleaseWait(True, vbNull, "Calculating... ")
Case 4
PleaseWait(True, vbNull, "Calculating....")
Case Else
PleaseWait(True, vbNull, "Calculating ")
End Select
nMsgState = (nMsgState + 1) Mod 5
Loop
PleaseWait(False, vbNull) 'make sure the wait screen goes away'
calcDoneEvent.Set() ' allow another calculate to proceed'
Application.DoEvents() ' I hate using this here'
End Sub
Private Sub CalculateBGW_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles CalculateBGW.DoWork
Try
'make WS Call, do data processing on it, can take a long time..'
'No Catch inside the DoWork for BGW, or exception handling wont work right...'
'Catch'
Finally
resetEvent.Set() 'unblock the main thread'
End Try
End Sub
Private Sub CalculateBGW_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles CalculateBGW.RunWorkerCompleted
'If an error occurs we must check e.Error prior to touching e.Result, or the BGW'
'will possibly "eat" the exception for breakfast (I hear theyre tasty w/ jam)'
If Not (e.Error Is Nothing) Then
'If a Web Exception timeout, retry the call'
If TypeOf e.Error Is System.Net.WebException And _
e.Error.Message = "The operation has timed out" And _
intRetryCount < intRetryMax Then
' Code for checking retry times, increasing timeout, then possibly recalling the BGW'
resetEvent.Reset()
CalculateBGW.RunWorkerAsync() 'restart the call to the WS'
Else
Throw e.Error ' after intRetryMax times, go ahead and throw the error up higher'
End If
Else
Try
'normal completion stuff'
Catch ex As Exception
Throw
End Try
End If
End Sub
You declared:
Private WithEvents CalculateBGW As New System.ComponentModel.BackgroundWorker
Dim resetEvent As New Threading.AutoResetEvent(False)
Dim calcDoneEvent As New Threading.AutoResetEvent(True)
as private fields of the containing class. Notice that this way, all calls to RunWorkerAsync() are referred to the same object instance of the BackgroundWorker class (that is, to the same object). That is why it is "busy". This code is built to hold only one BackgroundWorker at a given time.
If you mean to allow the UI code to call the Calculate() method whenever it needs to, you should declare CalculateBGW as a local variable within the Calculate() method, thus creating a new instance of the BackgroundWorker class with every call (and they will run asynchronosly). This means you'll have to add and remove the event handlers inside Calculate() as well, using AddHandler and RemoveHandler.
There are several approaches to updating the UI on the progress, but it is suggested to use the BackgroundWorker.ProgressChanged event and BackgroundWorker.ReportProgress method.
Use the BackgroundWorker.RunWorkerCompleted event as a callback trigger, reporting the UI that the calculation is completed, thus triggering the needed code to represent the result. This approach eliminates the need to maintain a thread looping around bossing the calculation thread - thereby eliminating the need for DoEvents(). It lets the calculation thread inform its boss when its done working, instead of having the boss checking the worker's status and going to sleep over and over.