In my application I make a number of webrequests to get information from an external server. A typical call takes only 5-10 seconds but during busy times it could take 20-30 seconds. I am struggling with providing user feedback during these calls to let the user know we are still processing. I have tried timers on the form. I have tried external timers (cpptimer.fll) as well. But, once the httpRequest fires I get no UI updates.
oHttp = CREATEOBJECT("wwHTTP")
lcHTML = oHTTP.HTTPGet(this._transactionRequest, '', '', cHTMLResult)
*- 5-30 seconds here where I get no UI updates.
oHttp.HTTPClose() && Close the connection
this._transactionResponse = ALLTRIM(lcHTML)
Is there a better way to handle this so that I can update the user? Even a simple count down timer would be great. Thanks!
Look at the OnHttpBufferUpdate()
event you can hook.
You can use BindEvent or subclass wwHttp to implement this progress method and provide UI feedback.
+++ RIck ---
Thank you Rick. I used the BindEvent method and just for testing purposes just ended up writing a log message and I added a DoEvents in the "httpBufferUpdateHandler" method I created.
As I somewhat expected. I got my log entry only after the data started receiving. And this makes sense. Its the 20 some second after the HttpRequest has been received on the server and it is processing that my client is not getting any updates and I am not able to get a screen refresh. once the server is done processing all the data comes back in one read. About 1100 bytes.
I guess that is the nature of a synchronous webrequest call.
Looks like the only way I will be able to update the user is if I spawn a new thread where I then handle the httpGet()?
Yup unfortunately that's what you have to do and it's not super straight forward to do with FoxPro.
If I had to do this I would probably use wwDotnetBridge.InvokeMethodAsync() to write a small wrapper HTTP request in .NET and let that run asynchronously and return a value.
Actually come to think of it the Client Tools ship with an internal HTTP client that makes it easy to make simple .NET HTTP calls:
CLEAR
do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
*** HttpClient is part of wwDotnetBridge.dll for full Client Tools/Web Connection
loHttp = loBridge.CreateInstance("Westwind.WebConnection.HttpClient")
loCallback = CREATEOBJECT("HttpCallback")
loBridge.InvokeMethodAsync(loCallBack, loHttp,"GetUrl","https://west-wind.com/files/wwclient.zip")
RETURN
DEFINE CLASS HttpCallback as AsyncCallbackEvents
*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lvResult,lcMethod)
WAIT WINDOW "File received. Size: " + TRANSFORM(LEN(lvResult))
ENDFUNC
* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC
ENDDEFINE
This works...
+++ Rick ---
Rick, that works indeed. I can now process UI feedback. Thank you!
Follow up question... with regards to exceptions. I see the call back object must have at least two methods: oncompleted and onerror. Is there anything I can provide on the call back object that would be read by InvokeMethodAsync to cancel the request and return? Or, is this even necessary?
In other words, the request is made and processing. But, it goes off into the weeds. (this is beyond my control) I would like for the user to be able to cancel the request. When all the object go out of scope or are released and then finally the httprequest returns (but at this point I don't care anymore and want to disregards any information that might have been returned) is it "safe" to assume the windows garbage collector will get rid of the thread? I am trying this and not seeing any errors. But I wonder if I am creating a memory hole or something?
You can't cancel, but you can certainly ignore the result by adding and then setting a property value on the loCallback
object.
The way this works is that wwDotnetBridge internally starts a new thread and makes the call, waits for completion then returns. Since the actual HTTP call in this case is not actually (I'm running sync on a separate thread) there's no way to cancel.
If you're just making a plain HTTP GET request, you can also use the native .NET WebClient object and call WebClient.DownloadStringAsync() directly in the framework. That gives you back a Task
instance on which you can cancel.
There's more info on how to use Task
based .NET APIs with wwDotnetBridge here:
Calling async/await methods with wwDotnetBridge
In fact the example in that post uses WebClient.DownloadStringAsync()
as an example:
do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = CreateObject("wwDotNetBridge","V4")
loClient = loBridge.CreateInstance("System.Net.WebClient")
*** execute and returns immediately
loTask = loBridge.InvokeMethod(loClient,"DownloadStringTaskAsync","https://west-wind.com")
? loTask && object
*** Do other stuff...
*** Check and Wait for Result dynamically - non-blocking
llDone = loBridge.GetProperty(loTask,"IsCompleted")
DO WHILE (!llDone)
WAIT WINDOW TIMEOUT 0.1
llDone = loBridge.GetProperty(loTask,"IsCompleted")
? "waiting..."
ENDDO
*** Waits for completion if not finished yet - blocks
lcHtml = loBridge.GetProperty(loTask,"Result")
? SUBSTR(lcHtml,1,1000)
+++ Rick ---
As another follow up I ended up adding a new method to wwDotnetBridge
to make it easier to use .NET Async methods using the same callback mechanism used with threads, but get true async behavior from .NET with a FoxPro callback:
wwDotnetBridge.InvokeTaskMethodAsync
Here's an example:
do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = CreateObject("wwDotNetBridge","V4")
loClient = loBridge.CreateInstance("System.Net.WebClient")
loCallback = CREATEOBJECT("HttpCallback")
*** execute and returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, loClient,"DownloadStringTaskAsync","https://west-wind.com")
*** Hit immediately
? "Done..."
RETURN
DEFINE CLASS HttpCallback as AsyncCallbackEvents
*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lvResult,lcMethod)
? "File received. Size: " + TRANSFORM(LEN(lvResult))
? SUBSTR(lvResult,1,1500)
ENDFUNC
* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod,lcMessage
ENDFUNC
ENDDEFINE
Given that there are now more and more async only APIs in .NET this makes it a little easier to consume these methods truly asynchronously as opposed to waiting for completion.
Note: This is not in the current version but will in 7.10+
+++ Rick ---
Rick, I was out of this project for a bit. But I am now back and trying out your suggestions. I am using the client tools and I believe the latest version I have is 7.07. I implemented the logic very similar to your example and when I run this in the VFP environment it works great. However, when I compile the code and run an exe it just hangs... I never seem to get the OnCompleted event from the callback object. Any ideas?
loBridge = GetwwDotnetBridge()
loHttp = loBridge.CreateInstance("Westwind.WebConnection.HttpClient")
loCallback = CREATEOBJECT("HttpCallback")
loCallback._stillwaiting = 1
loCallback._TransactionResponse = ''
loBridge.InvokeMethodAsync(loCallback, loHttp, "GetUrl", objPayment._transactionRequest)
DO WHILE loCallback._stillwaiting > 0
*note, I never see this wait window when compiled in EXE.
*this wait window shows fine when running in VFP enviro
WAIT WINDOW 'waiting on response..' NOWAIT
ENDDO
*-This returns fine and almost immediately when run in VFP enviro
*-This never gets this far when compiled into EXE
? ALLTRIM(loCallback._TransactionResponse)
DEFINE CLASS HttpCallback as AsyncCallbackEvents
_stillwaiting = 0
_TransactionResponse = ''
FUNCTION OnCompleted(lvResult,lcMethod)
this._TransactionResponse = ALLTRIM(lvResult)
this._stillwaiting = 0
ENDFUNC
FUNCTION OnError(lcMessage,loException,lcMethod)
this._stillwaiting = 0
this._TransactionResponse = lcMessage
ENDFUNC
ENDDEFINE
Put some error checking into that code if it fails.
It looks to me like perhaps you're not using the same version of the wwDotnetBridge.dll
?
No idea why otherwise it would hang and not work unless the URL is hanging. Note that you should make sure that the Callback object sticks around. Don't make it LOCAL
or PRIVATE
but attach to some object that will still be in scope. If it releases the callback will never fire.
The reason that might be happening with the EXE vs. the IDE is speed. The EXE might be running faster and the timing is such that it worked in the IDE (perhaps with debugging on) where the object in the EXE goes away faster.
Also check your MyApp.exe.config
for the EXE vs. what you have in vfp9.exe.config
(if anything).
+++ Rick ---