Catch event trigger from external application in Excel VBA - excel

I use VBA to automate an external application that recently changed their COM API. The new API loads files asynchronously (used to be synchronous) so I need to wait for the file loaded trigger before I continue when I try to load a file.
I have tried the methods listed on the Microsoft website (EX1, EX2) which were also part of an accepted answer on StackOverflow.
Below is the code I have in a class module named UCExternal to contain the external application object:
Public WithEvents obj As External.Application
Private fileLoaded As Boolean
Private Sub obj_OnFileLoaded(ByVal lLayer As Long, ByVal strUNCPath As String)
Debug.Print lLayer
Debug.Print strUNCPath
fileLoaded = True
End Sub
Public Sub LoadSingleFile(fileStr As String)
fileLoaded = False
obj.LoadFile 0, fileStr
Do
DoEvents
Loop Until fileLoaded
End Sub
And then this is what I had in a normal code module to run using a button on the sheet:
Sub TryLoadFile()
Dim extObj as New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub
The event code never seems to fire and instead the Do Loop just runs until Excel crashes. I don't know if there is a way to confirm the application actually sent the event trigger? I have read through the new documentation for the application and that is the event they say to wait for. I have reached out to them for help as well but I wasn't sure if there was something more general I may have been missing. I have not worked with events external to Excel in the past. If I just step through it using the debugger and manually exit the Do Loop eventually the rest of the code that works on the loaded file works as well, so it does load the file.

extObj needs to be declared outside of TryLoadFile, or it will go out of scope and get cleared as soon as TryLoadFile completes
Dim extObj as New UCExternal
Sub TryLoadFile()
Set extObj = New UCExternal
set extObj.obj = CreateObject("External.Application")
filePath = "path/to/file"
extObj.LoadSingleFile filePath
End Sub

Related

How to detect the event is trigger by end user? (ignore the event trigger by code)

This question is from one of my customers who is developing an excel web add-in, they would like to detect whether the event is trigger by end-user or trigger by the code.
They have registered a Worksheet.onChanged event handler.
They would like to ignore the onChanged event change that triggers by their code themselves. and focus on Processing the event handler should only be done when the user changed the values.
Scenario:
add-in listens to the sheetChange event. add-in user to enter values into the crosstabs, which the add-in will push to the server.
However, add-in API can also do some modification on the crosstabs, for example, the add-in can enable the user single click and drill down to get more data, their API logic will fetch data from the service and write to the crosstabs. however, the sheetChanged event will be triggered as well.
The solutions that the customer has been tried:
First try:
We tried to deregister the event handler before changing the values.
We then registered again after having the values changed.
This did not work as the deregistration is async and we were not able to await the deregistration.
Second try:
context.runtime.enableEvents = false
This is not possible as there are other events we are still interested in.
Current try:
We store the address of the changed range before changing the values.
In the onChanged handler, we compare the address.
If the same we do not do the logic.
After that, we delete the storage of the address again.
I also have tried to use a global flag and checking for that in the event handler, it doesn't work. here is my gist: https://gist.github.com/lumine2008/2b51d94d20cdca9ac9a0e97029dfd95c
Office.js doesn't provide such functionality yet.
Some workarounds, including a) context.runtime.enableEvents API, b) the global flag, or c) deregistering event before changing ranges and re-register event later, don't work cross-platform.
They will over-suppress events in some scenarios, which means some events triggered by user operations will be suppressed as well.
The most reliable way is your "current try"
Record the address of the to be changed range before changing the values.
In onChanged event handler, compare the event args with the address you recorded before. If it matches, clear the record and exit the event handler.
You can make a variable that chechs if the user is editing.
for example:
Private Sub Worksheet_Activate()
Static programChanging As Boolean
programChanging = False
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
If programChanging = False Then MsgBox("The User Is Changing")'*Do your code here*
End Sub
Private Sub Worksheet_SelectionChange(ByVal As Range)
programChanging = True
Range("A1").Value = "The Program Is Changing"
programChanging = False
End Sub
You can use the programChanging variable to check if the user is changing instead the if the program is changing but the principle stay the same.
~~Edit~~
You can also use one of the cells as the variable like:
Private Sub Worksheet_Change(ByVal Target As Range)
If Range("A1").Value = "False" Then MsgBox("The User Is Changing")'*Do your code here*
End Sub
Private Sub Worksheet_SelectionChange(ByVal As Range)
Range("A1").Value = "True"
Range("B5").Value = "The Program Is Changing"
Range("A1").Value = "False"
End Sub

Call helper function within parent without redefining objects from parent in the helper

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)

Calling a VBA form from a button causes UserForm_Initialize to run twice, breaking my code?

Hello wonderful VBA community,
I'm still really new to vba and am trying to learn a lot. Thank you in advance for looking through my code and my description of the issue I'm facing.
I have a button on a page that calls a new Userform.
CODE SNIPPET 1:
Sub btnShowDetails_Click()
Call frmShowDeets.ShowDeets
End Sub
... which calls the next bit of code in the 'frmShowDeets' UserForm:
CODE SNIPPET 2:
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
... triggering:
CODE SNIPPET 3:
Private Sub UserForm_Initialize()
Dim comboBoxItem As Range
For Each comboBoxItem In ContactList.Range("tblContactList[CompanyName]")
'^refers to unique values in a named range
With Me.boxCompanySelection
.AddItem comboBoxItem.Value
End With
Next comboBoxItem
End Sub
So at this point, the form I want to display has values loaded in its one combobox for user selection. The user selects a company and the Combobox_Change event triggers other routines that pull information for that company.
CODE SNIPPET 4:
Public Sub boxCompanySelection_Change()
Call frmShowDeets.PullData
End Sub
Sub PullData()
Dim numCompanies As Long
numCompanies = ContactList.Range("B6").Value 'this holds a count of the rows in the named range
Dim FoundCell As Range
Set FoundCell = ContactList.Range("tblContactList[Company Name]").Find(What:=boxCompanySelection.Text, LookIn:=xlValues, LookAt:=xlWhole)
Dim CompanyRow As Long
CompanyRow = FoundCell.Row
With ContactList
'pull a bunch of the company's details
End With
End Sub
Here is where it gets weird... Once the form is shown and the user selects one of the combo box items, triggering the Combobox_Change event the code breaks because the 'What:=boxCompanySelection.Text' part of the Range().Find method reads as "" empty (even though Code Snippet 3 is meant to load in company names and Code Snippet 4 is only triggered when the user selects one of those company names from the combobox) and I shouldn't need to build something to handle 'not found' exceptions since the only possible values should be the ones pulled in from my named range.
From stepping through the code, I have determined that for some reason, Code Snippets 2 and 3 run TWICE before Snippet 4 is run. Does anyone know what about my code is causing this to happen? I'm thinking there's a disconnect between the form that is shown and loaded with combobox values and whatever Code Snippet 4 is reading data from.
What is weirder is that if I run the code starting from Code Snippet 2 (ignoring the button call in Code Snippet 1), the form works as intended and from what I can tell 2 and 3 are only run once.
The problem is probably something simple I'm overlooking but I just cannot figure out what it is. Thanks again!
You have to understand that a form is an object - exactly as any other class module, except a form happens to have a designer and a base class, so UserForm1 inherits the members of the UserForm class.
A form also has a default instance, and a lot of tutorials just happily skip over that very important but rather technical bit, which takes us exactly here on Stack Overflow, with a bug involving global state accidentally stored on the default instance.
Call frmShowDeets.ShowDeets
Assuming frmShowDeets is the name of the form class, and assuming this is the first reference to that form that gets to run, then the UserForm_Initialize handler of the default instance runs when the . dot operator executes and dereferences the object. Then the ShowDeets method runs.
Public Sub ShowDeets()
Dim frm As frmShowDeets
Set frm = New frmShowDeets 'this line triggers the Userform_Initialize() event below
frm.Show
End Sub
That line triggers UserForm_Initialize on the local instance named frm - which is an entirely separate object, of the same class. The Initialize handler runs whenever an instance of a class is, well, initialized, i.e. created. The Terminate handler runs when that instance is destroyed.
So ShowDeets is acting as some kind of "factory method" that creates & shows a new instance of the frmShowDeets class/form - in other words whatever happened on the default instance is irrelevant beyond that point: the object you're working with exists in the ShowDeets scope, is named frm, and gets destroyed as soon as it goes out of scope.
Remove the ShowDeets method altogether. Replace this:
Call frmShowDeets.ShowDeets
With this:
With New frmShowDeets
.Show
End With
Now the Initialize handler no longer runs on the default instance.
What you want, is to avoid using the default instance at all. Replace all frmShowDeets in the form's code-behind, with Me (see Understanding 'Me' (no flowers, no bees)), so that no state ever accidentally gets stored in the default instance.
Call frmShowDeets.PullData
Becomes simply:
Call Me.PullData
Or even:
PullData
Since Call is never required anywhere, and the Me qualifier is always implicit when you make a member call in a class module's code.

Excel VBA - on Multi Page Change but only once

I have a flexgrid within a Multipage under Main_Window.MultiPage2.Value = 2 this flexgrid has 8000 rows and I don't want those to load unless this page is actually clicked on. The code I have does just that, but the problem is is that it loads every single time and not just once. Is there a way to make it load on the first change, and then that's it?
Private Sub MultiPage2_Change()
If Main_Window.MultiPage2.Value = 2 Then
Call form_segment_carrier_auto
End If
End Sub
in form_segment_carrier_auto is a module that populates the flexgrid.
If I understand you correctly, you could declare a Public Boolean variable, for example:
Public ChangedOnce As Boolean
This should be in some standard code module.
Then change your event handler to:
Private Sub MultiPage2_Change()
If ChangedOnce Then Exit Sub
ChangedOnce = True
If Main_Window.MultiPage2.Value = 2 Then
Call form_segment_carrier_auto
End If
End Sub
The event handler will still be called on multiple occasions if the event occurs on multiple occasions, but only the first call will do anything.

MS Access VBA: Reference Excel Application Object created in separate Module

This seems like it should be an easy one but I'm stuck.
I'm running a VBA script in Access that creates a 40+ page report in Excel.
I am creating an Excel Application Object using Early Binding:
Public obj_xl As New Excel.Application
Here is an example of how I am referencing the object:
With obj_xl
.Workbooks.Add
.Visible = True
.Sheets.Add
.blahblahblah
End With
The problem is that the procedure has become too large and I need to break the code up into separate modules.
If I try to reference the Excel Application Object from a different module than it was created in, it throws an error ("Ambiguous Name").
I'm sure I could do something with Win API but that seems like it would be overkill.
Any thoughts? Thanks
this is the type of situation that can cause the error "Ambiguous Name"
Function Split(s As String)
MsgBox s
End Function
Function Split(s As String)
MsgBox s
End Function
I know the example is trivial, but what you are looking for is a function , an object and/or a form control with the same names.
If you convert your declaration to Global, you can reference it in all your modules. For example, in one module, put this at the top:
Global obj_xl As Excel.Application
Then in an another module,
Sub xx()
Set obj_xl = New Excel.Application
Debug.Print obj_xl.Name
End Sub

Resources