I have a strange problem defining a class in an Excel VBA macro.
To do this I go to Excel→DEVELOPER(tab)→Visual Basic (the new view is opened). In this new view I do Project Name→Insert→Class Module and a new file is created.
Then I try to create a simple class in this file, like:
Class VirtualWindow
Public Sub Class_Initialize()
Debug.Print "Class_Initialize"
End Sub
Public Sub Class_Terminate()
Debug.Print "Class_Terminate"
End Sub
End Class
End Class is in red color, this meaning that it's not ok and when I try running it to see what error it throws it gives me the "Invalid outside procedure" error. I don't understand where is the problem.
You don't need the Class VirtualWindow and End Class here (since it is Visual Basic for Applications, not Visual Basic 6: VB vs. VBA) you can define the name in the properties field:
After this, from another function or class you can create an instance of the class:
Private w as VirtualWindow
Public Sub YourSub()
Set w = new VirtualWindow
End Sub
You're mixing up VB6 syntax with another language I think.
You do not have to put "Class VirtualWindow" and "End Class" in your code.
Just:
Public Sub Class_Initialize()
Debug.Print "Class_Initialize"
End Sub
Public Sub Class_Terminate()
Debug.Print "Class_Terminate"
End Sub
Related
Ok... So I am trying to wrap my head around properties. I have done a ton of googling but really need to try myself to fully understand. I have 3 components:
I have a UserForm: "frmView1"
I have a class module: "clsPresenter1"
I have a regular module: "modModel1"
My UserForm has a txtbox "TextBox1" and here is the userform code:
Option Explicit
'View (The V in MVP)
Public Event OnTxtBoxChange()
Public Property Get TxtBoxInfo() As String
TxtBoxInfo = TextBox1.Text
End Property
Private Sub TextBox1_Change()
RaiseEvent OnTxtBoxChange
End Sub
My Class module:
Option Explicit
'Presenter Class (The P in MVP)
'Implements Business Logic
Private WithEvents objUserForm As frmView1
Private Sub Class_Initialize()
Set objUserForm = New frmView1
End Sub
Public Sub TxtBoxContent()
objUserForm.TxtBoxInfo
End Sub
Private Sub objUserForm_OnTxtBoxChange()
DoSomething
End Sub
My regular module:
Option Explicit
'Model Module (The M in MVP)
'Business Logic
Private objPresenter As clsPresenter1
Public Sub DoSomething()
Sheet1.Cells.Clear
Sheet1.Cells(3, 3) = objPresenter.TxtBoxContent
End Sub
So this is a shortened version to simplify my question. In reality I have code to show the form and close the form and a few other things that are working fine. But my question is... How do I pass whatever the user types in the txt box to the worksheet? I keep getting:
Compile error: Expected Function or variable
Ok... So I may have figured it out... I changed:
Public Sub TxtBoxContent()
objUserForm.TxtBoxInfo
End Sub
To:
Public Function TxtBoxContent() As String
TxtBoxContent = objUserForm.TxtBoxInfo
End Function
This seems to work... Please let me know if this is not the correct way...
Maybe this is actually the correct way? Again changing:
Public Sub TxtBoxContent()
objUserForm.TxtBoxInfo
End Sub
To:
Public Property Get TxtBoxContent() As String
TxtBoxContent = objUserForm.TxtBoxInfo
End Property
Again this seems to work... But why would I ever use a function in my class module ("presenter") if I could always use a property instead?
I'm pretty new to VBA but I have been trying to figure out what is going wrong here. When I try to set a variable declared in my class module from a Sub in my module, the value isn't assigned for some reason and I can't figure out why. How do I get the variable table to go all the way through to the function call response = add_edit_data("fund_management.db", table)? It all compiles and runs, until it reaches the add_edit_data function which of course does not work without this variable. My Debug.Print checkpopup.table in the Sub returns nothing although I know the variable table from calling the Sub is set correctly as Debug.Print table returns the correct value when called from inside the Sub.
My module looks like this:
Private checkpopup As class_checkpopup
Public Sub checkbox_popup_fund(table)
Set checkpopup = New class_checkpopup
checkpopup.Show
checkpopup.table = table
Debug.Print checkpopup.table
End Sub
The class module is:
Public table As String
Private WithEvents check_box_popup As check_box
Private Sub Class_Initialize()
Set check_box_popup = New check_box
End Sub
Public Sub Show()
check_box_popup.Show
End Sub
Private Sub check_box_popup_Closed()
End Sub
Private Sub check_box_popup_Yes()
response = add_edit_data("fund_management.db", table)
End Sub
and I activate the eventhandler with:
Public Event Yes()
Private Sub check_box_yes_Click()
RaiseEvent Yes
End Sub
Thank you to Nicholas Hunter and Variatus for helping me out. I wasn't aware that it mattered to set the variable before showing the Form, but it does. I changed my module to:
Private checkpopup As class_checkpopup
Public Sub checkbox_popup_fund(table)
Set checkpopup = New class_checkpopup
Let checkpopup.table = table
checkpopup.Show
Debug.Print checkpopup.table
End Sub
and now my value carries through all the way to my function. Thank you very much for the help
I have defined a public sub in a common module (normal module). I am trying to access this in class module.
Below is both the code -
Public loadGroupIndicator As Boolean
Public Sub stopLoadGroupIndicator()
loadGroupIndicator = False
End Sub
Public Sub startLoadGroupIndicator()
loadGroupIndicator = True
End Sub
Public Sub getLoadGroupIndicator()
getLoadGroupIndicator = loadGroupIndicator
End Sub
I am trying to use "getLoadGroupIndicator" function in below function of a class module (testClass)
' This is in testClass
' Private sub to execute something on the event
Private Sub m_chckBox_Click()
If getLoadGroupIndicator Then
MsgBox "loadGroupIndicatorLocal is true"
Else
MsgBox "some other msg based on the object property"
End If
End Sub
I am getting the below error msg
Please help me fix this. I am new to VBA and struggling on this since yesterday.
Thanks in advance for any help or relevant pointers.
I've been moving some of my code to utilize UserForms utilizing Modeless so the user can copy data from the worksheet. I finally "generalized" this answer and got it working. But, I want to avoid having to create a class for each UserForm and truth is I don't 100% understand what is going on in the code. Is there a way I can easily migrate all my UserForms to utilize the below functions of "Event Driven Modeless UserForms". Basically, I'd like to pass the UserForm as a variable and have a bunch of subs in the class, mostly general and a specific or perhaps just using If/Then to call the correct exit module after the _Closed event.
Hope that makes sense, let me know if you require further clarification.
Generalized Code:
Module Name doesn't Matter
UserForm Name = UserForm1
Class Name = Class1
Module Code:
Private UserFormNameStr As Class1
Public Sub DoStuff()
Set UserFormNameStr = New Class1
UserFormNameStr.ClassSubNameStrSubName
End Sub
Public Sub CallMeWhenUserFormClosed()
Debug.Print "Module Code Run"
End Sub
Class Code:
Private WithEvents UserFormNameStr As UserForm1
Private Sub Class_Initialize()
Set UserFormNameStr = New UserForm1
End Sub
Public Sub ClassSubNameStrSubName()
UserFormNameStr.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed()
'_Closed is required syntax
Debug.Print "Closed Event"
Call CallMeWhenUserFormClosed
End Sub
UserForm Code:
Public Event Closed()
Private Sub UserForm_Initialize()
'
End Sub
Sub OkButton_Click()
Debug.Print "Raising Events from OK Button!"
RaiseEvent Closed
Unload Me
End Sub
Private Sub CancelButton_Click()
Unload Me
End
End Sub
Update:
Perhaps I'm looking for something on Workbook_Open with a "hook"?
What's making it all work is the WithEvents declaration. Instance variables declared with the WithEvents modifier will appear in the editor's left-side dropdown.
To create an event handler procedure for an event provider, select the variable from the left dropdown, then pick an event to handle in the right-side dropdown.
Ultimately the module would look something like this, i.e. with a WithEvents declaration for each modeless form you want to handle events for:
Private WithEvents UserFormNameStr As UserForm1
Private WithEvents SomeOtherUserForm As UserForm2
Private WithEvents AnotherUserForm As UserForm3
Private Sub Class_Initialize()
Set UserFormNameStr = New UserForm1
Set SomeOtherUserForm = New UserForm2
Set AnotherUserForm = New UserForm3
End Sub
Public Sub ClassSubNameStrSubName() 'weird name, consider methods that begin with a verb
UserFormNameStr.Show vbModeless
End Sub
Public Sub ShowSomeOtherForm()
SomeOtherUserForm.Show vbModeless
End Sub
Public Sub ShowAnotherForm()
AnotherUserForm.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed() 'select "UserFormNameStr" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (UserFormNameStr)"
CallMeWhenUserFormClosed
End Sub
Private Sub SomeOtherUserForm_Closed() 'select "SomeOtherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (SomeOtherUserForm)"
CallMeWhenSomeOtherUserFormClosed
End Sub
Private Sub AnotherUserForm_Closed() 'select "AnotherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (AnotherUserForm)"
CallMeWhenAnotherUserFormClosed
End Sub
One Handler, Many Forms?
If all _Closed handlers need to do exactly the same thing, then we can get interfaces and polymorphism involved and have one class exist in 3 instances that each do their own thing for their respective form - but VBA does not expose Public Event declarations on a class' default interface, so the paradigm is a little bit different here, and because it doesn't involve Event and WithEvents, it's arguably simpler that way, too.
Define an IHandleClosingForm interface: add a new class module to your project, but give no attention to the implementation - just a very high-level abstraction of the functionality you want (here with Rubberduck annotations):
'#ModuleDescription "An object that handles a form being closed."
'#Interface
'#Exposed
Option Explicit
'#Description "A callback invoked when a form is closed."
Public Sub Closing(ByVal Form As UserForm)
End Sub
In each form module, hold a reference to that interface, and invoke its Closing method in the form's QueryClose handler:
Option Explicit
Public CloseHandler As IHandleClosingForm
`...
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
'user clicked the [X] button, form instance is going to be destroyed!
Cancel = True 'prevents a self-destructing form instance.
Me.Hide
End If
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
'...
End Sub
Now implement that interface in the presenter class:
Option Explicit
Implements IHandleClosingForm
'...rest of the module...
Private Sub IHandleClosingForm_Closing(ByVal Form As UserForm)
'NOTE: procedure exits to the still-closing form's QueryClose handler
If TypeOf Form Is UserForm1 Then
CallMeWhenForm1Closes
Else
CallMeWhenAnyOtherFormCloses
End If
End Sub
The final step is to introduce a circular reference between the form and the presenter, by setting the public CloseHandler property before showing the form:
Set theForm.CloseHandler = Me
theForm.Show vbModeless
This will work, but then there's a memory leak and neither the form nor the presenter instance would terminate (handle Class_Terminate to find out!), and you will want to strive to avoid that (Excel will/should clean it all up when it shuts down the VBA environment though).
The solution is to untie the knot at the first opportunity, so make sure your forms' QueryClose handler sets the IHandleClosingForm reference to Nothing as soon as it is no longer useful:
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
Set CloseHandler = Nothing 'release the handler reference
The next time the form is shown and the handler is set, it's going to be on another instance of the form.
If you need the state of the form to persist between it being shown and closed, then you must separate the state from the form (and keep the state around but still properly destroy the form object), ...but that's another topic for another day :)
Update:
It seems Visio and Excel on my PC do not use the same Office Object Libraries, at least that is what a look at the used references tells me. Visio uses 15.0, Excel uses 16.0, we use different subscriptions for both, Excel is part of the MS office 365 ProPlus Package, Visio is separate.
I tested it on a PC with a current subscription of Visio with the 16.0 Office Object Libraries. This time closing the Userform did not cause a crash. So I guess the problem was with the old version. If anybody has the ability to cross check this and test the code in a Excel 15.0 installation that would be great.
Original Post
I have been using an Observer Implementation in VBA to create some "better" UserForms that implement an Interface.
The solution works great as far as I'm concerned, but there is one minor problem:
Whenever I close the UserForm by pressing the FormControlMenu Close Control (the red X in the top/right corner) my application crashes with a "Out of Stack Space Error".
Crashing occurs in the following way: I get the Message box with the error (standart VBA), when I close it there seems nothing amiss, but as soon as I try to run another piece of code (any) Visio will crash to the Desktop.
Now the strange part: I actually catch the vbQueryClose event, cancel it and run my own closing routine which only hides the userform. When closing (hiding) the UserForm via a commandButton the same way (me.hide), the error does not occur.
This happens only in Visio, the exact same code causes no error/crashing in Excel!
I hope someone with some more knowledge on the whole Reference/Object/COM-Business can shed some light into this
The Code:
Code also available as zipped file (no Excel/Visio Files, just the exported modules) so you don't have to copy/paste and create UserForms: https://www.dropbox.com/s/ziqjv2umcy3co5t/ObserverExample.zip?dl=0
The actual Observer Implementation is a bit longer, but this code is the minimal verifiable example.
Module1 (Module):
'#Folder("ObserverTest")
Option Explicit
Sub StartTest()
With New Foo
.Test
End With
End Sub
Foo (Class):
'#Folder("ObserverTest")
Option Explicit
Private WithEvents myObs As Observer
Private myView As IBar
Public Sub Test()
Set myView = New Bar
Dim myViewAsObservable As IObservable
Set myViewAsObservable = myView
myViewAsObservable.AddObserver myObs
Set myViewAsObservable = Nothing
myView.Show
Debug.Print myView.howClosed
End Sub
Private Sub Class_Initialize()
Set myObs = New Observer
End Sub
Private Sub Class_Terminate()
Set myObs = Nothing
End Sub
Private Sub myObs_Notify(source As Object, arg As Variant)
If VarType(arg) = vbString Then
Debug.Print arg
End If
End Sub
IBar (Class)
'#Folder("ObserverTest")
Option Explicit
Public Sub Show(): End Sub
Public Property Get howClosed() As String: End Property
IObservable(Class)
'#Folder("ObserverTest")
Option Explicit
Public Sub AddObserver(ByVal obs As Observer): End Sub
Bar (UserForm)
'#Folder("ObserverTest")
Option Explicit
Implements IBar
Implements IObservable
Private obsCol As Collection
Private cancelHow As String
'---IBar Stuff
Private Sub IBar_Show()
Me.Show
End Sub
Private Property Get IBar_howClosed() As String
IBar_howClosed = cancelHow
End Property
'--- Closing Stuff
Private Sub btCancel_Click()
cancelHow = "Closed by pressing the >Cancel< Button"
onCancel
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
cancelHow = "Closed by pressing the >X< Control"
onCancel
End If
End Sub
Private Sub onCancel()
Me.hide
End Sub
'---Observer Stuff
Private Sub UserForm_Initialize()
Set obsCol = New Collection
End Sub
Private Sub UserForm_Terminate()
Set obsCol = Nothing
End Sub
Private Sub tbTest_Change()
Notify Me.tbTest.Text
End Sub
Private Sub IObservable_AddObserver(ByVal obs As Observer)
obsCol.Add obs
End Sub
Private Sub Notify(ByVal arg As Variant)
Dim obs As Observer
For Each obs In obsCol
obs.Notify Me, arg
Next obs
End Sub
Observer (Class)
'#Folder("ObserverTest")
Option Explicit
Public Event Notify(source As Object, arg As Variant)
Public Sub Notify(ByVal source As Object, ByVal arg As Variant)
RaiseEvent Notify(source, arg)
End Sub
The Problem lay apparently with the version of the Office Object Library I was using. Using the 16.0(2016) Library instead of the 15.0(2013) solved the problem for me.
If anybody has a good explanation, post it here and I will accept it as answer.