I have a VBA function that is supposed to get some information from the user's cell, make a POST request with that info, then print the response in the output cell.
It's required that the user be able to make about 2000 requests at a time, so I thought to make the requests async to help improve performance.
As it stands right now, I have a function ConnectToAPI that makes the asynchronous request, then passes the response off to a callback function. The problem I'm having is that the data lives in the callback function, but I need it in the query function in order to return it.
Function Query(ID, quote, field)
Application.Volatile
Query = ConnectToAPI(ID)
Some logic with parsed data from callback
End Function
Function ConnectToAPI(ID)
Dim Request As New WebRequest
Dim Client As New WebClient
Client.BaseUrl = "http://www.endpoint.com"
Dim Wrapper As New WebAsyncWrapper
Dim Wrapper.Client = Client
Dim Body As New Dictionary
Body.Add "ID", ID
Set Request.Body = Body
Request.Method = HttpPost
ConnectToAPI = Wrapper.ExecuteAsync Request, "CallbackFunction"
End Function
Function CallbackFunction
Callback = Parsed Data
End function
So ultimately in the query function, I want to write
Query = (Parsed Data From the Callback)
How can I pass the data from the callback back up to query?
It is important that the cell have the Query function in it. The data updates frequently, so we want clients to be able to calculate the workbook to get the newest data.
With what I currently have, my thought process is that the callback will pass the data back to ConnectToAPI, then that will be passed up to Query. However, my function returns 0 and I think this might be that the parsed data is not available once the function tries to return.
For reference, I am using the VBA-Web library
https://github.com/VBA-tools/VBA-Web
VBA-Web/src/WebAsyncWrapper.cls
WebAsyncWrapper.ExecuteAsync has an optional parameter: CallbackArgs. Use this parameter to pass back you an ID or a cell address.
ExecuteAsync has an example callback function that receives an Array of arguments.
Here is how you can get the information back to the function for processing.
Sub ConnectToAPI(ID As Variant, quote As Variant, field As Variant, CellAddress As Variant)
Dim Request As New WebRequest
Dim Client As New WebClient
Client.BaseUrl = "http://www.endpoint.com"
Dim Wrapper As New WebAsyncWrapper
Dim Body As New Dictionary
Body.Add "ID", ID
Set Request.Body = Body
Request.Method = HttpPost
Set Wrapper.Client = Client
Wrapper.ExecuteAsync Request, "Callback", Array(ID, CellAddress)
End Sub
Public Function Callback(Response As WebResponse, Args As Variant)
Dim ID As Variant, CellAddress As Variant
ID = Args(0)
CellAddress = Args(1)
With Worksheets("Web Requests")
.Range(CellAddress).Value = Response
.Range(CellAddress).Offset(0, 1).Value = ID
End With
End Function
MSDN - Application.Volatile Method (Excel)
Marks a user-defined function as volatile. A volatile function must be recalculated whenever calculation occurs in any cells on the worksheet. A nonvolatile function is recalculated only when the input variables change. This method has no effect if it's not inside a user-defined function used to calculate a worksheet cell.
I would not recommend trying to have a UDF that can be used as a worksheet function to return the web-requests. Application.Volatile will cause all 2000 queries to refresh every time a value is changed. When the first query updates all the other queries will refresh. This will cause an infinite loop and crash the application.
Function Query(ID, quote, field)
Application.Volatile
Query = ConnectToAPI(ID)
Some logic with parsed data from callback
End Function
Using the Worksheet_Change event would give the users the ability to update the information without the problems associated with Application.Volatile.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Columns("A")) Is Nothing Then
If Target.Count = 1 Then
Debug.Print Target.Value, Target.Address
End If
End If
End Sub
I ended up populating a dictionary with the response values from the API call, then recursively calling the query function in the callback.
Query checks if the response value is in the dictionary, if it is, then it returns it. If not, it connects to the api, the callback puts the value in the dictionary, and also calls the query function again.
Related
Hello I am running a VBA script in excel 365, and am trying to make an object (day) store a list of custom objects (job) but when trying to input the array of objects I run into an error.
I am using Property handlers to access the private internal array for the day object.
This is how it gets ran, the jobTest function only creates some arbitrary job objects.
Sub dayTest()
jobTest
Debug.Print testJob.GNum
Dim foo As New day
foo.DateBlock = DateValue("14 / 03 / 2020")
Debug.Print TypeName(testJob)
Dim x As Variant
x = foo.JobsForDay(0, testJob)
End Sub
When running this code I get the error:
Wrong number of arguments or property assignment
But when looking at my access methods I created, these don't seem to apply.
Private mDateStored As Date
Private mNumJobs As Long
Private mJobsForDay() As job
Property Get JobsForDay(val As Long) As Variant
JobsForDay = mJobsForDay(val)
End Property
Property Let JobsForDay(val As Long, jobObj As Variant)
mNumJobs = mNumJobs + 1
ReDim Preserve mJobsForDay(mNumJobs)
mJobsForDay(val) = jobObj
End Property
I am trying to call the Let function, however the compiler throws a generic "Expected =" error when I run the code as
foo.JobsForDay(0, testJob)
Which is why I have the variant x accepting all input from the method.
Thanks for any help!
I'm working in Excel with VBA to collect data for a table I'm building I have to go out to a TN3270 emulator to get it. In order to work with with the emulator I have to define a few objects to do the work. I also have a few helper functions that are used by multiple functions to navigate to different screens in the emulator. So far in order to use them I have had to copy the object definitions into those functions to get them to work. This works most of the time but occasionally (and in a way I cant predictably replicate) I get an error when the helper is recreating a particular object to use.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
go_to_screen_2
#grab and put data back in excel
Else
go_to_screen_3
#grab and put data back in excel
End If
go_to_screen_1
#next loop logic
End Sub
Public Sub go_to_screen_1()
Dim TN_Emulator As Object
#the next step occasionally throws the error
Set TN_Emulator = CreateObject("TN_Emulator.Program")
#send instructions to the emulator
End Sub
Is there a way to import the existing objects (that get created and used without any errors) without redefining them into the helper functions to avoid this problem? I have tried searching in google but I don't think I'm using the right search terms.
First thanks goes to #JosephC and #Damian for posting the answer for me in the comments.
From JosephC 'The Key words you're looking for are: "How to pass arguments to a function".', and he provided the following link ByRef vs ByVal describing two different ways to pass arguments in the function call.
And from Damian the solution to my immediate concern. Instead of declaring and setting the objects that will be used in body of the helper function. Place the object names and types in the parentheses of the initial helper name, and when calling the helper from the other function also in the parentheses, shown below.
Option Explicit
Public Sub gather_data()
Dim TN_Emulator As Object
Dim Workbook As Object
Set TN_Emulator = CreateObject("TN_Emulator.Program")
Set Workbook = ActiveWorkbook
Dim string_from_excel As String
#for loop to go through table rows
#put value in string_from_excel
If string_from_excel = some condition
Call go_to_screen_2(TN_Emulator)
#grab and put data back in excel
Else
Call go_to_screen_3(TN_Emulator)
#grab and put data back in excel
End If
Call go_to_screen_1(TN_Emulator)
#next loop logic
End Sub
Public Sub go_to_screen_1(TN_Emulator As Object)
#send instructions to the emulator
End Sub
I believe I understood the instructions correctly, and have successfully tested this for my-self. I also passed multiple objects in the helper function definition and calls as needed for my actual application, in the same order each time Ex.
Sub go_to_screen_1(TN_Emulator As Object, ConnectionName As Object)
and
Call go_to_screen_1(TN_Emulator, ConnectionName)
I have a function that looks something like:
Public Function GetData(DataType As String) As String
Dim Client As New WebClient
Client.BaseUrl = "http://url/to/get/data"
Dim Response As New WebResponse
Set Response = Client.GetJson(DataType)
GetInstruments = Response.Data("data")
End Function
It's a simple HTTP GET that returns a value based on an argument.
My problem is that I'm trying to execute this function for many different cells at once in Excel (i.e. =GetData(A$1)) that leads to hundreds of HTTP calls which is very slow.
Is there a way that in VBA that I am able to intercept function calls so I can then make a single and quick HTTP call and then return all the data at once?
You can use global variables in a module to cache and reuse alread downloaded data.
First easy to digest example using simple Collection:
Private someCollection As Collection
Public Function GetData() As Integer
' Make sure that data is already read/created
If someCollection Is Nothing Then
' If we didn't get any data, then get it
Set someCollection = New Collection
someCollection.Add (1)
End If
' Get data :)
GetData = someCollection(1)
End Function
Now, applying this logic to your problem you could do:
Private Response As WebResponse
Public Function GetData(DataType As String) As String
' You can alter check to see if URL has changed.
' In order to do that just store URL in some global variable
If Response Is Nothing Then
Dim Client As New WebClient
Client.BaseUrl = "http://url/to/get/data"
Set Response = Client.GetJson(DataType)
End If
GetInstruments = Response.Data("data")
End Function
Of course, all this code goes into module.
I want to call a bloomberg function inside a UDF, hence, I can't use
Cells(x,y).Formula = bdp(equity, field)
Which is what I normally see. I've tried Aplication.Run and WorkSheetFunction without success, are there other ways so that I can call this function?
A UDF isn't setting formulas, nor is it even setting a value - it is returning a value - so just return the value of the Bloomberg function as the result of your UDF.
For example:
Public Function MyUDF(....) As Variant
'...
'whatever calcs you are doing to determine "equity" and "field"
'(or maybe they are parameters)
'...
MyUDF = bdp(equity, field)
End Function
I'm doing threaded webrequests. How do I pass the parameter 'index' from the Start() sub to GetResponseCallback()?
The two subs:
Shared Sub Start(ByVal index As Integer)
Dim request As HttpWebRequest = CType(WebRequest.Create("http://sternbud.com/login/checklogin.php"), HttpWebRequest)
request.ContentType = "application/x-www-form-urlencoded"
request.Method = "POST"
Debug.Print(index & ">" & AccountArray(dictThread.Keys(index)))
Dim result As IAsyncResult = CType(request.BeginGetRequestStream(AddressOf GetRequestStreamCallback, request), IAsyncResult)
allDone.WaitOne()
End Sub
Private Shared Sub GetRequestStreamCallback(ByVal asynchronousResult As IAsyncResult)
Dim request As HttpWebRequest = CType(asynchronousResult.AsyncState, HttpWebRequest)
Dim postStream As Stream = request.EndGetRequestStream(asynchronousResult)
Dim postData As [String] = "myusername=" & AccountArray(AccountIndex) & "&mypassword=test"
Dim byteArray As Byte() = Encoding.UTF8.GetBytes(postData)
postStream.Write(byteArray, 0, postData.Length)
postStream.Close()
Dim result As IAsyncResult = CType(request.BeginGetResponse(AddressOf GetResponseCallback, request), IAsyncResult)
End Sub
You pass it as an Object in the last parameter to BeginGetRequestStream. Currently you have:
Dim result As IAsyncResult = CType(request.BeginGetRequestStream(AddressOf GetRequestStreamCallback, request), IAsyncResult)
You're passing the result in the state parameter, and that value gets set in the AsyncState property of the passed IAsyncResult.
If you want to pass two values, you have some choices:
Create a new object that has the result and index values as separate properties.
Create an array of Object where the first item is the request and the second is the index. You can then get the AsyncState property, case it to an object array, and peel out the items.
Create a Tuple from your two values, and pass that Tuple in the state parameter.
I prefer the second because it's so easy, but creating a Tuple is cleaner (i.e. type-safe) and almost as easy. I have a C# example of the second method at https://stackoverflow.com/a/4555766/56778.