I'm trying to call a web service and have my code wait for that service to return a result (or timeout). My project is Silverlight 5 with Web Services using .NET 4.0 and I'm running this project under VS 2012 with the Microsoft.Bcl.Async.1.0.16\lib\sl4\Microsoft.Threading.Tasks.dll ... Task.Extensions.dll ... and Extensions.Silverlight.dll.
This is how I've been doing it and it's working, but I'm trying to figure out how to change my code so that I can use the Async/Await process. The web service reference is configured to return ObservableCollection and Generic.Dictionary with Reuse types in all referenced assemblies.
Some of my code I need to convert to Async/Await:
Private _Units As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Public Property Units() As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Get
Return _Units
End Get
Set(ByVal value As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
_Units = value
OnPropertyChanged(New PropertyChangedEventArgs("Units"))
End Set
End Property
Public Sub ReadUnits()
Try
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client
If Me.Client.ClientID > 0 Then
' My have a sites
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadsCompleted, AddressOf LoadUnitsCompleted
' Call web service to get Sites the user has access to
wsUnits.LoadsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
End If
End If
End If
End If
Catch ex As Exception
Dim Problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
Private Sub LoadUnitsCompleted(ByVal sender As Object, ByVal e As DC.SL.Services.WebServiceUnit.LoadsCompletedEventArgs)
Try
If Not IsNothing(e.Result) Then
Me.Units = e.Result
If Me.Units.Count > 0 Then
Me.Unit = Me.Units.Item(0)
End If
End If
Catch ex As Exception
Dim Problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
Still not getting this to work ... here is what I have now, but the problem remains ... UI thread execution continues and does NOT wait for the Web Service call to finish.
Calling code:
ReadUnitsAsync().Wait(3000)
Here is the updated code:
Public Async Function ReadUnitsAsync() As Task(Of Boolean)
Dim Results As Object = Await LoadReadUnitsAsync()
Return True
End Function
Public Function LoadReadUnitsAsync() As Task(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
LoadReadUnitsAsync = Nothing
Dim tcs = New TaskCompletionSource(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client associated
If Me.Client.ClientID > 0 Then
' Only get associated sites IF we don't have any defined
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup Site web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadUnitsCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
'' Set Busy Status
'BusyStack.Manage(ProcessManager.StackAction.Add, "ReadUnits", Me.IsWorking, Me.IsWorkingMessage)
' Call web service to get Sites the user has access to
wsUnits.LoadUnitsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
Return tcs.Task
End If
End If
End If
End If
End Function
So here is the final code (abbreviated) that seems to be working to my goals (aka waiting for a Web Service to finish before proceeding).
Public Class UIUnits
Implements INotifyPropertyChanged, IDataErrorInfo
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Async Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
Dim propertyEventHandler As PropertyChangedEventHandler = PropertyChangedEvent
Try
If propertyEventHandler IsNot Nothing Then
RaiseEvent PropertyChanged(Me, e)
Select Case e.PropertyName
Case "Size"
Await ReadUnitsAsync()
DoSomethingElseAfterWebServiceCallCompletes()
Case Else
End Select
End If
Catch ex As Exception
Dim problem As New DC.SL.Tools.Errors(ex)
End Try
End Sub
...
Private _Units As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Public Property Units() As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units)
Get
Return _Units
End Get
Set(ByVal value As Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
_Units = value
OnPropertyChanged(New PropertyChangedEventArgs("Units"))
End Set
End Property
...
Public Async Function ReadUnitsAsync() As Task(Of Boolean)
Me.Units = Await LoadReadUnitsAsync()
Return True
End Function
...
Public Function LoadReadUnitsAsync() As Task(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
LoadReadUnitsAsync = Nothing
Dim tcs = New TaskCompletionSource(Of System.Collections.ObjectModel.ObservableCollection(Of DC.SL.Services.WebServiceUnit.Units))
' Client is required
If Not Me.Client Is Nothing Then
' User is required
If Not Me.User Is Nothing Then
' Must be a real Client associated
If Me.Client.ClientID > 0 Then
' Only get associated sites IF we don't have any defined
If Not Me.Site Is Nothing Then
' Call the web service relative to where this application is running
Dim webServiceURI As New Uri("../WebServices/Unit.svc", UriKind.RelativeOrAbsolute)
Dim webServiceAddress As New EndpointAddress(webServiceURI)
' Setup web Service proxy
Dim wsUnits As New DC.SL.Services.WebServiceUnit.UnitClient
wsUnits.Endpoint.Address = webServiceAddress
' Add event handler so we can trap for web service completion
AddHandler wsUnits.LoadUnitsCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
' Call web service
wsUnits.LoadUnitsAsync(Me.Client, Me.Site.SiteID, Me.Size.SizeID, Me.RentalType.RentalTypeID, Me.UnitState)
Return tcs.Task
End If
End If
End If
End If
End Function
In this case it's probably easiest to convert from the lowest level and work your way up. First, you need to define your own TAP-friendly extension methods on your service. VS will generate these for you if you are doing desktop development, but unfortunately it will not do this for Silverlight.
The MSDN docs describe how to wrap EAP (EAP is the pattern that uses *Async methods with matching *Completed events). If you have APM methods, it's even easier to wrap those into TAP (APM is the pattern that uses Begin*/End* method pairs).
Once you have a wrapper, e.g., LoadUnitsTaskAsync, change your method to call that instead of LoadUnitsAsync and Await the result. This will require your ReadUnits method to be Async, so change it to a Task-returning Function (and change its name from ReadUnits to ReadUnitsAsync). Next change all callers of ReadUnitsAsync so they Await its result. Repeat until you reach an actual event handler, which may be Async Sub (do not use Async Sub for any intermediate methods; use Async Function ... As Task instead).
I used this library to achieve async await in my Silverlight 5 project
Related
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 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
can i have a function in windows service and return the data to vb.net application?
here's my sample code in windows service:
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.
EventLog1.WriteEntry("In OnStart")
End Sub
Protected Overrides Sub OnStop()
' Add code here to perform any tear-down necessary to stop your service.
EventLog1.WriteEntry("In OnStop.")
End Sub
Protected Overrides Sub OnContinue()
EventLog1.WriteEntry("In OnContinue.")
End Sub
Protected Overrides Sub OnCustomCommand(ByVal command As Integer)
Dim mymsg As String
Dim servicestorun As New System.ServiceProcess.ServiceBase
If (command < 128) Then
MyBase.OnCustomCommand(command)
Else
Select Case command
Case 129
mymsg = "i want this msg to my vb.net application"
Case Else
End Select
End If
End Sub
Here's my code with vb.net application:
Dim myServiceController As New System.ServiceProcess.ServiceController("MyNewService")
Dim status As String
Try
myServiceController.ExecuteCommand(129)
' i want to get the msg from my windows service "i want this msg to my vb.net application"
status = "Custom Command Executed Successfully!"
Catch ex As Exception
status = "Failed To Execute Custom Command! " & ex.Message
End Try
Please need help.
You can use your windows service as a WCF host. See for instance http://msdn.microsoft.com/en-us/library/ms733069.aspx
For communication on the same machine, I would recommend Named Pipe (NetNamedPipeBinding). You could even use Named Pipes without using WCF (http://stackoverflow.com/questions/4303154/)
In my application, I have a MainWindow with a ToolStripProgressBar and a ToolStripStatusLabel.
This properties:
Property ProgressBarPercantage() As Integer Implements BCSXPSearchTool.Presenter.IMainView.ProgressPercentage
Get
Return Me._progressbarpercentage
End Get
Set(ByVal value As Integer)
Me._progressbarpercentage = value
Me.StatusStripCurrentProgressBar.Value = Me._progressbarpercentage
End Set
End Property
Private _progressbarpercentage As Integer = 0
Property ProgressStatusText() As String Implements BCSXPSearchTool.Presenter.IMainView.ProgressStatusText
Get
Return Me._progressstatustext
End Get
Set(ByVal value As String)
Me._progressstatustext = value
Me.StatusStripCurrentState.Text = Me._progressstatustext
End Set
End Property
Private _progressstatustext As String = "Ready"
In the MainWindowPresenter I start a new BackgroundWorker which should read from a database.
Public Sub Search()
Dim bw As New BackgroundWorker
bw.WorkerReportsProgress = True
bw.WorkerSupportsCancellation = True
AddHandler bw.DoWork, AddressOf runproc
If bw.IsBusy = False Then
bw.RunWorkerAsync()
End If
End Sub
Public Sub runproc()
Dim statusToSub As delegateStatusTo = AddressOf statusTo
Dim percToSub As delegatePercTo = AddressOf percTo
statusToSub.Invoke("Test")
'percToSub.Invoke(50)
End Sub
Public Sub percTo(ByVal value As Integer)
_view.ProgressPercentage = value
End Sub
Public Sub statusTo(ByVal value As String)
_view.ProgressStatusText = value
End Sub
Delegate Sub delegateStatusTo(ByVal value As String)
Delegate Sub delegatePercTo(ByVal value As Integer)
The code above is working. But if I change the sub runproc() to:
Public Sub runproc()
Dim statusToSub As delegateStatusTo = AddressOf statusTo
Dim percToSub As delegatePercTo = AddressOf percTo
' statusToSub.Invoke("Test")
percToSub.Invoke(50)
End Sub
It doesn't work. I get an exception:
InvalidOperationException
I got the text in english and can't translate it to english very well but I think something like:
The access to the control, created by another thread from another thread is not allowed.
I'm using Visual Studio 2008 Express + VB 2.0.
Thank you!
This is due to cross-thread UI access which is disallowed (but for every UI access, so your other code shouldn’t work either!). The easiest solution is to use BeginInvoke when required:
Public Sub statusTo(ByVal value As String)
If InvokeRequired Then
BeginInvoke(New Action(Of String)(AddressOf statusTo))
Return
End If
_view.ProgressStatusText = value
End Sub
Furthermore, #vulkanino’s comment is spot-on: your calls should be direct method calls, not delegate invocations.
Dim statusToSub As **new** delegateStatusTo(AddressOf WriteToDebug)
statusToSub.Invoke("Test")
Dim percToSub As **new** delegatePercTo (AddressOf percTo)
percToSub.Invoke(50)
It looks like you are attempting to access UI controls from the DoWork event handler. Remember, that event handler is running on a worker thread. You are not allowed to touch any UI control from a thread other than the one that created it. There is a ProgressChanged event that will be marshaled onto the UI thread automatically upon calling ReportProgress. You safely update the UI from this event.
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!