How to assign an event to multiple objects with excel vba? - excel

I have ten drop down menus on a worksheet each of which should respond the same to the GotFocus() event.
I have written the following code but I get a run time error (459) - "Object or class does not support the set if events"
In a class called clsPDRinput I have the following:
Public WithEvents inputObj As OLEObject
Public Property Set myInput(obj As OLEObject)
Set inputObj = obj
End Property
Public Sub tbPDRInput_GotFocus()
//Do some stuff...
End Sub
I am then running the following code which is producing the error:
Dim tbCollection As Collection
Public Sub InitializePDRInput()
Dim myObj As OLEObject
Dim obj As clsPDRInput
Set tbCollection = New Collection
For Each myObj In Worksheets("1. PDR Documentation").OLEObjects
If TypeName(myObj.Object) = "ComboBox" Then
Set obj = New clsPDRInput
Set obj.myInput = myObj <-- **THIS LINE THROWS ERROR**
tbCollection.Add obj
End If
Next myObj
Set obj = Nothing
End Sub
I am not sure what is causing this error. One thought I had is that OLEObject is too generic and not every OLEObject supports the GotFocus() event and that is why the code is giving the error message?
I have tried replacing OLEObject with MSForms.ComboBox but that doesn't resolve issue.
Any ideas - have googled for two hours now and come up blank...
EDIT - Update on what I think the issue is...
I did more investigating and here is what the issue is as far as I can tell.
If you declare a variable as OLEObject (as in ...inputObj as OLEObject) then the only events exposed are GotFocus() and LostFocus().
If you declare a variable as MSForms.ComboBox (as in ...inputObj as MSForms.ComboBox) then a variety of events are exposed (e.g. Change(), Click(), DblClick()) but the events GotFocus() and LostFocus() are not exposed
Points 1 and 2 are consistent with the object model in excel. As a result, when I try to assign a ComboBox to my class I get an error (see original post) as the ComboBox does not support the GotFocus() and LostFocus events.
Now for the puzzle. If I add a ComboBox onto a worksheet (using Control ToolBox) and I double click that ComboBox to get to the code behind then all events are exposed, including GotFocus() and LostFocus()!

The below works for me. There were a couple of problem with your code, and comboboxes don't have a GotFocus event, so you'll have to use a different one.
The collection needs to be a global in the module, not part of the class.
I couldn't get this to work using the generic "OLEobject" approach (same error you got).
' ### in the class
Public WithEvents inputObj As MSForms.ComboBox
Private Sub inputObj_Change()
MsgBox "Change!"
End Sub
' ### in a module
Dim tbCollection As Collection
Public Sub InitializePDRInput()
Dim myObj As OLEObject
Dim obj As clsPDRInput
Set tbCollection = New Collection
For Each myObj In Worksheets("Sheet1").OLEObjects
If TypeName(myObj.Object) = "ComboBox" Then
Set obj = New clsPDRInput
Set obj.inputObj = myObj.Object
tbCollection.Add obj
End If
Next myObj
End Sub

Update
I was too focused in making the code compile and someone was nice enough to point out that the answer below is bad juju. So do not use. It does compile, but not a good answer.
I reproduced your error and fixed by changing the following declaration:
Public WithEvents inputObj As OLEObject
to this:
Public inputObj As New OLEObject
Of course, this is a different type of declaration so I'm not sure if it will work for you. It does remove the exception.
I'd also like to note that if you don't have Option Explicit set, you should. There are some variables in your code that are not declared. My guess is that you perhaps modified the code before posting your question.
Just making sure.

Related

Runtime Error '459': Object or class does not support the set of events

i have created a simple class named Class1 and was testing if events are available for the objects or containers listed with WithEvents.
Class1:
Option Explicit
Public WithEvents Obj As OLEObject
Private Sub Obj_GotFocus()
MsgBox "Got U"
End Sub
Private Sub Obj_LostFocus()
MsgBox "Lost U"
End Sub
As i understand, OleObject is a container and such it has the above 2 events listed for it.
In Module1, this is a simple code which sets a single OleObject on Sheet1 of my workbook. For testing purpose, I have added 5 different ActiveX controls to see which one gets the events.
Module1:
Option Explicit
Dim clsObj As Class1
Sub doit()
Set clsObj = New Class1
Set clsObj.Obj = Sheet1.OLEObjects(1) ' Getting error 459 on this line
End Sub
The following line above results in an error:
Runtime Error '459':
Object or class does not support the set of events
Note:
I can add a WithEvents to the object within this container e.g.
a Label1 and get it to work, but it gives a different set of
events and not the Got_Focus() & Lost_Focus() events. However, this is not what i want :
Class1:
Module1:
Questions:
Can anyone explain why code does not allow to set the OleObject
container object?
What is the logic or purpose behind making these events available
when they cannot be triggered at all?
Why is the error message informing that these events are not
available for any of the ActiveX controls added to Sheet1?
Is there an alternative way to access the 2 OleObject container
events?

Data connection refreshes but the data does not using VBA

I have a data connection in a workbook and I refresh it using VBA.
sub RefreshData()
ActiveWorkbook.Connections("LoadData1").Refresh
End Sub
The code runs without error but the data does not change or update. But when I step through the code, it works fine and the data is updated. I also tried doing a wait.
sub RefreshData()
ActiveWorkbook.Connections("LoadData1").Refresh
Application.Wait (Now + TimeValue("00:00:20"))
End Sub
I have also tried ActiveSheet.Unprotect, Application.ScreenUpdating = True but to no avail.
Simple Solution
I had issues with some of my subs ending before the data had finished refreshing. If I understand correctly, that's what's happening with your sub. If so, you could try adding the line:
Application.CalculateUntilAsyncQueriesDone
Adding this line into my code directly after the refresh command worked for me.
Note: When I had the queries set as background queries, I occasionally got weird freeze/crash issues; so I would recommend turning off the background query option with any query you use the above code with.
Complex Solution
If the simple solution doesn't work, an alternative is to add a custom class that raises an event when the refresh is finished. The downside to this solution is that you may need to rewrite existing code to be triggered by an event, instead of having an in-line refresh command.
An example of such a custom class is below. Please note that there are some assumptions built into the code -- the most prominent being that the query is set to load onto a sheet in the workbook, and refresh in the background.
To use the custom class, insert a "class module" (this is not the same as a "Module"), and copy the code from the "class code" section below into the "class module". Next, in the code module for the worksheet holding the resulting query table, add this code:
Private WithEvents queryData As QueryClass
Public Sub querySetup()
Set queryData = New QueryClass
Set queryData.QryTble = Me.ListObjects("QueryName").QueryTable
End Sub
Private Sub queryData_Refreshed(ByVal RefreshSuccess As Boolean, ByVal isEmpty As Boolean)
End Sub
(Note that this code is assuming that the class module has been renamed to "QueryClass", and that the query was named "QueryName". If you used different names, you'll need to adjust the code accordingly.)
You can put custom code in the queryData_Refreshed sub to happen after the query has finished refreshing. Note that the sub has two indicators -- if the query refreshed successfully, and if the query is empty (did not return any records). Then, to refresh the data, just call:
queryData.Refresh 5 'optional maximum of attempts; defualt is 1
These questions may also be helpful.
Class Code
Option Explicit
'class basics from Paul Renton, https://stackoverflow.com/questions/18136069/excel-vba-querytable-afterrefresh-function-not-being-called-after-refresh-comp
Private WithEvents mQryTble As Excel.QueryTable
Private RefreshFinished As Boolean
Private RefreshSuccessful As Boolean
Private attemptCount As Long
Private attemptMax As Long
Public Event Refreshed(ByVal RefreshSuccess As Boolean, ByVal isEmpty As Boolean)
Public Property Set QryTble(ByVal QryTable As QueryTable)
Set mQryTble = QryTable
End Property
Public Property Get QryTble() As QueryTable
Set QryTble = mQryTble
End Property
Public Property Get RefreshDone() As Boolean
RefreshDone = RefreshFinished
End Property
Public Property Get RefreshSuccess() As Boolean
RefreshSuccess = RefreshSuccessful
End Property
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
attemptCount = attemptCount + 1
If Success Or attemptCount = attemptMax Then
RefreshFinished = True
RefreshSuccessful = Success
RaiseEvent Refreshed(Success, mQryTble.ListObject.DataBodyRange Is Nothing)
Else
mQryTble.ListObject.Refresh
End If
End Sub
Public Sub Refresh(Optional attempts As Long = 1)
If Not mQryTble.Refreshing Then
RefreshFinished = False
attemptMax = attempts
mQryTble.ListObject.Refresh
End If
End Sub

Modify an existing VBA class

I would like to know if there is some way to add your own methods/properties to an existing VBA class (such Range, Charts, etc).
An example:
I would like the currently VBA class Worksheet have a specific method done by myself, something like:
'Worksheet methods
Public Sub LookFor (ByVal Value as String)
'My code
End Sub
Then I can call from any declared Worksheet class this function.
In class MyClass:
'MyClass members
Private pWS1 as Worksheet
Private pWS2 as Worksheet
Private pWS3 as Worksheet
'MyClass methods
Private Sub Class_Initialization()
Set pWS1 = Worksheets("WS1")
Set pWS2 = Worksheets("WS2")
Set pWS3 = Worksheets("WS3")
End Sub
Public Sub Example()
pWS1.LookFor("abc")
pWS2.LookFor("123")
pWS3.LookFor("def")
End Sub
Thanks!
There is no direct way to do this in VBA.
Best you can do is create a "wrapper" class which has a private Worksheet member, and expose that via a Sheet property. Add your "extension" methods to the class and have them operate on m_sheet.
Initialize your class by creating an instance of it and assigning a worksheet object to its Sheet property.
You can call your "extension" methods directly on the object, and any existing methods you'd access via the Sheet property.
Class MySheet:
Private m_sht As Worksheet
Public Property Set Sheet(ws As Worksheet)
Set m_sht = ws
End Property
Public Property Get Sheet() As Worksheet
Set Sheet = m_sht
End Property
Public Property Get CountHellos() As Long
CountHellos = Application.CountIf(m_sht.Cells, "Hello")
End Property
Test sub:
Sub Tester()
Dim sht As MySheet
Set sht = New MySheet
Set sht.Sheet = ActiveSheet
MsgBox sht.CountHellos '<< "extension" method
MsgBox sht.Sheet.Rows.Count '<< built-in object property
End Sub
Edit: you might be able to make the Sheet property the default for your class by following the steps outlined by Chip here: http://www.cpearson.com/excel/DefaultMember.aspx
May work to allow you to skip the Sheet property when working with instances of your class (but I've not tested this)
What you are looking for is called "Extension" methods in tradition Object Oriented Programming Languages.
See MSDN: Extension Methods (Visual Basic)
AFAIK, what you are looking for is not supported / available in traditional Visual Basic for Applications (VBA).
Here's an example of doing extension methods in Visual Basic .Net (VB.Net) from the MSDN source.
Step 1. Declare Extension method like so...
Step 2. Call extension method like so...

How do you store a worksheet reference in a VBA object?

This is going to seem trivial to those of you steeped in Excel object programming but it's beat me.
In the past, I've done the following in Excel's vba to restore the activesheet before exiting a subroutine..
sub foo()
dim cursheet
cursheet = ActiveSheet
someOtherSheet.activate
....
cursheet.activate
end sub
That works fine. I attempted to do something similar using objects and after several different approaches, wrote the following in a new Problem class...
''''''''''''''''''''''
' sheet property
''''''''''''''''''''''
Public Property Get sheet() As Worksheet
Set sheet = psheet
End Property
Public Property Let sheet(Value As Worksheet)
Set psheet = Value
End Property
Public Sub saveCursheet()
Me.sheet = ActiveSheet
End Sub
Public Sub activateSheet()
Me.sheet.Activate
End Sub
In my code, I invoke the methods this way...
Sub TallyQuizScore()
Dim curStudent As Problem
Set curStudent = New Problem
curStudent.saveCursheet
Worksheets("QuizTallies").Activate
...
curStudent.activateSheet
End Sub
When I attempt to execute curStudent.activateSheet, I get an error saying I need an object. So I reran the calling code and stepped through the saveCursheet method. I see the activesheet get stored but notice that the sheet object disappears as soon as I hit the setter's end property line. I don't know if that's an artifact of the debugger or if the sheet really does get tossed when I hit the end property line but whatever it is, the object is gone when I attempt to reactivate it when I'm done.
The frustrating thing is what I really wanted to write in my caller was
curStudent.sheet = Activesheet
and
curStudent.sheet.Activate
by somehow inheriting the builtin worksheet methods but that led to a rabbit's warren of code as I tried to make it work.
So three questions:
Why did the sheet I stored in saveCursheet disappear?
What do I need to change to make the code work?
What do I need to do differently from the above approach to make the curStudent.sheet = Activesheet and it's partner, curStudent.sheet.Activate approach work?
You need a module-level variable to store the value while your code is doing other things. Note that it's private.
Also, as caught by ja72, in the case of objects it's Set, not Let:
UNTESTED:
Private m_Sheet as Worksheet
Public Property Get Sheet() As Worksheet
Set sheet = m_Sheet
End Property
Public Property Set Sheet(Value As Worksheet)
Set m_Sheet = Value
End Property
Public Sub saveCursheet()
Me.Sheet = ActiveSheet
End Sub
Public Sub activateSheet()
Me.m_Sheet.Activate
End Sub

I have a range in an excel document which I need to extract into a listbox in Visual basic

I have a range in an excel document which I need to extract into a listbox. I can do this through .txt file, but really would prefer to source the information from excel. How?
I am using Visual Studio 2010 and so far my code is:
Public class1 Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim objXLApp As Excel.Application
Dim intLoopCounter As Integer
objXLApp = New Excel.Application
With objXLApp
.Workbooks.Open("C:\report.xls")
.Workbooks(1).Worksheets(1).Select()
For intLoopCounter = 1 To CInt(.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row)
List1.AddItem.Range("A" & intLoopCounter)
Next intLoopCounter
.Workbooks(1).Close(False)
.Quit()
End With
objXLApp = Nothing
End Sub
When I build this program I get two errors;
statement is not valid in namespace
end of statement expected (I did put end class but it adds more couple of errors")
I don't really do VB, but I think you should break the binding out of the loop with something like:
var data = .ActiveSheet.Range("A1:A"&.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row).Value2
List1.DataSource = data
List1.DataBind()
You could try (assuming you want the whole Range object to be bound to the list and not just the displayed value.
For intLoopCounter = 1 To CInt(.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row)
List1.AddItem .ActiveSheet.Range("A" & intLoopCounter)
Next intLoopCounter
Edit:
Also you should use Set objXLApp = New Excel.Application. You're missing the set keyword.
This is also not how you declare classes in VBA. You make a new class like you make a new module. The class is named in the properties pane.
Also this is not how events are handled in VBA. If you want to handle the button click event of Button1 then the event handler must look like
Private Sub Button1_Click()
End Sub
It must have the same name as the object and the name of the event being handled. It also must be on the code page for the user form and has no parameters. VBA is really lacking in this regard.
Hope this helps!

Resources