I'm trying to write a small Windows Service in .NET 3.5, that check every 10mn or so if thee are new files in "C:\demofolder", then send e-mail out. So far, I made it to here like in the following code, then there's error in Public Sub New()
Imports System
Imports System.Timers
Imports System.ServiceProcess
Public Class TestMyService
' A timer that controls how frequenty the example writes to the
' event log.
Private serviceTimer As Timer
Public Sub New()
' Set the ServiceBase.ServiceName property.
ServiceName = "TestMyService Service"
' Configure the level of control available on the service.
CanStop = True
CanPauseAndContinue = True
CanHandleSessionChangeEvent = True
' Configure the service to log important events to the
' Application event log automatically.
AutoLog = True
End Sub
Protected Overrides Sub OnStart(ByVal args() As String)
' Add code here to start your service. This method should set things
' in motion so your service can do its work.
End Sub
Protected Overrides Sub OnStop()
' Add code here to perform any tear-down necessary to stop your service.
End Sub
Public Shared Sub Main()
' Create an instance of the TestMyService class that will write
' an entry to the Application event log. Pass the object to the
' shared ServiceBase.Run method.
ServiceBase.Run(New TestMyService)
End Sub
End Class
I got the following error message:
Sub Main' is declared more than once in 'mcWinService.TestMyService': mcWinService.TestMyService.Main(), mcWinService.TestMyService.Main()
Public Shared Sub Main()' has multiple definitions with identical signatures.
Public Sub New()' in designer-generated type 'mcWinService.TestMyService' should call InitializeComponent method.
Try to move
ServiceBase.Run(New TestMyService) from Public Shared Sub Main()
to
Protected Overrides Sub OnStart
then remove Public Shared Sub Main()
Also, remove Public Sub New(), because you can set those properties from "Property Windows". F7 to toggle from code to designer view.
Update 1: Example of Windows Service for folder monitor
Imports System.Net.Mail
Imports System.Net
Imports System.Timers
Imports System.IO
Public Class DemoFolderMonitor
Private Shared timer As System.Timers.Timer
Private Shared timerInterval As Integer
Protected Overrides Sub OnStart(ByVal args() As String)
' Add code here to start your service. This method should set things
' in motion so your service can do its work.
' Use the EventLog object automatically configured by the
' ServiceBase class to write to the event log.
'EventLog.WriteEntry(String.Format("DemoFolderMonitor Service starting."))
' Set the Interval (1sec = 1000 milliseconds).
timerInterval = 2000
timer = New System.Timers.Timer(timerInterval)
EventLog.WriteEntry("DemoFolderMonitor Service starting.")
' Hook up the Elapsed event for the timer.
AddHandler timer.Elapsed, AddressOf WatchFolder
timer.Interval = timerInterval
timer.Enabled = True
End Sub
Protected Overrides Sub OnStop()
' Add code here to perform any tear-down necessary to stop your service.
EventLog.WriteEntry("DemoFolderMonitor Service stopping...")
End Sub
Protected Sub WatchFolder()
Dim watcher As New FileSystemWatcher
watcher.Path = "C:\Demo\"
watcher.NotifyFilter = (NotifyFilters.LastAccess Or NotifyFilters.LastWrite Or NotifyFilters.FileName Or NotifyFilters.DirectoryName)
'watch all file types.
watcher.Filter = "*.*"
' Add event handlers.
AddHandler watcher.Changed, AddressOf OnChanged
AddHandler watcher.Created, AddressOf OnChanged
AddHandler watcher.Deleted, AddressOf OnChanged
AddHandler watcher.Renamed, AddressOf OnRenamed
' Begin watching.
watcher.EnableRaisingEvents = True
End Sub
'Define the event handlers.
Private Shared Sub OnChanged(ByVal source As Object, ByVal e As FileSystemEventArgs)
' Specify what is done when a file is changed, created, or deleted.
Dim changeLog = "File: " & e.FullPath & " " & " | Action: " & e.ChangeType.ToString
WriteChangeLog(changeLog)
End Sub
Private Shared Sub OnRenamed(ByVal source As Object, ByVal e As RenamedEventArgs)
' Specify what is done when a file is renamed.
Dim changeLog = "File" & e.OldFullPath & " " & "renamed to" & " " & e.FullPath
WriteChangeLog(changeLog)
End Sub
'Custom action. You can either send an e-mail or write change to local file
'In this example I write change to local file
Private Shared Sub WriteChangeLog(ByVal changeLog As String)
'Create a text file and write the change log to the text file
Dim filename As String = DateTime.Now.ToString("hh-mm-ss") & ".txt"
Dim writer As StreamWriter
writer = File.CreateText("C:\ChangeLog\" & filename)
writer.WriteLine("Datetime Log at {0} Log Message: {1}", DateTime.Now.ToString("MMM-dd-yyyy # hh:mm:ss tt"), changeLog)
writer.Close()
End Sub
End Class
For further related reading check:
Setup project for a Windows Service application
System.Timers Namespace
FileSystemWatcher Class
Cheer!
Sounds like you have another Main method in another of the project files. Do a project wide search for the word Main and see if you can find it.
You can also fix that error by going into the properties dialog for the project and selecting TestMyService as the startup object rather that Sub Main.
Related
I have created a service named "IranStockCodal" and Built and installed it "Sucessfully" on windows services, but when I start It a warning comes on windows services window:
"The "IranStockCodal" service on Local Computer started and then stopped. Some services stop automatically if they are not in use by other service or programs". The task "Call vbIranStock.VBNetLib.vbScript4.VbsRun4(False)" which is called works truly in console application, but when I import and call it from service it does not.
please help me what should be done to solve this.
Imports System.IO
Imports System.Threading
Imports System.Configuration
Imports vbIranStock.VBNetLib.vbScript4
Imports vbIranStock
Public Class Service1
Protected Overrides Sub OnStart(ByVal args() As String)
Me.WriteToFile("Iran Stock Codal started at " + DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt"))
Call vbIranStock.VBNetLib.vbScript4.VbsRun4(False)
Me.WriteToFile("Iran Stock Codal Task Called at " + DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt"))
Me.ScheduleService()
End Sub
Protected Overrides Sub OnStop()
Me.WriteToFile("Iran Stock Codal Service stopped at " + DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt"))
Me.Schedular.Dispose()
End Sub
Private Schedular As Timer
Public Sub ScheduleService()
Try
Schedular = New Timer(New TimerCallback(AddressOf SchedularCallback))
Dim mode As String = ConfigurationManager.AppSettings("Mode").ToUpper()
Me.WriteToFile((Convert.ToString("Iran Stock Codal Service Mode: ") & mode) + " {0}")
'Set the Default Time.
Dim scheduledTime As DateTime = DateTime.MinValue
If mode.ToUpper() = "DAILY" Then
'Get the Scheduled Time from AppSettings.
scheduledTime = DateTime.Parse(System.Configuration.ConfigurationManager.AppSettings("ScheduledTime"))
If DateTime.Now > scheduledTime Then
'If Scheduled Time is passed set Schedule for the next day.
scheduledTime = scheduledTime.AddDays(1)
End If
End If
If mode.ToUpper() = "INTERVAL" Then
'Get the Interval in Minutes from AppSettings.
Dim intervalMinutes As Integer = Convert.ToInt32(ConfigurationManager.AppSettings("IntervalMinutes"))
'Set the Scheduled Time by adding the Interval to Current Time.
scheduledTime = DateTime.Now.AddMinutes(intervalMinutes)
If DateTime.Now > scheduledTime Then
'If Scheduled Time is passed set Schedule for the next Interval.
scheduledTime = scheduledTime.AddMinutes(intervalMinutes)
End If
End If
Dim timeSpan As TimeSpan = scheduledTime.Subtract(DateTime.Now)
Dim schedule As String = String.Format("{0} day(s) {1} hour(s) {2} minute(s) {3} seconds(s)", timeSpan.Days, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds)
Me.WriteToFile((Convert.ToString("Iran Stock Codal Service scheduled to run after: ") & schedule) + " {0}")
'Get the difference in Minutes between the Scheduled and Current Time.
Dim dueTime As Integer = Convert.ToInt32(timeSpan.TotalMilliseconds)
'Change the Timer's Due Time.
Schedular.Change(dueTime, Timeout.Infinite)
Catch ex As Exception
WriteToFile("Iran Stock Codal Service Error on: {0} " + ex.Message + ex.StackTrace)
'Stop the Windows Service.
Using serviceController As New System.ServiceProcess.ServiceController("IranStockCodal")
serviceController.[Stop]()
End Using
End Try
End Sub
Private Sub SchedularCallback(e As Object)
Me.WriteToFile("Iran Stock Codal Service Log: " + DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt"))
Me.ScheduleService()
End Sub
Private Sub WriteToFile(text As String)
Dim path As String = "C:\ServiceLog.txt"
Using writer As New StreamWriter(path, True)
writer.WriteLine(String.Format(text, DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss tt")))
writer.Close()
End Using
End Sub
End Class
Here is Project Installed Codes:
Imports System.ComponentModel
Imports System.Configuration.Install
Public Class ProjectInstaller
Public Sub New()
MyBase.New()
'This call is required by the Component Designer.
InitializeComponent()
'Add initialization code after the call to InitializeComponent
End Sub
Protected Overrides Sub OnAfterInstall(savedState As IDictionary)
MyBase.OnAfterInstall(savedState)
'The following code starts the services after it is installed.
Using serviceController As New System.ServiceProcess.ServiceController(ServiceInstaller1.ServiceName)
serviceController.Start()
End Using
End Sub
Private Sub ServiceInstaller1_AfterInstall(sender As Object, e As InstallEventArgs) Handles ServiceInstaller1.AfterInstall
End Sub
Private Sub ServiceProcessInstaller1_AfterInstall(sender As Object, e As InstallEventArgs) Handles ServiceProcessInstaller1.AfterInstall
End Sub
End Class
Here is ComponentModel codes
<System.ComponentModel.RunInstaller(True)> Partial Class ProjectInstaller
Inherits System.Configuration.Install.Installer
'Installer overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Component Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Component Designer
'It can be modified using the Component Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Me.ServiceProcessInstaller1 = New System.ServiceProcess.ServiceProcessInstaller()
Me.ServiceInstaller1 = New System.ServiceProcess.ServiceInstaller()
'
'ServiceProcessInstaller1
'
Me.ServiceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem
'
'ServiceInstaller1
'
Me.ServiceInstaller1.ServiceName = "IranStockCodal"
Me.ServiceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Automatic
'
'ProjectInstaller
'
Me.Installers.AddRange(New System.Configuration.Install.Installer() {Me.ServiceProcessInstaller1, Me.ServiceInstaller1})
End Sub
Friend WithEvents ServiceProcessInstaller1 As System.ServiceProcess.ServiceProcessInstaller
Friend WithEvents ServiceInstaller1 As System.ServiceProcess.ServiceInstaller
End Class
I have an application whose main window upon click of a button gives users an option to load a list of files in the cloud.
Private Sub ImportCloudContent()
Dim cloudForm As Form_CloudImport
cloudForm = New Form_CloudImport()
cloudForm.Show()
cloudForm.populateDataGrid()
AddHandler cloudForm._DownloadComplete, New EventHandler(AddressOf OpenProject)
cloudForm.DownloadNotifier(FullPathOfContent)
End Sub
Ideally I should be able to get the value of the FullPathOfContent variable and pass it onto Open Project, but I am not sure how to go about it.
In the new Window users can click and download the file they want. Below is the section of code that handles the download in the Form_CloudImport class :
Private Async Sub Btn_download_Click(sender As Object, e As EventArgs) Handles Btn_download.Click
Dim fileNameRows As DataGridViewSelectedRowCollection = datagridview_cloudContent.SelectedRows
Dim fileName As String
Dim fileType As String = Cloud.CONTENT
Dim FullPathOfContent As String
For Each fileNameRow As DataGridViewRow In fileNameRows
fileName = fileNameRow.Cells(0).Value.ToString() & ".zip"
Try
FullPathOfContent = CloudToCCT(fileName, fileType)
Catch ex As Exception
CSMessageBox.ShowError("Content Import failed : ", ex)
End Try
Next
Me.Close()
DownloadNotifier(FullPathOfContent)
End Sub
Once the download is complete, the main window needs to call some of its methods. I am new to VB and have created a custom event to facilitate this(again in the Form_CloudImport class)
Public Event _DownloadComplete(e As String)
Public Sub DownloadNotifier(FullPathOfContent As String)
RaiseEvent _DownloadComplete(FullPathOfContent)
End Sub
According to what have read, once the download method is complete, it will fire the DownloadNotifier method, which will raise the _DownloadComplete event and the MainWindow should trigger the following events.
However, I receive the below errors in the MainWindow part of the code :
Value of type 'MainWindow.EventHandler' cannot be converted to 'Form_CloudImport._DownloadCompleteEventHandler'
and
'FullPathOfContent' is not declared. It may be inaccessible due to its protection level.
This question seems to be very long but any help would be appreciated. Thank you in advance!
First things first, you should create a type and event with proper names and signature and raise it properly.
Public Class CloudImportForm
Public Event DownloadComplete As EventHandler(Of DownloadCompleteEventArgs)
Protected Overridable Sub OnDownloadComplete(e As DownloadCompleteEventArgs)
RaiseEvent DownloadComplete(Me, e)
End Sub
'...
End Class
Public Class DownloadCompleteEventArgs
Inherits EventArgs
Public Sub New(contentPath As String)
Me.ContentPath = contentPath
End Sub
Public ReadOnly Property ContentPath As String
End Class
In that form, you would have code that performed a download and then raised that event.
'...
Dim contentPath = GetContentPath()
'Perform download here.
'Raise event.
OnDownloadComplete(New DownloadCompleteEventArgs(contentPath))
In your main form you would create and configure the download form, which includes handling the event, and then display it.
Dim cloudForm As New CloudImportForm
AddHandler cloudForm.DownloadComplete, AddressOf CloudImportForm_DownloadComplete
cloudForm.PopulateDataGrid()
cloudForm.Show()
The method you specify as the event handler should have the appropriate signature and it should retrieve the content path from the e parameter.
Private Sub CloudImportForm_DownloadComplete(sender As Object, e As DownloadCompleteEventArgs)
Dim contentPath = e.ContentPath
'Use contentPath here.
End Sub
First a little background information: The purpose of this application is to capture images and save them automatically to a network directory that will be either created or appended using the input of the text box. This code DOES work on my computer (windows 7 home 64 bit). I've created it using microsoft visual basic express 2010.
However..... when attempting to run the application on a windows 10 tablet, I get the follow errors:
On form load:
An error occurred while capturing the image. The video capture will now be terminated.
Object reference not set to an instance of an object.
On button2_Click Event:
Object reference not set to an instance of an object.
Below is the entirety of the code.
Form2.vb
Public Class Form2
Public scanIsSet As Boolean
Private webcam As WebCam
Private Sub Form2_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
webcam = New WebCam()
webcam.InitializeWebCam(imgVideo)
webcam.Start()
scanIsSet = False
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim CFGfile As String
Dim SaveDir As String
Dim imgIndex As Integer
Dim existingImages() As String
SaveDir = "C:\somepath\"
'save image to directory with index number
Try
imgCapture.Image.Save(SaveDir & OrderNumber.Text & "\" & CStr(imgIndex) & ".jpg")
Catch ex As Exception
MsgBox("error while accessing object imgCapture" & ex.Message)
End Try
imgIndex = imgIndex + 1
Else
Beep()
MsgBox("Please scan or type in order number first")
End If
End Sub
End Class
WebCam.vb
Imports System
Imports System.IO
Imports System.Linq
Imports System.Text
Imports WebCam_Capture
Imports System.Collections.Generic
Imports ZXing
Imports ZXing.OneD
'Design by Pongsakorn Poosankam
Class WebCam
Public scanz As Boolean
Public Sub setScan(ByVal x As Boolean)
scanz = x
End Sub
Private webcam As WebCamCapture
Private _FrameImage As System.Windows.Forms.PictureBox
Private FrameNumber As Integer = 30
Public Sub InitializeWebCam(ByRef ImageControl As System.Windows.Forms.PictureBox)
webcam = New WebCamCapture()
webcam.FrameNumber = CULng((0))
webcam.TimeToCapture_milliseconds = FrameNumber
AddHandler webcam.ImageCaptured, AddressOf webcam_ImageCaptured
_FrameImage = ImageControl
End Sub
Private Sub webcam_ImageCaptured(ByVal source As Object, ByVal e As WebcamEventArgs)
_FrameImage.Image = e.WebCamImage
If scanz = True Then
Dim BCreader As New ZXing.BarcodeReader
'BCreader.Options.TryHarder = True
Try
Dim resu As Result = BCreader.Decode(e.WebCamImage)
Form2.OrderNumber.Text = resu.Text
setScan(False)
Form2.Label2.Text = ""
Beep()
Catch ex As Exception
'do nothing
End Try
End If
End Sub
Public Sub Start()
webcam.TimeToCapture_milliseconds = FrameNumber
webcam.Start(0)
End Sub
Public Sub [Stop]()
webcam.[Stop]()
End Sub
Public Sub [Continue]()
' change the capture time frame
webcam.TimeToCapture_milliseconds = FrameNumber
' resume the video capture from the stop
webcam.Start(Me.webcam.FrameNumber)
End Sub
Public Sub ResolutionSetting()
webcam.Config()
End Sub
Public Sub AdvanceSetting()
webcam.Config2()
End Sub
End Class
As you can see toward the end of Form2.vb, I've wrapped imgCapture.Image.Save(SaveDir & OrderNumber.Text & "\" & CStr(imgIndex) & ".jpg") in a Try-Catch block because I suspect it's some sort of problems with the pictureBox objects. The try catch block does indeed catch the exception, but I still have no idea what the problem is, why it happens on the tablet and not the PC, or how to fix it.
I've found similar questions, but none with a solution I can make use of.
Since you are using a library, EasyWebCam, that is outdated and not compatible with Win10, I would suggest switching libraries. Other options out there:
DirectX.Capture
Windows.Media.Capture
I FOUND THE SOLUTION BUT I DON'T KNOW IF YOU NEED IT NOW ANYWAY THE PROBLEM IS IF YOU HAVE CHANGED THE PICTUREBOX NAME THEN IN REFENCES USE THE EXACT NAME YOU HAVE CHANGED TO. EXAMPLE IF I CHANGE ALL MY PICTUREBOX NAMES AS -
PictureBox_A1 , PictureBox_A2 ,... and so on then my refence should be as -
Dim r As DataRow
For Each r In t1.Rows
CType(Controls("PictureBox_" & r(2)), PictureBox).Image = bookedicon
Next
MY REFERENCE IS - "PictureBox_"
I know how to use BackgroundWorker (gui object in WinForms designer), and to manually instantiate Threads that elevate the custom event to the UI, however, I am having some trouble figuring out how to use the ThreadPool object (simplest form) to handle elevating an event to the form for "safe" UI manipulation.
Example is as follows :
Form1.vb
Public Class Form1
WithEvents t As Tools = New Tools
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
t.Unzip("file 1", "foo")
t.Unzip("file 2", "foo")
t.Unzip("file 3", "foo")
t.Unzip("file 4", "foo")
t.Unzip("file 5", "foo")
t.Unzip("file 6", "foo")
t.Unzip("file 7", "foo")
t.Unzip("file 8", "foo")
t.Unzip("file 9", "foo")
End Sub
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
End Sub
End Class
( add a multiline textbox, and a button to this form for the demo )
Tools.vb
Imports System
Imports System.Threading
Imports System.IO.Compression
Public Class Tools
#Region "Zip"
Private _zip As System.IO.Compression.ZipFile
Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)
Public Class ZipInfo
Public Property ZipFile As String
Public Property Path As String
End Class
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Destination
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Folder
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Shared Sub ThreadUnzip(ZipInfo As Object)
RaiseEvent UnzipComplete(ZipInfo)
End Sub
Shared Sub ThreadZip(ZipInfo As Object)
RaiseEvent ZipComplete(ZipInfo)
End Sub
#End Region
End Class
What this code should do, is as follows :
On Button1_Click, add 9 items to the ThreadPool
On each thread completion (order is irrelevant), raise an event that elevates to Form1
The event being raised on Form1 should be UI safe, so I can use the information being passed to the ZipCompleted / UnzipCompleted events in the Textbox. This should be generic, meaning the function that raises the event should be reusable and does not make calls to the form directly. (aka, I do not want a "custom" sub or function in Tools.vb that calls specific elements on Form1.vb . This should be generic and reusable by adding the class to my project and then entering any "custom" form code under the event being raised (like when Button1_Click is raised, even though it's threaded, the other form interactions are not part of the Button1 object/class -- they are written by the coder to the event that is raised when a user clicks.
If you want to ensure that an object that has no direct knowledge of your UI raises its events on the UI thread then use the SynchronizationContext class, e.g.
Public Class SomeClass
Private threadingContext As SynchronizationContext = SynchronizationContext.Current
Public Event SomethingHappened As EventHandler
Protected Overridable Sub OnSomethingHappened(e As EventArgs)
RaiseEvent SomethingHappened(Me, e)
End Sub
Private Sub RaiseSomethingHappened()
If Me.threadingContext IsNot Nothing Then
Me.threadingContext.Post(Sub(e) Me.OnSomethingHappened(DirectCast(e, EventArgs)), EventArgs.Empty)
Else
Me.OnSomethingHappened(EventArgs.Empty)
End If
End Sub
End Class
As long as you create your instance of that class on the UI thread, its SomethingHappened event will be raised on the UI thread. If there is no UI thread then the event will simply be raised on the current thread.
Here's a more complete example, which includes a simpler method for using a Lambda Expression:
Imports System.Threading
Public Class Form1
Private WithEvents thing As New SomeClass
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Me.thing.DoSomethingAsync()
End Sub
Private Sub thing_DoSomethingCompleted(sender As Object, e As IntegerEventArgs) Handles thing.DoSomethingCompleted
MessageBox.Show(String.Format("The number is {0}.", e.Number))
End Sub
End Class
''' <summary>
''' Raises events on the UI thread after asynchronous tasks, assuming the instance was created on a UI thread.
''' </summary>
Public Class SomeClass
Private ReadOnly threadingContext As SynchronizationContext = SynchronizationContext.Current
Public Event DoSomethingCompleted As EventHandler(Of IntegerEventArgs)
''' <summary>
''' Begin an asynchronous task.
''' </summary>
Public Sub DoSomethingAsync()
Dim t As New Thread(AddressOf DoSomething)
t.Start()
End Sub
Protected Overridable Sub OnDoSomethingCompleted(e As IntegerEventArgs)
RaiseEvent DoSomethingCompleted(Me, e)
End Sub
Private Sub DoSomething()
Dim rng As New Random
Dim number = rng.Next(5000, 10000)
'Do some work.
Thread.Sleep(number)
Dim e As New IntegerEventArgs With {.Number = number}
'Raise the DoSomethingCompleted event on the UI thread.
Me.threadingContext.Post(Sub() OnDoSomethingCompleted(e), Nothing)
End Sub
End Class
Public Class IntegerEventArgs
Inherits EventArgs
Public Property Number() As Integer
End Class
You should register from the Form to events of the Tools class (you already have these events defined), of course the actual event will be fired under a non-UI thread, so the code it executes during the callback will only be able to update the UI via an Invoke()
You want to simply raise the event in the Tools class, the Invoke needs to be done because you want to update the UI, the Tools class should be concerned about that.
Change your event handling like so:
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
End Sub
To register to the event from the view: (this would go in the Button1_Click event
AddHandler t.UnzipComplete, AddressOf t_UnzipComplete
Make sure you only register to the event one time
Does this solve your issue?
Private Sub t_UnzipComplete(ZipInfo As Tools.ZipInfo) Handles t.UnzipComplete
If TextBox1.InvokeRequired Then
TextBox1.Invoke(Sub () t_UnzipComplete(ZipInfo))
Else
TextBox1.Text = TextBox1.Text & ZipInfo.ZipFile & vbCr
End If
End Sub
You could create a callback to do the invoking in a safer way. Something like this:
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String, _
ByVal SafeCallback As Action(Of ZipInfo))
And then the calling code does this:
t.Unzip("file 1", "foo", Sub (zi) TextBox1.Invoke(Sub () t_UnzipComplete(zi)))
Personally I think it is better - and more conventional - to invoke on the event handler, but you could do it this way.
Okay, so here is what I came up with using a combination of the information from everyone contributing to this question -- all excellent and VERY helpful answers, which helped lead me to the final solution. Ideally, I would like this as a straight "class", but I can accept a UserControl for this purpose. If someone can take this and do exactly the same thing with a class, that would definitely win my vote. Right now, I will really have to consider which one to vote for.
Here is the updated Tools.vb
Imports System
Imports System.Threading
Imports System.Windows.Forms
Imports System.IO.Compression
Public Class Tools
Inherits UserControl
#Region "Zip"
Private _zip As System.IO.Compression.ZipFile
Private threadingContext As SynchronizationContext = SynchronizationContext.Current
Private Delegate Sub EventArgsDelegate(ByVal e As ZipInfo)
Public Shared Event UnzipComplete(ByVal ZipInfo As ZipInfo)
Public Shared Event ZipComplete(ByVal ZipInfo As ZipInfo)
Public Class ZipInfo
Public Property ZipFile As String
Public Property Path As String
End Class
Public Sub Unzip(ByVal ZipFile As String, ByVal Destination As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Destination
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Public Sub Zip(ByVal Folder As String, ByVal ZipFile As String)
Dim _ZipInfo As New Tools.ZipInfo
_ZipInfo.ZipFile = ZipFile
_ZipInfo.Path = Folder
ThreadPool.QueueUserWorkItem(AddressOf ThreadUnzip, _ZipInfo)
End Sub
Private Sub ThreadUnzip(ZipInfo As Object)
If Me.InvokeRequired Then
Me.Invoke(New EventArgsDelegate(AddressOf ThreadUnzip), ZipInfo)
Else
RaiseEvent UnzipComplete(ZipInfo)
End If
End Sub
Private Sub ThreadZip(ZipInfo As Object)
If Me.InvokeRequired Then
Me.Invoke(New EventArgsDelegate(AddressOf ThreadZip), ZipInfo)
Else
RaiseEvent ZipComplete(ZipInfo)
End If
End Sub
#End Region
End Class
If you drop this on Form1.vb, and select/activate the UnzipComplete/ZipComplete events, you will find that they will interact with the UI thread without having to pass a Sub, or Invoke, etc, from the Form. It is also generic, meaning it is unaware of what form elements you will be interacting with so explicit invoking such as TexBox1.Invoke() or other element specific calls are not required.
I apologize in advance if my question is too long-winded. I looked at the question “How to update data in GUI with messages that are being received by a thread of another class?” and it is very close to what I am trying to do but the answer was not detailed enough to be helpful.
I have converted a VB6 app to VB.NET (VS2013). The main function of the app is to send queries to a Linux server and display the results on the calling form. Since the WinSock control no longer exists, I’ve created a class to handle the functions associated with the TcpClient class. I can successfully connect to the server and send and receive data.
The problem is that I have multiple forms that use this class to send query messages to the server. The server responds with data to be displayed on the calling form. When I try to update a control on a form, I get the error "Cross-thread operation not valid: Control x accessed from a thread other than the thread it was created on." I know I’m supposed to use Control.InvokeRequired along with Control.Invoke in order to update controls on the Main/UI thread, but I can’t find a good, complete example in VB. Also, I have over 50 forms with a variety of controls on each form, I really don’t want to write a delegate handler for each control. I should also mention that the concept of threads and delegates is very new to me. I have been reading everything I can find on this subject for the past week or two, but I’m still stuck!
Is there some way to just switch back to the Main Thread? If not, is there a way I can use Control.Invoke just once to cover a multitude of controls?
I tried starting a thread just after connecting before I start sending and receiving data, but netStream.BeginRead starts its own thread once the callback function fires. I also tried using Read instead of BeginRead. It did not work well if there was a large amount of data in the response, BeginRead handled things better. I feel like Dorothy stuck in Oz, I just want to get home to the main thread!
Thanks in advance for any help you can provide.
Option Explicit On
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Friend Class ATISTcpClient
Public Event Receive(ByVal data As String)
Private Shared WithEvents oRlogin As TcpClient
Private netStream As NetworkStream
Private BUFFER_SIZE As Integer = 8192
Private DataBuffer(BUFFER_SIZE) As Byte
Public Sub Connect()
Try
oRlogin = New Net.Sockets.TcpClient
Dim localIP As IPAddress = IPAddress.Parse(myIPAddress)
Dim localPrt As Int16 = myLocalPort
Dim ipLocalEndPoint As New IPEndPoint(localIP, localPrt)
oRlogin = New TcpClient(ipLocalEndPoint)
oRlogin.NoDelay = True
oRlogin.Connect(RemoteHost, RemotePort)
Catch e As ArgumentNullException
Debug.Print("ArgumentNullException: {0}", e)
Catch e As Net.Sockets.SocketException
Debug.Print("SocketException: {0}", e)
End Try
If oRlogin.Connected() Then
netStream = oRlogin.GetStream
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0, BUFFER_SIZE, _
AddressOf DataArrival, DataBuffer)
End If
Send(vbNullChar)
Send(User & vbNullChar)
Send(User & vbNullChar)
Send(Term & vbNullChar)
End If
End Sub
Public Sub Send(newData As String)
On Error GoTo send_err
If netStream.CanWrite Then
Dim sendBytes As [Byte]() = Encoding.UTF8.GetBytes(newData)
netStream.Write(sendBytes, 0, sendBytes.Length)
End If
Exit Sub
send_err:
Debug.Print("Error in Send: " & Err.Number & " " & Err.Description)
End Sub
Private Sub DataArrival(ByVal dr As IAsyncResult)
'This is where it switches to a WorkerThread. It never switches back!
On Error GoTo dataArrival_err
Dim myReadBuffer(BUFFER_SIZE) As Byte
Dim myData As String = ""
Dim numberOfBytesRead As Integer = 0
numberOfBytesRead = netStream.EndRead(dr)
myReadBuffer = DataBuffer
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Do While netStream.DataAvailable
numberOfBytesRead = netStream.Read(myReadBuffer, 0, myReadBuffer.Length)
myData = myData & Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead)
Loop
'Send data back to calling form
RaiseEvent Receive(myData)
'Start reading again in case we don‘t have the entire response yet
If netStream.CanRead Then
netStream.BeginRead(DataBuffer, 0,BUFFER_SIZE,AddressOf DataArrival,DataBuffer)
End If
Exit Sub
dataArrival_err:
Debug.Print("Error in DataArrival: " & err.Number & err.Description)
End Sub
Instead of using delegates one could use anonymous methods.
Singleline:
uicontrol.Window.Invoke(Sub() ...)
Multiline:
uicontrol.Window.Invoke(
Sub()
...
End Sub
)
If you don't want to pass an UI control every time you need to invoke, create a custom application startup object.
Friend NotInheritable Class Program
Private Sub New()
End Sub
Public Shared ReadOnly Property Window() As Form
Get
Return Program.m_window
End Get
End Property
<STAThread()> _
Friend Shared Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Dim window As New Form1()
Program.m_window = window
Application.Run(window)
End Sub
Private Shared m_window As Form
End Class
Now, you'll always have access to the main form of the UI thread.
Friend Class Test
Public Event Message(text As String)
Public Sub Run()
Program.Window.Invoke(Sub() RaiseEvent Message("Hello!"))
End Sub
End Class
In the following sample code, notice that the Asynchronous - Unsafe run will throw a Cross-thread exception.
Imports System.Threading
Imports System.Threading.Tasks
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.cbOptions = New ComboBox() With {.TabIndex = 0, .Dock = DockStyle.Top, .DropDownStyle = ComboBoxStyle.DropDownList} : Me.cbOptions.Items.AddRange({"Asynchronous", "Synchronous"}) : Me.cbOptions.SelectedItem = "Asynchronous"
Me.btnRunSafe = New Button() With {.TabIndex = 1, .Dock = DockStyle.Top, .Text = "Run safe!", .Height = 30}
Me.btnRunUnsafe = New Button() With {.TabIndex = 2, .Dock = DockStyle.Top, .Text = "Run unsafe!", .Height = 30}
Me.tbOutput = New RichTextBox() With {.TabIndex = 3, .Dock = DockStyle.Fill}
Me.Controls.AddRange({Me.tbOutput, Me.btnRunUnsafe, Me.btnRunSafe, Me.cbOptions})
Me.testInstance = New Test()
End Sub
Private Sub _ButtonRunSafeClicked(s As Object, e As EventArgs) Handles btnRunSafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunSafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunSafe(mode))
End If
End Sub
Private Sub _ButtonRunUnsafeClicked(s As Object, e As EventArgs) Handles btnRunUnsafe.Click
Dim mode As String = CStr(Me.cbOptions.SelectedItem)
If (mode = "Synchronous") Then
Me.testInstance.RunUnsafe(mode)
Else 'If (mode = "Asynchronous") Then
Task.Factory.StartNew(Sub() Me.testInstance.RunUnsafe(mode))
End If
End Sub
Private Sub TestMessageReceived(text As String) Handles testInstance.Message
Me.tbOutput.Text = (text & Environment.NewLine & Me.tbOutput.Text)
End Sub
Private WithEvents btnRunSafe As Button
Private WithEvents btnRunUnsafe As Button
Private WithEvents tbOutput As RichTextBox
Private WithEvents cbOptions As ComboBox
Private WithEvents testInstance As Test
Friend Class Test
Public Event Message(text As String)
Public Sub RunSafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
Program.Window.Invoke(Sub() RaiseEvent Message(String.Format("Safe ({0}) # {1}", mode, Date.Now)))
End Sub
Public Sub RunUnsafe(mode As String)
'Do some work:
Thread.Sleep(2000)
'Notify any listeners:
RaiseEvent Message(String.Format("Unsafe ({0}) # {1}", mode, Date.Now))
End Sub
End Class
End Class
Thank you to those who took the time to make suggestions. I found a solution. Though it may not be the preferred solution, it works beautifully. I simply added MSWINSCK.OCX to my toolbar, and use it as a COM/ActiveX component. The AxMSWinsockLib.AxWinsock control includes a DataArrival event, and it stays in the Main thread when the data arrives.
The most interesting thing is, if you right click on AxMSWinsockLib.DMSWinsockControlEvents_DataArrivalEvent and choose Go To Definition, the object browser shows the functions and delegate subs to handle the asynchronous read and the necessary delegates to handle BeginInvoke, EndInvoke, etc. It appears MicroSoft has already done the hard stuff that I did not have the time or experience to figure out on my own!