2

I am tying to set-up a Excel VBA project to readout individual survey responses into a form in Excel for some calculations and then PDF reporting.

However I have great difficulty to deploy the .NET library (SurveyMonkeyApi) to be available for reference in VBA.

I have set up a VisualStudio project to test that way , and I can install it for that specific VS project (through NuGet PM). But the library is not made available for Excel on that machine.

I have downloaded (on another machine) the libraries through standalone NuGet and they download OK but then I am at loss on how to register for Excel VBA access. On top of it there is a dependency on NewtonsoftJson library too (which downloaded automatically on both occasions).

Good advice appreciated!

Community
  • 1
  • 1
Mats Olsson
  • 101
  • 1
  • 2
  • 10

4 Answers4

2

I just saw this now - is there a feature for StackOverflow to alert me when a comment is added or a question answered, so I know to look back?

Here is starting code:

Option Explicit
Public Const gACCESS_TOKEN As String = "xxxxxxxxxxxxxxxxxxxxxx"
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
' for a JSON parser see https://code.google.com/p/vba-json/

Public Sub test()
Dim vRequestBody  As Variant, sResponse As String, sSurveyID As String
sSurveyID = "1234567890"

vRequestBody = "{""survey_id"":" & """" & sSurveyID & """" _
              & ", ""fields"":[""collector_id"", ""url"", ""open"", ""type"", ""name"", ""date_created"", ""date_modified""]" _
              & "}"
sResponse = SMAPIRequest("get_collector_list", vRequestBody)

End Sub
Function SMAPIRequest(sRequest As String, vRequestBody As Variant) As String
Const SM_API_URI As String = "https://api.surveymonkey.net/v2/surveys/"
Const SM_API_KEY As String = "yyyyyyyyyyyyyyyyyyyyyyyy"
Dim bDone As Boolean, sMsg As String, sUrl As String, oHttp As Object ' object MSXML2.XMLHTTP
Static lsTickCount As Long

If Len(gACCESS_TOKEN) = 0 Then
   Err.Raise 9999, "No Access token"
End If
On Error GoTo OnError

sUrl = SM_API_URI & URLEncode(sRequest) & "?api_key=" & SM_API_KEY
   'Debug.Print Now() & " " & sUrl
Application.StatusBar = Now() & " " & sRequest & " " & Left$(vRequestBody, 127)
Set oHttp = CreateObject("MSXML2.XMLHTTP") ' or "MSXML2.ServerXMLHTTP"

Do While Not bDone ' 4.33 offer retry
   If GetTickCount() - lsTickCount < 1000 Then ' if less than 1 sec since last call, throttle to avoid sResponse = "<h1>Developer Over Qps</h1>"
      Sleep 1000 ' wait 1 second so we don't exceed limit of 2 qps (queries per second)
   End If
   lsTickCount = GetTickCount()
   'Status  Retrieves the HTTP status code of the request.
   'statusText Retrieves the friendly HTTP status of the request.
   'Note   The timeout property has a default value of 0.
   'If the time-out period expires, the responseText property will be null.
   'You should set a time-out value that is slightly longer than the expected response time of the request.
   'The timeout property may be set only in the time interval between a call to the open method and the first call to the send method.
RetryPost:  ' need to do all these to retry, can't just retry .Send apparently
   oHttp.Open "POST", sUrl, False   ' False=not async
   oHttp.setRequestHeader "Authorization", "bearer " & gACCESS_TOKEN
   oHttp.setRequestHeader "Content-Type", "application/json"

   oHttp.send CVar(vRequestBody)     ' request body needs brackets EVEN around Variant type
   '-2146697211   The system cannot locate the resource specified. => no Internet connection
   '-2147024809   The parameter is incorrect.
   'String would return {"status": 3, "errmsg": "No oJson object could be decoded: line 1 column 0 (char 0)"} ??
   'A Workaround would be to use parentheses oHttp.send (str)
   '"GET" err  -2147024891   Access is denied.
   '"POST" Unspecified error = needs URLEncode body? it works with it but

   SMAPIRequest = oHttp.ResponseText
   'Debug.Print Now() & " " & Len(SMAPIRequest) & " bytes returned"
   sMsg = Len(SMAPIRequest) & " bytes returned in " & (GetTickCount() - lsTickCount) / 1000 & " seconds: " & sRequest & " " & Left$(vRequestBody, 127)

   If Len(SMAPIRequest) = 0 Then
      bDone = MsgBox("No data returned - do you wish to retry?" _
            & vbLf & sMsg, vbYesNo, "Retry?") = vbNo
   Else
      bDone = True ' got reply.
   End If
Loop ' Until bdone

   Set oHttp = Nothing
   GoTo ExitProc

OnError:   ' Pass True to ask the user what to do, False to raise to caller
   Select Case MsgBox(Err.Description, vbYesNoCancel, "SMAPIRequest")
   Case vbYes

      Resume RetryPost
   Case vbRetry
      Resume RetryPost
   Case vbNo, vbIgnore
      Resume Next
   Case vbAbort
      End
   Case Else
      Resume ExitProc ' vbCancel
   End Select
ExitProc:
End Function


Public Function URLEncode(StringVal As String, Optional SpaceAsPlus As Boolean = False) As String
Dim StringLen As Long
 StringLen = Len(StringVal)
 If StringLen > 0 Then
   ReDim result(StringLen) As String
   Dim i As Long, CharCode As Integer
   Dim Char As String, Space As String
   If SpaceAsPlus Then Space = "+" Else Space = "%20"
   For i = 1 To StringLen
      Char = Mid$(StringVal, i, 1)
      CharCode = Asc(Char)
      Select Case CharCode
      Case 97 To 122, 65 To 90, 48 To 57, 45, 46, 95, 126
      result(i) = Char
      Case 32
      result(i) = Space
      Case 0 To 15
      result(i) = "%0" & Hex(CharCode)
      Case Else
      result(i) = "%" & Hex(CharCode)
      End Select
   Next i
   URLEncode = Join(result, "")
End If
End Function

EDIT 23-APRIL add more code.

the Me. comes from code in a Userform.

Set jLib = New JSONLib
vRequestBody = "{"
If Me.txtDaysCreated > "" Then
   vRequestBody = vRequestBody & JKeyValue("start_date", Format$(Now() - CDbl(Me.txtDaysCreated), "yyyy-mm-dd")) & ","
End If
If Me.txtTitleContains > "" Then
' title contains "text", case insensitive
vRequestBody = vRequestBody & JKeyValue("title", Me.txtTitleContains) & ","
End If
vRequestBody = vRequestBody _
   & JKeyValue("fields", Array("title", "date_created", "date_modified", "num_responses", _
      "language_id", "question_count", "preview_url", "analysis_url")) & "}"


'returns in this order: 0=date_modified  1=title  2=num_responses  3=date_created   4=survey_id
' and in date_created descending
sResponse = GetSMAPIResponse("get_survey_list", vRequestBody)

------------------------------------------
Function JKeyValue(sKey As String, vValues As Variant) As String
      Dim jLib As New JSONLib
 JKeyValue = jLib.toString(sKey) & ":" & jLib.toString(vValues)
 Set jLib = Nothing
End Function

Edit 25-April overview of VBA code to get the data

This is covered in the SM documentation, but I'll sketch how that looks in VBA. the response to get_survey_details gives you all the survey setup data. Use Set oJson = jLib.parse(Replace(sResponse, "\r\n", " ")) to get a json object.
Set dictSurvey = oJson("data")
gives you the dictionary so you can get data like dictSurvey("num_responses"). I take it you know how to index into dictionary objects to get field values.

Set collPages = dictSurvey("pages") 

gives you a collection of Pages. The undocumented field "position" gives you the order of pages in the survey UI.

For lPage = 1 To collPages.Count
   Set dictPage = collPages(lPage) 
Set collPageQuestions = dictPage("questions") ' gets you the Qs on this page
For lPageQuestion = 1 To collPageQuestions.Count
     Set dictQuestion = collPageQuestions(lPageQuestion) ' gets you one Q
Set collAnswers = dictQuestion("answers") ' gets the QuestionOptions for this Q
        For lAnswer = 1 To collAnswers.Count
           Set dictAnswer = collAnswers(lAnswer) ' gets you one Question Option

etc etc

Then given the number of responses from above, loop through the respondents 100 at a time - again see the SM doc for details of how to specify start and end dates to do incremental downloads over time. create a json object from the response to "get_respondent_list" Collect the fields for each respondent and accumulate a list of at most 100 respondent IDs. Then "get_responses" for that list.

Set collResponsesData = oJson("data")
For lResponse = 1 To collResponsesData.Count

If not IsNull(collResponsesData(lResponse)) then 
... get fields...
Set collQuestionsAnswered = collResponsesData(lResponse)("questions")
  For lQuestion = 1 To collQuestionsAnswered.Count
     Set dictQuestion = collQuestionsAnswered(lQuestion)
        nQuestion_ID = CDbl(dictQuestion("question_id"))
        Set collAnswers = dictQuestion("answers") ' this is a collection of dictionaries
        For lAnswer = 1 To collAnswers.Count

           On Error Resume Next ' only some of these may be present
           nRow = 0: nRow = CDbl(collAnswers(lAnswer)("row"))
           nCol = 0: nCol = CDbl(collAnswers(lAnswer)("col"))
           nCol_choice = 0: nCol_choice = CDbl(collAnswers(lAnswer)("col_choice"))
           sText = "": sText = collAnswers(lAnswer)("text")
           nValue = 0: nValue = Val(sText)  
           On Error GoTo 0

and save all those values in a recordset or sheet or whatever Hope that helps.

sysmod
  • 463
  • 3
  • 11
  • Thanks @sysmod I really appreciate your help. I will have a look at this. Give me a few more hours to digest this and I will come back to you! I thought that putting a @ in front of your "name" in the comments will alert you of replies etc.? – Mats Olsson Apr 22 '15 at 11:21
  • You have made my day, @sysmod! I have it working now! Even for a basic get_survey_list call. Though it seems a bit complicated building the vRequestBody string! This will be trickier than I thought. will JSON help in building the vRequestBody string too? Or is this just for "unpacking" the response I get back? – Mats Olsson Apr 22 '15 at 18:44
  • No, I don't get an email alert from Stackoverflow after posts. Is there some box I should have checked "alert me when someone replies" ? I use the .toString function in the JSONlib to form the body. eg Function JKeyValue(sKey As String, vValues As Variant) As String Dim jLib As New JSONLib JKeyValue = jLib.toString(sKey) & ":" & jLib.toString(vValues) Set jLib = Nothing End Function I need more space to reply, and cannot format comments as code, so I;ll add another answer. – sysmod Apr 23 '15 at 08:38
  • You'll need to patch the JSONlib - the owner may have done this too since I found it: in function ParseString change this: Case """", "\", "/" ' POB fix issue 22 in lib And in parseNumber: parseNumber = CDbl(Value) ' POB always Double, not Int, need not be decimal point in long number eg 623478675 and in sub skipChar While Index > 0 And Index <= Len(str) And InStr(vbCr & vbLf & vbTab & " ", Mid(str, Index, 1)) ' POB don't need vbCrLf as well – sysmod Apr 23 '15 at 08:44
  • I searched Help and found that hovering over the [surveymonkey] tag gives a popup with a 'subscribe' link to notify me of new posts with that tag. I'll try that. – sysmod Apr 23 '15 at 08:57
  • OK I have added your patches (one were already there) @sysmod. Thanks again for your help! Now on to trying to figure the whole Json thing out. The biggest issue now will be to match the "get_responses" to the "get_survey_details" to actually in the end wind up with the answer to a specifc question from a user. – Mats Olsson Apr 24 '15 at 11:37
  • Hey, that subscribe link worked! I was alerted to this new post in my Stack Exchange Inbox. (Stack Exchange? I thought this was StackOverflow) Now for your question: I need more space so will edit the reply. – sysmod Apr 25 '15 at 14:02
  • Again thanks @sysmod! You have saved my sanity ;-) and hours and hours of my life. I am now in a much better position to get this thing off the ground. Using disconnected recordsets seems another good idea from you. – Mats Olsson Apr 27 '15 at 15:12
1

I access the SM API in straight VBA. Just CreateObject("MSXML2.XMLHTTP") then issue calls and use the SimpleJsON JSONLib to parse it. If I wanted to access VB.Net code, I'd package it with ExcelDNA to create a XLL and that gives a straight Excel addin.

sysmod
  • 463
  • 3
  • 11
  • Thanks sysmod, this sounds encouraging! However I am not at all used to working with the MSXML2.XMLHTTP object. On top of that, all examples for authorization and requests are in Phyton, and I struggle (fail) with the conversion to VBA or VB.NET. A few code examples would be very helpful. – Mats Olsson Apr 21 '15 at 08:05
0

I would think you would need to add it into the References for your Excel project.

From the Ribbon, select, Tools, then References, then scroll through the list looking for something about SurveyMonkey API.

enter image description here

FreeMan
  • 5,660
  • 1
  • 27
  • 53
  • Yes that is what I intend to do but the problem is that it is not showing in the list, so I cannot select it. Further I am not sure I need to have it referenced there if I do "Late Binding", but how will I know if I have it correctly regoistered? I will try Regasm and update with my findings. – Mats Olsson Mar 31 '15 at 13:02
  • So I have tried to Regasm the libraries, and I think I was successful when I used the /tlb switch (generate type library file). I then could browse for the libraries from the reference dialog and select the tlb file (NB not the DLL file) and then I got an entry in the reference dialog. However when trying to acces the library in code I fail and looking at the Object Browser I see only the names of the libraries but content (Classes etc) – Mats Olsson Mar 31 '15 at 13:34
  • Sorry, @MatsOlsson, you're far deeper than I've gone with this. You'll have to wait for someone else to chime in. – FreeMan Mar 31 '15 at 13:38
0

So encouraged by @sysmod I have tried to do something in VBA directly. I have left out the JSON for now as I am already in trouble. The below is giving me "Developer Inactive" as a result, though I have another project in VB.NET where the same key and token works fine.

Public Sub GetSMList()

Dim apiKey As String
Dim Token As String
Dim sm As Object

apiKey = "myKey" 
Token = "myToken"

Set sm = CreateObject("MSXML2.XMLHTTP.6.0")

With sm
    .Open "POST", "https://api.surveymonkey.net/v2/surveys/get_survey_list", False
    .setRequestHeader "Authorization", "Bearer " & Token
    .setRequestHeader "Content-Type", "application/json"

    .send "api_key=" & apiKey

    result = .responseText
End With

End Sub
Mats Olsson
  • 101
  • 1
  • 2
  • 10