I am trying to update the progress bar which is on my Form1 using Form1.ProgressBar1.PerformStep() from a class function. I am using the async functions. There are 10 processes running but the progress bar should be updated based on the value of the records read (and later processed) within the While loop. It looks like I'm not able to update the GUI using the code below. I have tried begininvoke and invoke but not luck. Any ideas?
Await Cheque.MultiProcessCheques()
Public Shared Async Function MultiProcessCheques() As Task
Dim tasks As New List(Of Task)()
For i As Integer = 0 To 9
Dim temp_i As Integer = i
tasks.Add(Task.Run(Function() Cheque.CopyBinaryValueToFile(temp_i)))
Next
Await Task.WhenAll(tasks)
End Function
Public Shared Async Function CopyBinaryValueToFile(i As Integer) As Task
Try
Using connection = ConnectionController.GetConnection
Await connection.OpenAsync()
Using command = ConnectionController.GetCommand
command.CommandText = ("SELECT ch.RECORDID FROM TABLE WHERE VALUE = '%" & i & "'")
command.Connection = connection
command.CommandTimeout = 0
Using reader As Common.DbDataReader = Await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess)
While Await reader.ReadAsync()
If reader.HasRows Then
End If
' update Progress bar here
Form1.ProgressBar1.PerformStep()
End While
End Using
End Using
End Using
Catch ex As Exception
MessageBox.Show("1" & ex.ToString)
End Try
End Function
End Class
Create a Shared Member to hold the Form1 reference in your Cheque Class, and modify your MultiProcessCheques() function to receive a reference:
Public Class Cheque
Private Shared F1 As Form1
Public Shared Async Function MultiProcessCheques(ByVal f1 As Form1) As Task
Cheque.F1 = f1
' ... other code ...
End Function
Public Shared Async Function CopyBinaryValueToFile(i As Integer) As Task
' ... other code ...
Cheque.F1.Invoke(Sub()
Cheque.F1.ProgressBar1.PerformStep()
End Sub)
' ... other code ...
End Function
End Class
Then pass in "Me" when you call it:
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' ... other code ...
Await Cheque.MultiProcessCheques(Me)
' ... other code ...
End Sub
Related
I am working on coronavirus statistics dashboard as university project, and I have some problems with asynchronous source data download from sites with statistics.
Well, I failed to understand how to do it myself.
I tried to create my own class with function what will create multiple async web requests
and then wait until they all finished, then return results of all these requests.
Imports System.Net.WebClient
Imports System.Net
Public Class AsyncDownload
Private result As New Collection
Private Sub DownloadCompletedHander(ByVal sender As Object, ByVal e As System.Net.DownloadStringCompletedEventArgs)
If e.Cancelled = False AndAlso e.Error Is Nothing Then
Dim myString As String = CStr(e.Result)
result.Add(myString, sender.Headers.Item("source"))
End If
End Sub
Public Function Load(sources As Array, keys As Array) As Collection
Dim i = 0
Dim WebClients As New Collection
While (i < sources.Length)
Dim newClient As New WebClient
newClient.Headers.Add("source", keys(i))
newClient.Headers.Add("sourceURL", sources(i))
AddHandler newClient.DownloadStringCompleted, AddressOf DownloadCompletedHander
WebClients.Add(newClient)
i = i + 1
End While
i = 1
For Each client As WebClient In WebClients
Dim url As String = client.Headers.Item("sourceURL")
client.DownloadStringAsync(New Uri(url))
Next
While (result.Count < WebClients.Count)
End While
Return result
End Function
End Class
And it is used in:
Dim result As New Collection
Private Sub test() Handles Me.Load
Dim downloader As New CoronaStatisticsGetter.AsyncDownload
result = downloader.Load({"https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_total.json"}, {"Nationalwide Data"})
End Sub
It should work like:
I create a new instance of my class.
Calling function Load of this class
Funciton Load creates instances of System.Net.WebClient for each url and adds as handler DownloadCompletedHander
Function Load goes calls DownloadStringAsync of each client
Function Load waits in While loop until result collection items count is not as big as number of url on input
If item count in result is same as urls number that means what everything is downloaded, so it breaks loop and returns all requested data
The problem is that it doesn't work, it just endlessly remain in while loop, and as I see using debug collection result is not updated (its size is always 0)
Same time, when I try to asynchronously download it without using my class, everything works fine:
Private Sub Download() 'Handles Me.Load
Dim wc As New System.Net.WebClient
wc.Headers.Add("source", "VaccinationByAgeGroup")
AddHandler wc.DownloadStringCompleted, AddressOf DownloadCompletedHander
wc.DownloadStringAsync(New Uri("https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_agegroup.json"))
End Sub
Could somebody tell me please why it is not working and where is the problem?
The following shows how one can use System.Net.WebClient with Task to download a string (ie: data) from a URL.
Add a project reference (System.Net)
VS 2019:
In VS menu, click Project
Select Add reference...
Select Assemblies
Check System.Net
Click OK
Create a class (name: DownloadedData.vb)
Public Class DownloadedData
Public Property Data As String
Public Property Url As String
End Class
Create a class (name: HelperWebClient.vb)
Public Class HelperWebClient
Public Async Function DownloadDataAsync(urls As List(Of String)) As Task(Of List(Of DownloadedData))
Dim allTasks As List(Of Task) = New List(Of Task)
Dim downloadedDataList As List(Of DownloadedData) = New List(Of DownloadedData)
For i As Integer = 0 To urls.Count - 1
'set value
Dim url As String = urls(i)
Debug.WriteLine(String.Format("[{0}]: Adding {1}", i, url))
Dim t = Task.Run(Async Function()
'create new instance
Dim wc As WebClient = New WebClient()
'await download
Dim result = Await wc.DownloadStringTaskAsync(url)
Debug.WriteLine(url & " download complete")
'ToDo: add desired code
'add
downloadedDataList.Add(New DownloadedData() With {.Url = url, .Data = result})
End Function)
'add
allTasks.Add(t)
Next
For i As Integer = 0 To allTasks.Count - 1
'wait for a task to complete
Dim t = Await Task.WhenAny(allTasks)
'remove from List
allTasks.Remove(t)
'write data to file
'Note: The following is only for testing.
'The index in urls won't necessarily correspond to the filename below
Dim filename As String = System.IO.Path.Combine("C:\Temp", String.Format("CoronavirusData_{0:00}.txt", i))
System.IO.File.WriteAllText(filename, downloadedDataList(i).Data)
Debug.WriteLine($"[{i}]: Filename: {filename}")
Next
Debug.WriteLine("all tasks complete")
Return downloadedDataList
End Function
End Class
Usage:
Private Async Sub btnRun_Click(sender As Object, e As EventArgs) Handles btnRun.Click
Dim helper As HelperWebClient = New HelperWebClient()
Dim urls As List(Of String) = New List(Of String)
urls.Add("https://opendata.digilugu.ee/covid19/vaccination/v3/opendata_covid19_vaccination_total.json")
urls.Add("https://api.covidtracking.com/v2/states.json")
urls.Add("https://covidtrackerapi.bsg.ox.ac.uk/api/v2/stringency/date-range/2020-01-01/2022-03-01")
urls.Add("http://covidsurvey.mit.edu:5000/query?age=20-30&gender=all&country=US&signal=locations_would_attend")
Dim downloadedDataList = Await helper.DownloadDataAsync(urls)
Debug.WriteLine("Complete")
End Sub
Resources:
How do I wait for something to finish in C#?
How should Task.Run call an async method in VB.NET?
VB.net ContinueWith
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
I am making an application that reads values from a meter that is connected to my computer via a serial cable. When i press a button i send a command to the meter and after a few miliseconds i get a response back from the meter with the answer.
I am saving these values to a class that has properties init, so that i can access these values from anywhere.
So my problem is that when i try to get the values back it returns a 'nothing value', and its probably from the initialization i have that has a 'New' like this'Dim clsSavedValues As New clsSavedValues', so when i try to get the values from that property class i create a new instanse and that instanse is empty if am not mistaken.
Ill post the code below but here is how the code flows:
I have 3 classes. MainClass, ProtocolClass, PropertiesClass.
From main i call a method inside ProtocolClass, and that method sends a command to the meter. after a few miliseconds i get a call back inside ProtocolClass anf this method is called 'Private Sub SerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort.DataReceived' and it saves that return value to the PropertiesClass.
And after the DataReceived method is finished i go back to the MainClass and call another method to get the values from the PropertiesClass that i just saved but they return null. I know they are saved correctly because i can access them if i call them from within the ProtocolClass. But they are null from MainClass.
Here is my code:
MainClass
'Here i call the ProtocolClass
Private Sub btnGetLastTransaction_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGetLastTransaction.Click
clsProtocol.GetLastTransaction(1, Integer.Parse(tbxTransactionPosition.Text))
End Sub
'Here i try to read the valies from PropertiesClass
Public Sub RetrieveMeterSerialNumber()
Dim clsSavedValues As New clsSavedValues
lblMeterSerialNumber.Text = clsSavedValues.SaveMeterSerialNumber
End Sub
ProtocolClass
Public Sub GetLastTransaction(ByVal destinationAddress As String, ByVal transactionNum As Integer)
clsSavedValues = New clsSavedValues 'Creating Instance of the properties class
Try
Dim v_bodyOfMessage As [Byte]() = {ASCIItoHEX("G"), _
ASCIItoHEX("r")}
Dim v_bytearray As [Byte]() = ConstructCommand(v_bodyOfMessage)
SendCommand(v_bytearray)
Catch ex As Exception
Console.WriteLine("Meter serial number button click exception: {0}", ex)
End Try
End Sub
Private Sub SerialPort_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort.DataReceived
If comOpen Then
Try
ReDim rx(rxPacketSize)
Console.WriteLine("RESPONSE")
For i = 0 To rxPacketSize - 1
readByte = SerialPort.ReadByte.ToString
Console.WriteLine(i.ToString & ": " & Conversion.Int(readByte).ToString)
rx(i) = Conversion.Int(readByte).ToString
If i <> 0 Then
If Convert.ToByte(rx(i)) = vDelimeterFlag(0) Then Exit For
End If
Next
DecodeResponse()
Catch ex As Exception
MsgBox("SerialPort_DataReceived Exception: " & ex.Message)
End Try
End If
End Sub
Private Sub GetMeterSerialNumber()
Dim i_startPosition As Integer = 5
Dim meterSerialNumber As String = GetRemainingPortionOfString(i_startPosition)
clsSavedValues.SaveMeterSerialNumber = meterSerialNumber
frmExplorer.RetrieveMeterSerialNumber() 'This is the call to the main class
End Sub
PropertiesClass
Public Property SaveMeterSerialNumber() As String
Get
Return _MeterSerialNumber
End Get
Set(ByVal meterSerialNumber As String)
_MeterSerialNumber = meterSerialNumber
End Set
End Property
I want to get the values from the PropertiesClass because ill get more than wan response from the meter and that causes thread issues and i cannot keep track with them. So i save the values in one class and then i want to access them all from that class.
Sorry for the long post, ask me anything you want for clarification
clsSavedValues in SerialPort_DataReceived() and in your Main Class RetrieveMeterSerialNumber() are two different objects (with same variable names but each 'new' create a new instance of clsSavedValues ) maybe you should pass the clsSavedValues var from protocol to Main as parameter.
Main :
Public Sub RetrieveMeterSerialNumber(clsSavedValues As clsSavedValues )
lblMeterSerialNumber.Text = clsSavedValues.SaveMeterSerialNumber
End Sub
Protocol :
Private Sub GetMeterSerialNumber()
Dim i_startPosition As Integer = 5
Dim meterSerialNumber As String = GetRemainingPortionOfString(i_startPosition)
clsSavedValues.SaveMeterSerialNumber = meterSerialNumber
frmExplorer.RetrieveMeterSerialNumber(clsSavedValues) 'This is the call to the main class
End Sub
or use a static property in your PropertiesClass
I have been creating a single instance application using a Mutex.
In the Sub Main code, the app checks to see if it is the first instance, if so it starts the form (called MainForm). The MainForm creates an asynchronous named pipe server to receive arguments passed from a new instance.
If the app is not the first instance, Sub Main creates a named pipe client, sends the command line arguments through to the first app, and proceeds to exit.
The application is tab-based, and each command line argument is a file path, which is used to create the tab. The argument is received (I can MsgBox() it), but when I try to pass it as an argument to the control I'm creating, nothing happen
Pipe classes:
Namespace Pipes
' Delegate for passing received message back to caller
Public Delegate Sub DelegateMessage(Reply As String)
Public Class PipeServer
Public Event PipeMessage As DelegateMessage
Private _pipeName As String
Public Sub Listen(PipeName As String)
Try
' Set to class level var so we can re-use in the async callback method
_pipeName = PipeName
' Create the new async pipe
Dim pipeServer As New NamedPipeServerStream(PipeName, PipeDirection.[In], 1, PipeTransmissionMode.[Byte], PipeOptions.Asynchronous)
' Wait for a connection
pipeServer.BeginWaitForConnection(New AsyncCallback(AddressOf WaitForConnectionCallBack), pipeServer)
Catch oEX As Exception
Debug.WriteLine(oEX.Message)
End Try
End Sub
Private Sub WaitForConnectionCallBack(iar As IAsyncResult)
Try
' Get the pipe
Dim pipeServer As NamedPipeServerStream = DirectCast(iar.AsyncState, NamedPipeServerStream)
' End waiting for the connection
pipeServer.EndWaitForConnection(iar)
Dim buffer As Byte() = New Byte(254) {}
' Read the incoming message
pipeServer.Read(buffer, 0, 255)
' Convert byte buffer to string
Dim stringData As String = Encoding.UTF8.GetString(buffer, 0, buffer.Length)
Debug.WriteLine(stringData + Environment.NewLine)
' Pass message back to calling form
RaiseEvent PipeMessage(stringData)
' Kill original sever and create new wait server
pipeServer.Close()
pipeServer = Nothing
pipeServer = New NamedPipeServerStream(_pipeName, PipeDirection.[In], 1, PipeTransmissionMode.[Byte], PipeOptions.Asynchronous)
' Recursively wait for the connection again and again....
pipeServer.BeginWaitForConnection(New AsyncCallback(AddressOf WaitForConnectionCallBack), pipeServer)
Catch
Return
End Try
End Sub
End Class
Class PipeClient
Public Sub Send(SendStr As String, PipeName As String, Optional TimeOut As Integer = 1000)
Try
Dim pipeStream As New NamedPipeClientStream(".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous)
' The connect function will indefinitely wait for the pipe to become available
' If that is not acceptable specify a maximum waiting time (in ms)
pipeStream.Connect(TimeOut)
Debug.WriteLine("[Client] Pipe connection established")
Dim _buffer As Byte() = Encoding.UTF8.GetBytes(SendStr)
pipeStream.BeginWrite(_buffer, 0, _buffer.Length, AddressOf AsyncSend, pipeStream)
Catch oEX As TimeoutException
Debug.WriteLine(oEX.Message)
End Try
End Sub
Private Sub AsyncSend(iar As IAsyncResult)
Try
' Get the pipe
Dim pipeStream As NamedPipeClientStream = DirectCast(iar.AsyncState, NamedPipeClientStream)
' End the write
pipeStream.EndWrite(iar)
pipeStream.Flush()
pipeStream.Close()
pipeStream.Dispose()
Catch oEX As Exception
Debug.WriteLine(oEX.Message)
End Try
End Sub
End Class
End Namespace
MainForm logic:
#Region "Pipes"
Public ArgumentPipe As New Pipes.PipeServer
Public Sub RecievedMessage(reply As String)
GetMainformHook.Invoke(MySTDelegate, reply)
End Sub
Public Sub InitializeServer()
AddHandler ArgumentPipe.PipeMessage, AddressOf RecievedMessage
ArgumentPipe.Listen(_pipename)
End Sub
Public Delegate Sub RecievedMessageDel(txt As String)
Public MySTDelegate As RecievedMessageDel = AddressOf SetText
Public Sub SetText(txt)
MsgBox(txt)
TabStrip1.AddTab(txt.ToString) 'PROBLEM OCCURS HERE
End Sub
Public Shared Function GetMainformHook() As MainForm
Return Application.OpenForms("MainForm")
End Function
Public Shared Function GetTabControl() As TabStrip
Return CType(Application.OpenForms("MainForm"), MainForm).TabStrip1
End Function
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitializeServer()
End Sub
#End Region
In Sub Main when sending argument:
Dim _pipeClient = New Pipes.PipeClient()
If cmdArgs.Length > 0 Then
For i = 0 To cmdArgs.Length - 1
_pipeClient.Send(cmdArgs(i), _pipename, 1000)
Next
End If
_pipename is a global string like "myappv6"
Am I missing something?
I'm thinking this has something to do with cross threading, but can't pinpoint where to fix it.
Thanks
Relevant to Silverlight 5 / Async CTP
I want to create an asynchronous function that initiates a layout update and then Awaits for the layout update to complete. Something like:
Private Async Function UpdateLayoutRoot() As Task
LayoutRoot.UpdateLayout()
Await LayoutRoot.LayoutUpdated <--- (NOT valid but shows desired outcome)
End Function
How can this be done? More generally, how can you use Await to wait for existing events?
One way to accomplish this is to await on a TaskCompletionSource that is set inside the event. I don't know VB.NET, hopefully you can understand it from C#:
// The type and value returned by the TaskCompletionSource
// doesn't matter, so I just picked int.
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
// The delegate sets the TaskCompletionSource -- the result value
// doesn't matter, we only care about setting it. Keep hold of
// the delegate so it can be removed later.
EventHandler d = (o, e) => { tcs.TrySetResult(1); };
LayoutRoot.LayoutUpdate += d;
try
{
LayoutRoot.UpdateLayout();
await tcs.Task;
}
finally
{
// Don't leak the delegate!
LayoutRoot.LayoutUpdate -= d;
}
Thanks Cory! Your suggestion to use TaskCompletionSource is just what I needed. I've combined the use of a TaskCompletionSource with the Lucian Wischik's Async CTP specification to develop a pair of generic Silverlight 5 classes that can be used to Await any CLR, or routed event. Only the Async CTP (AsyncCtpLibrary_Silverlight) is required (The formidable Rx library is not needed). Here are the two classes:
Public Class AwaitableEvent(Of TResult)
Private eta As EventTaskAwaiter(Of TResult) = Nothing
Sub New(ByVal Sender As Object, ByVal EventName As String)
eta = New EventTaskAwaiter(Of TResult)
Dim ei as EventInfo = Sender.GetType.GetEvent(EventName)
Dim d = [Delegate].CreateDelegate(ei.EventHandlerType,
Me, "EventCompletedHandler", True, True)
ei.AddEventHandler(Sender, d)
End Sub
Public Function GetAwaiter() As EventTaskAwaiter(Of TResult)
Return eta
End Function
Private Sub EventCompletedHandler(ByVal sender As Object, ByVal e As TResult)
eta.tcs.TrySetResult(e)
End Sub
End Class
Public Class EventTaskAwaiter(Of TResult)
Friend tcs As New TaskCompletionSource(Of TResult)
Public ReadOnly Property IsCompleted As Boolean
Get
Return tcs.Task.IsCompleted
End Get
End Property
Sub OnCompleted(r As Action)
Dim sc = SynchronizationContext.Current
If sc Is Nothing Then
tcs.Task.ContinueWith(Sub() r())
Else
tcs.Task.ContinueWith(Sub() sc.Post(Sub() r(), Nothing))
End If
End Sub
Function GetResult() As TResult
If tcs.Task.IsCanceled Then Throw New TaskCanceledException(tcs.Task)
If tcs.Task.IsFaulted Then Throw tcs.Task.Exception.InnerException
Return tcs.Task.Result
End Function
End Class
Here's an example of using the AwaitableEvent class to Await mouse, keyboard and timer events.
Private Sub AECaller()
GetMouseButtonAsync()
MessageBox.Show("After Await mouse button event")
GetKeyAsync()
MessageBox.Show("After Await key event")
GetTimerAsync()
MessageBox.Show("After Await timer")
End Sub
Private Async Sub GetMouseButtonAsync()
Dim ae As New AwaitableEvent(Of MouseButtonEventArgs)(LayoutRoot, "MouseLeftButtonDown")
Dim e = Await ae
MessageBox.Show(String.Format("Clicked {0} at {1},{2}",
e.OriginalSource.ToString,
e.GetPosition(LayoutRoot).X,
e.GetPosition(LayoutRoot).Y))
End Sub
Private Async Sub GetKeyAsync()
Dim ae As New AwaitableEvent(Of KeyEventArgs)(LayoutRoot, "KeyDown")
Dim e = Await ae
MessageBox.Show(String.Format("Key {0} was pressed", e.Key.ToString))
End Sub
Private Async Sub GetTimerAsync()
Dim StopWatch As New DispatcherTimer
StopWatch.Interval = New TimeSpan(TimeSpan.TicksPerSecond * 6)
Dim ae As New AwaitableEvent(Of EventArgs)(StopWatch, "Tick")
StopWatch.Start()
Await ae
MessageBox.Show(String.Format("It's {0}seconds later!", StopWatch.Interval.TotalSeconds))
StopWatch.Stop()
End Sub
As expected the Await statement returns control to the calling function immediately. When the events are subsequently completed, Await assigns the result (the event args appropriate for the event being monitored) and the remaining code in the asynchronous method is then run.