Add Public Methods to a Userform Module in VBA - vba

Is it possible to call a public sub located in a UserForm from a Class Module? I want to put a callback in the Form Module but I can't seem to get it to expose.
Is this a fundamental limitation of UserForms in VBA?
It is exposed inside the UserForm Code Module, I can see it in the intelisense for the Me object, but I can't seem to access it from outside the Form Module.

The real answer to my question is to have a better understanding of UserForms and since I could not find a good reference for that I thought I would answer my own question to share my learnings.
Thanks to #Dick Kusleika for the key insight!
First of all, this is not a UserForm:
It is no more a Form than a Class Module is a variable.
UserForm1 is a Class Module with a GUI and with the following default, inherited properties
These properties are like a standard Interface that is common to all Form Class Modules and therefore instances. The Name property is in parentheses because it is not the name of the object, it is the name of the the Type that is used to declare variables to instantiate the particular Form Class.
More properties and methods can be added by the user at design time and this is done in exactly the same way as a Class Module.
For example, in a Form Module...
Option Explicit
Dim mName As String
Property Let instName(n As String)
mName = n
End Property
Property Get instName() As String
If Len(mName) = 0 Then mName = Me.Name
instName = mName
End Property
In this example, the Form Class Name is used as the default Instance Name.
When you add Controls to the form, its like graphically adding
Public WithEvents controlName As MSForms.ControlType
...in a Class Module.
The Methods inherited in the standard interface include one called Show.
You can create an instance of a form using UserForm1.Show and this is very confusing and misleading. To me it implies that you are showing the Object called UserForm1 but you are not. I don't know why you would want to use this method because, apart from being confusing, it does not deliver any direct reference to the object created. Its a bit like Dim v as New Type only worse, because there is no referencing variable.
You can instantiate a Form Class in exactly the same way you can a Custom Class object and then use the show method to deploy it...
Dim f As UserForm1
Set f = New UserForm1
f.Show
For me, this is the preferred method.
You can add custom properties and controls to the UserForm1 Class and you can give it a meaningful name when creating it, but you can also reference it using the standard UserForm interface.
For example
'In a Class Module
Dim mForm as UserForm1
Property let Form(f as MSForms.UserForm)
Set mForm = f
End Property
For me, after understanding the above, all of my confusion about UserForms and my frustration at not being able to find a decent reference disappears. I just treat them as Class Modules and its fine.

The only difference between a Userform and Class Module is that a Userform has a UI element that a Class Module doesn't. So a Userform is just a special type of Class Module. That means that Public Subs inside a Userform behave just as they do in any other class - as a method of the class.
To access a Public Sub inside a class module (such as a userform), you need to instantiate the class, then call the method.
Dim uf1 As UserForm1
Set uf1 = New UserForm1
Uf1.MyPublicSub

Related

Calling Public variable from userform

In a userform, I have this at the top:
Public DelMonth As Variant
The value of DelMonth is read from a ComboBox, and I can call it from different subroutines within that userform just fine. But when I call it from a separate module, it doesn't read it. It doesn't even throw an error. If I do a MsgBox DelMonth, it doesn't do anything.
A form is an object; a public field in an object module belongs to an instance of that object. UserForms are little more than class modules with a default instance (i.e. a VB_PredeclaredId = True attribute) and a designer.
If you're using the form's default instance (a rather bad idea), then you can do this:
MsgBox UserForm1.DelMonth
Note that storing state in global objects is bug-prone, and will end up causing issues.
If you're treating the form like the full-fledged class it is, then you'll have something like this:
With New UserForm1
.Show
MsgBox .DelMonth
End With
Note that the field being Public means anyone, anywhere can go and write to it. What you mean is for the form to determine its value, and for the caller to be able to read that value. You do this by encapsulating the field with a Property Get member - start by making the field Private:
Option Explicit
Private DelMonth As Variant ' wouldn't Integer or Long be more appropriate?
Public Property Get DeliveryMonth() As Long
DeliveryMonth = DelMonth
End Property
Now the callers don't get to see the private DelMonth, and all they can do with DeliveryMonth is call the Get accessor, which doesn't let them tamper with the encapsulated value.
It doesn't even throw an error.
That's worrying. You're allowing VBA to happily compile typos and otherwise illegal code. Specify Option Explicit at the top of every module. Always.

Reference to a non-shared member requires an object reference in VB.net

I have a VB.net program that I got from someone else. It is comprised of a main form and 6 other modules (all .vb files). These files all have a "VB" icon next to them in the Explorer pane. I am trying to make a call to a sub-routine in one of the modules from the main form. My line of code is:
QuoteMgr.StartGettingQuotesLevel2(sSym)
where QuoteMgr is the name of the module and StartGettingQuotesLevel2(sSym) is the name of the sub-routine. When I enter this, I get the error message:
Reference to a non-shared member requires an object reference.
The sub-routine is defined in the QuoteMgr Module as follows:
Public Sub StartGettingQuotesLevel2(ByVal oSymbol As String)
What is strange is when I enter:
QuoteMgr.
(the name of the module with a period), it does not show me all the sub-routines and functions in the module. It only shows:
Update_Level1
Update_Level12
Update_Level2
These are Public Const in the module.
Can you tell me what I need to do?
What the compiler is trying to tell you with this error message
Reference to a non-shared member requires an object reference
is that the StartGettingQuotesLevel2 subroutine is an instance method not a shared or class method, see a more detailed explanation here
To call an instance method, you need to have an object instance to call it on. In your case, an object instance of the class type QuoteMgr. Like in the example below:
' create a new QuoteMgr object instance
Dim myQuoteMgr As QuoteMgr = New QuoteMgr()
' call its instance method with "abc" as its oSymbol argument.
myQuoteMgr.StartGettingQuotesLevel2("abc")
It is possible that you only want a single QuoteMgr object instance to be created and used by your main form. In that case, you can make it a member variable of your main form and create it once.
Public Partial Class MainForm
' Create it as a private member variable of the main form
Private m_QuoteMgr As QuoteMgr = New QuoteMgr()
' Use it when "some" button is pressed
Private Sub btnSome_Click(sender As Object, e As EventArgs) Handles btnSome.Click
m_QuoteMgr.StartGettingQuotesLevel2(txtSymbol.Text)
' And possibly do something with the results.
End Sub
End Class
Also, if instances of your QuoteMgr class depend on other object instances for their tasks, you will have to supply these to the constructor method of the QuoteMgr class as the arguments for its constructor's method parameters. Constructors (Sub New(...)) look like this:
Public Class QuoteMgr
' This is a constructor that takes two arguments
' - oMainSymbol: a string value
' - oKernel: an instance of the type Kernel
Public Sub New(oMainSymbol As String, ByRef oKernel As Kernel)
' ....
End Sub
End Class
That means, that when you create a QuoteMgr instance, you have to call its constructor method with the things it need, for example
' There must be an instance of Kernel created somewhere.
Dim myKernel As Kernel = ....
' create a new QuoteMgr object instance with these arguments:
' - oMainSymbol = "SYMABC"
' - oKernel = myKernel
Dim myQuoteMgr As QuoteMgr = New QuoteMgr("SYMABC", myKernel)
Some other recommendations
The explanations I have provided, are about basic VB.NET language features (e.g. the terms highlighted in bold). I suggest that before you make any changes to the code you have, you (1) make a backup of it, and (2) try to read a tutorial and practice on something smaller.
The compiler is (virtually) always right. When it gives you an error message, read it carefully, it will indicate the line where something is wrong and a message that tells you what it needs or is missing.
It is not the purpose of Stack Overflow to provide tutorials or code. It is a Q&A site where the best questions and answers deal with specific, delineated programming problems, for which succinct answers are possible.
Right click your application and go to Properties.
Make sure your application type is "Windows Forms Application".
It means that the routine you are trying to call needs to reference an instance of the form to access the routine. You can either reference an instance as Alex says, or you can make the routine 'Shared', so it doesn't need an instance. To do this, change the definition in QuoteMgr.vb to
Friend Shared Sub StartGettingQuotesLevel2(ByVal oSymbol As String)
Switching it to `Shared' may start showing compiler errors, if the routine accesses form controls or module-level variables. These will need to be added to the parameter list.

Reference to a non-shared member requires reference

I am updating some code from a vb6 application to VB.NET.
I have a problem that occurs when I try to open a form from the main form.
It calls this function:
Public Sub optDrawSafeFile_CheckedChanged(ByVal eventSender As System.Object, ByVal eventArgs As System.EventArgs) Handles optDrawSafeFile.CheckedChanged
If eventSender.Checked Then
'--------------------------------------------------------------------------------
' JRL 11-03-06
' change the enables
UpdateGUI((False))
cboProject.SelectedIndex = frmMain.cboProjects.SelectedIndex
SelectJob()
End If
End Sub
And when it goes to execute this line:
cboProject.SelectedIndex = frmMain.cboProjects.SelectedIndex
It blows up and says this:
frmMain is declared like this:
How can I fix this error?
TL;DR
It is described in more detail in this video.
Short answer: change frmMain to My.Forms.frmMain.
cboProject.SelectedIndex = My.Forms.frmMain.cboProjects.SelectedIndex
Long answer:
In VB6, referencing a form by its name allowed you to access it both as a class and an instance of that class. The instance that you access in this manner is called the default instance. This is not possible in VB.NET. However, VB.NET includes a dynamically generated class, My.Forms, that provides functionality similar to that of default instances.
See http://msdn.microsoft.com/en-us/library/ms379610%28v=vs.80%29.aspx#vbmy_topic3 for more information about My.Forms and the "My" namespace.
A better and more object-oriented way to handle this, however, would be to pass the instance of the main form to the constructor of the frmAddMethod form and store it in an instance field.
So, within the class definition in frmAddMethod.vb:
Sub New(ByVal mainForm As frmMain)
_mainForm = mainForm
End Sub
Private _mainForm as frmMain
And when you create the frmAddMethod instance from frmMain, pass in "Me" to the constructor:
Dim addMethodForm as new frmAddMethod(Me)
"Me" is the instance of the class from which a non-shared class method was called.
This will allow you to use the _mainForm class field to access the instance of the main form from non-shared methods of frmAddMethod.
*Edited to recommend My.Forms instead of DefInstance per Plutonix's comment.
Nothing is "blowing up", your program is not crashing. Using a type name, like frmMain, where an object reference is expected is something that the VB.NET compiler allows. Specifically for the Form class, an appcompat hack for VB6 code. It is the debugger that doesn't think much of it. It merely gives you a diagnostic on your watch expression since it doesn't have the same appcompat hack as the compiler does. So doesn't know what to display.
You can use My.Forms instead to get the active form object reference. So make your watch expression:
My.Forms.frmMain.cboProjects.SelectedIndex
Only do this when you are single-stepping the code, it will still go wrong if you use Debug + Break All to break into the program. Setting a watch expression on Me.cboProject is otherwise the obvious workaround in this specific case.

Public variables are not REALLY public in VBA in Forms

Below is a question that I will answer myself, however it caused a GREAT deal of frustration for me and I had a lot of trouble searching for it on the web, so I am posting here in hopes of saving some time & effort for others, and maybe for myself if I forget this in the future:
For VBA (in my case, MS Excel), the Public declaration is supposed to make the variable (or function) globally accessible by other functions or subroutines in that module, as well as in any other module.
Turns out this is not true, in the case of Forms, and I suspect also in Sheets, but I haven't verified the latter.
In short, the following will NOT create a public, accessible variable when created in a Form, and will therefore crash, saying that the bYesNo and dRate variables are undefined in mModule1:
(inside fMyForm)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
However, if you make the slight alteration below, it all will work fine:
(inside fMyForm)
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
mModule1 will work perfectly fine and, assuming that the fMyForm is always called first, then by the time the PrintVals routine is run, the values from the textbox and checkbox in the form will properly be captured.
I honestly cannot possibly fathom what MS was thinking with this change, but the lack of consistency is a huge suck on efficiency, learning idiosyncracies like these, which are so poorly documented that a Google search in 2013 for something that has likely been around for a decade or more is so challenging to search.
First comment:
Userform and Sheet modules are Object modules: they don't behave the same way as a regular module. You can however refer to a variable in a userform in a similar way to how you'd refer to a class property. In your example referring to fMyForm.bYesNo would work fine. If you'd not declared bYesNo as Public it wouldn't be visible to code outside of the form, so when you make it Public it really is different from non-Public. – Tim Williams Apr 11 '13 at 21:39
is actually a correct answer...
As a quick add-on answer to the community answer, just for a heads-up:
When you instantiate your forms, you can use the form object itself, or you can create a new instance of the form object by using New and putting it in a variable. The latter method is cleaner IMO, since this makes the usage less singleton-ish.
However, when in your userform you Call Unload(Me), all public members will be wiped clean. So, if your code goes like this:
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Show(vbModal)
If Not oForm.bCancelled Then ' <- poof - bCancelled is wiped clean at this point
The solution I use to prevent this, and it is a nice alternative solution for the OP as well, is to capture all IO with the form (i.e. all public members) into a separate class, and use an instance of that class to communicate with the form. So, e.g.
Dim oFormResult As CWhateverResult
Set oFormResult = New CWhateverResult
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Initialize(oFormResult)
Call oForm.Show(vbModal)
If Not oFormResult.bCancelled Then ' <- safe
There are other limitations to Public within Excel VBA.
MSoft documentation in learn.microsoft.com states that public variables are global to the VBA project - it's not true.
Public variables are only global to the workbook within which they are declared, and then only across standard modules. Public variables declared within workbook code are not visible in standard modules, even though standard module sub's are - which are defined to be public.
Public variables declared in one workbook's standard modules are certainly not accessible from other workbooks in the same VBA project, contrary to the MSoft documentation.

Curious question - Interface Variables (ie dim x as Iinterface = object?) And also if object is a form

vb.net windows forms question.
I've got 3 forms that have exactly the same functions, so I decided to create an interface.
public Interface IExample
public sub Add()
Public sub Edit()
Public sub View()
End Interface
Then I created the 3 forms, and added the 'implements interface IExample' to each.
public class frmExample1
implements Interface IExample
Same for frmExample2, frmExample3
Finally, in code, I declare a variable of the interface type ..
Dim objfrmExample as IExample
then ...
objFrmExample = frmExample2
At this point, objfrmExample is now instantiated, even though I've not done a "objfrmExpample = new [what-goes-here?] " and I'm curious as to why.
I could possibly guess that because you cannot instantiate an interface variable, then vb.net automatically creates an instance. But thats just a guess. The question is , what is meant by declaring a variable of type Interface, and how does it work?
Anyway, just curious :-)
At this point, objfrmExample is now instantiated, even though I've not done a "objfrmExpample = new [what-goes-here?] " and I'm curious as to why.
This has nothing to do with interfaces. You can always treat a form class name in VB as though it were an instance. The reason is that the VB compiler creates properties of all your forms inside My.Forms. Now you can access a “default” instance of each form by accessing My.Forms.<FormName>.
Now comes the ugly part: you can also omit My.Forms.. In other words, whenever you write just FormName and from the context it’s unambiguous that you need an instance rather than the class name, VB will act as though you’d written My.Forms.<FormName>.
Luckily, this only works for forms, not for any other classes. VB creates each default instance when you first access it. So as long as you don’t access a default instance, it’s not created. Once you access it for the first time, VB creates it and invokes its constructor.
When you declare a variable of type interface you can work with any object that implements the interface. Therefore when setting a variable that is of an interface type equal to a class that implements the interface an implicit cast is done. For example.
Dim oExample as IExample
dim testForm as MyTestForm
oExample = MyTestForm
Now, this is the way that you do it, you can do an explicit cast this way
Dim oExample as IExample
Dim testForm as MyTestForm
oExample = CType(MyTestForm, IExample)
For your specific example with VB.NET and an un-instantiated form this is due to a VB "feature" that auto-creates an instance of the form.