vb.net HttpWebRequest BeginGetResponse parameters - multithreading

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.

Related

Unable to find a way to store data into an array inside a class module

I've been wrestling with this problem for a while. My problem is that I have a bunch of JSON data and I want to represent it as objects.
Arrays are problematic.
I create a class module such as FancyCat with a public Name as String for its name.
Then I can set this with
Dim MyFancyCat as FancyCat
Set MyFancyCat = new FancyCat
FancyCat.Name = JSONData("Name")
I've typed that from memory but I think it's correct. Anyhoo, it works fine.
The problem is that a fancy cat has several pairs of socks. The number of socks is variable.
In vba you cannot for some reason have a public array. So this code is illegal:
public Socks() as FancySock 'Illegal
Looking on SO I found two solutions, one, to make it private and use a property to access it, and the other, to declare it as Variant and then stick an array into it later.
My approach to populating this array, is to examine the JSON array to get the Count, and then to ReDim the array to match and then populate it.
The problem is my ReDim statement refuses to work.
It seems I cannot redim a property, I get an error. And I also get an error trying to redim the public variant field. My ReDim works OK if I declare a local array and redim it, so potentially I could do that and then assign it to the property... but it just seems bizarre that I can't redim it directly.
Any idea why it's not working?
With the Variant approach above my code is:
ReDim MyFancyCat.Socks(socksLength) As FancySocks
And in the FancyCat class module:
public Socks As Variant
I get Method or Data Member Not Found.
The error for the other approach was different but I rejigged all my code to try the second approach so I am not sure what it was.
Edit: I'm gonna explain what I am trying to do a bit more clearly. I have some JSON data coming in, and I want to store it as an object hierarchy.
In C# I would do this (pseudo code without linq shortcuts):
var myData = ReadJsonData(); // Produces a kind of dictionary
var myFancyCat = new FancyCat();
myFancyCat.Name = myData["Name"];
myFancyCat.Age = myData["Age"];
myFancyCat.Socks = new List<FancySock>();
foreach (var sock in myData["Socks"])
{
myFancyCat.Socks.Add(sock);
}
In excel I want to do the same thing.
So I make a class module for FancyCat and FancySock and give FancyCat public members for Name, Age etc but then I also want an array of socks that my cat owns. I wanted to do this with strongly typed references, e.g. my c# code above I can do:
myFancyCat.Socks[0].Colour // Intellisense works, shows colour as a property
However it seems in excel you can't have publicly declared arrays. So you can get around this according to the comments by declaring it as variant and then sticking an array in anyway, but you would lose the intellisense. Or you can use a get/let property which kinda works but is more fiddly as it seems you can't actually expose an array using a get/let you have to have it take an index and expose elements individually.
So at this point I am thinking forget the strongly typed it's not happening, perhaps use a collection?
The FancySock class may have further nested arrays within it. I've read that there's no ByRef for arrays (at least, not completely - I think you can get an array ByRef but not set one?). I am not sure if that would create problems with trying to set it.
But ultimately, I just want to end up with my JSON data represented easily in an OO way, so that in my excel ultimately I can just do
myFancyCat.Name or myFancyCat.Socks.Count or myFancyCat.Socks(1).Colour etc
It seems much harder than it looks to simply deserialise JSON into 'objects' in vba.
Please, try the next way:
Insert a class module, name it FancyCat and copy the next code:
Option Explicit
Private arrL As Object
Public myName As String, myAge As Long
Public Sub Class_Initialize()
Set arrL = CreateObject("System.Collections.ArrayList")
End Sub
Public Property Let Name(strName As String)
myName = strName
End Property
Public Property Let Age(lngAge As String)
myAge = lngAge
End Property
Public Property Let SocksAdd(sMember)
arrL.Add sMember
End Property
Public Property Get Socks() As Variant
Socks = arrL.toarray()
End Property
Use it in the next testing Sub:
Sub testClassDictListArray()
Dim myFancyCat As New FancyCat, myData As Object
Dim arrSocks, sock
Set myData = CreateObject("Scripting.Dictionary") 'this should be the dictionary returned by ParseJSON
myData.Add "Name", "John Doe": myData.Add "Age", 35
myData.Add "Socks", Array("Blue", "White", "Red", "Green", "Yellow")
myFancyCat.Name = myData("Name")
myFancyCat.Age = myData("Age")
For Each sock In myData("Socks")
myFancyCat.SocksAdd = sock
Next sock
arrSocks = myFancyCat.Socks
Debug.Print Join(arrSocks, "|")
End Sub
I am not sure I perfectly understand the scenario you try putting in discussion...
If you want to benefit of instellisense suggestions, I will tell you what references to be added. Even, I will send two pieces of code to automatically add the necessary references (I mean, Scripting.Dictionary and ArrayList`).
Please, test it and send some feedback.
In your class:
Private m_Name As String
Private m_Socks() As String
Public Property Let Name(Name As String)
m_Name = Name
End Property
Public Property Get Name() As String
Name = m_Name
End Property
Public Sub SetSize(Quantity As Long)
ReDim m_Socks(1 To Quantity)
End Sub
Public Property Let Socks(Index As Long, Sock As String)
m_Socks(Index) = Sock
End Property
Public Property Get Socks(Index As Long) As String
Socks = m_Socks(Index)
End Property
In a regular module:
Sub UseFancyCat()
Dim MyFancyCat As FancyCat
Set MyFancyCat = New FancyCat
MyFancyCat.Name = "Fancy Name"
MyFancyCat.SetSize 2
MyFancyCat.Socks(1) = "Sock1"
MyFancyCat.Socks(2) = "Sock2"
Debug.Print MyFancyCat.Name
Debug.Print MyFancyCat.Socks(1)
Debug.Print MyFancyCat.Socks(2)
End Sub

How to solve "object converted to string" error in B4A

Sub Process_Globals
'These global variables will be declared once when the application starts.
'These variables can be accessed from all modules.
Type DBResult (Tag As Object, **Columns As Map**, Rows As List)
Type DBCommand (Name As String, Parameters() As Object)
Private const rdcLink As String = "http://192.168.8.100:17178/rdc"
End Sub
This is the methods for process globals . Here columns As Map is initialized .
However the line bolded in the below code gives an error as , "Object converted to String. This is probably a programming mistake. (warning #7)"
Sub GetRecord
Dim req As DBRequestManager = CreateRequest
Dim cmd As DBCommand = CreateCommand("selectAllNames", Null)
Wait For (req.ExecuteQuery(cmd, 0, Null)) JobDone(j As HttpJob)
If j.Success Then
req.HandleJobAsync(j, "req")
Wait For (req) req_Result(res As DBResult)
'work with result
'req.PrintTable(res)
***Log(res.Columns)***
ListViewListTable.Clear
For Each row() As Object In res.Rows
Dim oBitMap As Bitmap
Dim buffer() As Byte
buffer = row(res.Columns.Get("gambar"))
oBitMap = req.BytesToImage(buffer)
ListViewListTable.AddTwoLinesAndBitmap(row(1), "See more...", oBitMap)
Next
Else
Log("ERROR: " & j.ErrorMessage)
End If
j.Release
End Sub
So what should I do to remove the error?
If columns is a map? (which it looks to be?).
Then to display the columns you can use this:
For Each MyKey As String in res.Columns.Keys
log("Key name = " & MyKey)
log("Key value = " & res.Columns.Get(MyKey))
Next

Is it possible to group function calls in VBA?

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.

Call a method object with arguments that returns variable

How can I call a method passing multiple parameters returning a variable?
I have a class called cExcelTable. Inside I have a method:
Public Function GetColumnNumberByColumnName(strColumnName As String)
Dim resultColumnFound As Integer
'... Here i have the code which find the right column
'... then i return the result as an integer
GetColumnNumberByColumnName = resutColumnFound
End Function
How can I call this method and get the returning value?
I want the equivalent of this:
Dim myTable As New cExcelTable
Dim i as Integer
myTable.Workbook = "file.xlsx"
myTable.Sheet = "TestingSheet"
myTable.Table = "TabDatas"
'... And here is my bug that i don't know how to solve
i = myTable.GetColumnNumberByColumnName("TOTAL")
I know that for a method not have returning values I must use Call but what about a method that return a value?
SOLVED
Thanks to PeterT and Domenic in comments i found after a night sleep that my mistake was not the calling of the method but misspelled of two variables inside my method and adding the type it should return
So for a new one who go inside this post the answer is
Inside the method you should use this
'Add the type the method it should return "As Integer"
Public Function nameOfTheMethod(parameterNumber1 As String) As Integer
Dim resultColumnFound As Integer
'... type your code
'... then return the result with the correct spelling
GetColumnNumberByColumnName = resultColumnFound
End Function
And then inside your code you can call your method like this
Dim myObject As New cMyClass 'cMyClass is the name of your class and myObject is the name of your object you are creating
Dim i as Integer
i = myObject.nameOfTheMethod("test")

Async VBA Function

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.

Resources