How to limit scope of functions in a standard module - vba

I've prepared a standard module, MyPath, which contains a few useful functions. In this case standard module is better for me than a class module.
Much more comfortable to write:
Dim ext As String
ext = MyPath.getFileExtension("test.docx") ' returns "docx"
Instead of:
Dim Pth As MyPath
Set Pth = New MyPath
Dim ext As String
ext = Pth.getFileExtension("test.docx")
or something like
Dim ext As String
With New MyPath
ext = .getFileExtension("test.docx")
End With
The only problem with my "pseudo-static-class" module is its scope (and IntelliSense use).
Dim ext As String
ext = getFileExtension("test.docx") ' This works, but I don't want it to
What I would like to achieve is:
If one doesn't specify MyPath when calling a function, IntelliSense does not suggest the methods from the module and they cannot be used.
In order to use any methods from the MyPath module, one needs to type in MyPath., and after the dot IntelliSense should suggest methods from the module.
I've tried some combinations with Option Private Module, Private functions, but no one works the way I described.
I am preparing a lot of classes and similar modules and I would keep everything in order.
edit
In short, I would like to force the compiler to throw an error if I don't specify the "parent" module for the function or sub. At the same time, if I type the name of my module I would be able to use IntelliSense (no solutions with run/call, just simply type module name, dot and select the member).

Another solution, inspired by VBA's Err object, is to implement a function that returns a reference to your "static object"
Public Static Function MyPath() As PathObject
Dim result As PathObject
If result Is Nothing Then Set result = New PathObject
Set MyPath = result
End Function
Where PathObject is the class that contains all your pseudo static methods.
This approach has the added bonus that the caller cannot set their MyPath reference to Nothing - in case you decide your Static module should be stateful (perhaps for some costly initialisation routine)
PS I think the Static Function syntax is cute, but you could equally just declare result with the static keyword
In fact you could possibly make it even more concise with
Public Static Function MyPath() As PathObject
Dim result As New PathObject
Set MyPath = result
End Function
Not tested, but should have the behaviour that MyPath is instantiated only when used for the first time, but thereafter hangs around indefinitely

The behavior you describe is actually achievable via a Predeclared Class, but will require a little prep work.
To make a predeclared class, you can write up a class with whatever functions you want. Then export it to text (.cls) and edit it, setting the Attribute VB_PredeclaredId to True. Then re-import it. My Class is called PredeclaredClass and looks like this in the .cls file:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "PredeclaredClass"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Attribute VB_Ext_KEY = "Rubberduck" ,"Predeclared Class Module"
Option Explicit
Public Sub ThisIsVisible()
End Sub
After you import this class, you can now use its methods only by referencing the class module (and no new instance needed).
You will not see it in intellisense nor be able to use its function without the module reference. The compiler will complain, provided you are using Option Explicit
By the way, Rubberduck makes this pretty easy to do this without the need to export/import modules.

Related

VBA: Use of Public Property Get in place of Const (for Non-Unicode Characters)

I have a VBA code where I need to define a constant string containing Non-Unicode characters (£). As some might know, VBA Editor doesn't support non-unicode and uses windows "System Locale" setting in regional and language setting to parse/map these characters. The machine I develop the code is set to English system Locale but some of the users have that setting as other languages, e.g. Chinese which turns the string constant to question mark (£ --> ?).
Now, £ = chr(163), however, you cannot use chr as part of defining a constant in VBA. So while this is allowed:
public const mystring = "reference constant string with £"
this is not allowed in VBA"
public const mystring = "reference constant string with " & chr(163).
One way around is to define mystring as a public/global variable:
Constants.bas
public mystring as string
and then set the public variable on start of running code or Excel opening.
ThisWorkbook
Private Sub Workbook_Open()
mystring = "reference constant string with " & chr(163).
End Sub
One issue with this process is the public variables get cleared when error happens or the code stops. To keep the value an alternate I came across was to avoid public variable and instead use a public property get. Note that I have to include this a part of a class.
**.cls
Public Property Get mystring () As String
mystring = "\R;;" & Chr(163)
End Property
So, now I am wondering if there will be any issue with this approach? or perhaps there is a better approach to address the constant variable with non-unicode character.
The main issue is the name of the module, Constants - it's misleading, since neither a public/global variable nor a public get-only property are constants.
Side node, constants not being assignable to non-constant expressions isn't a limitation that's specific to VBA.
Properties are perfectly legal in standard modules, and public get-only properties are a perfectly fine way to expose a read-only value that needs to be constructed at run-time.
the public variables get cleared when error happens or the code stops
Assuming "when error happens" involves clicking End and effectively ending the execution context, that is true of everything exposed anywhere, whether it's a global variable, a public property, an object, or anything that exists in-memory at run-time... and that's just normal stuff - the value is simply available on-demand any time.
Again there's no need to have a class module for this, this is perfectly legal in a standard module:
Option Explicit
Public Property Get MyString() As String
MyString = "\R;;" & Chr(163)
End Property
If re-creating the string every time the property getter is accessed is a problem, then you need a way to persist its value in a backing field - but then, this backing field (whether it's in a class or standard module) only has a value when the execution context exists, meaning it has exactly the same problem a global variable would have: execution stops, it's gone.
One work-around could be to use a class with the VB_PredeclaredId attribute set to True (default is False).
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Class1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit
Private internalString As String
Private Sub Class_Initialize()
internalString = "\R;;" & Chr(163)
End Sub
Public Property Get MyString() As String
MyString = internalString
End Property
And now VBA will automatically create an instance of Class1 whenever it's referenced, as soon as it's referenced, and that instance remains "alive" until an End statement is explicitly executed, or the execution context is otherwise terminated. Exactly like, say, a UserForm class, you access this default instance using the class name as an identifier:
Debug.Print Class1.MyString
If the default instance existed when Class1 was referenced, the internalString is returned. If it didn't, then Class_Initialize executes, then the internalString is returned.

Implement a string

I would like to make a class which looks like a String to VBA. That is, if I make a function with a string argument, and pass this object instead, I won't get any errors.
I thought Implements String would work, but apparently it's not even an option!
Why is it not possible, and is there any way to get the behaviour I'm after? (Of course I could just make my own IString interface and specify my functions to request that, but I don't want to do that)
I'm trying to make a file-selection dialogue, which I can pass to any functions that require string-filepaths as arguments. This would be a neat self contained way of retro-fitting file-selection to existing functions.
Because the String is not an "object" to VBA, as it is to other languages like Java or .NET. If you want custom behavior, I'd probably just create a custom VBA class that wraps a string, rather than implementing it, and return a String output, in similar vein to a string builder class.
With credit to the awesome Chip Pearson (http://www.cpearson.com/excel/DefaultMember.aspx):
You can do this by exporting the .cls to a text file; editing it Notepad to add a default attribute; saving it; then re-importing it into VBA. Let's give you an example.
In a class module:
Property Get Value() As String
Value = "Hello"
End Property
Then export the module. I called it Str.cls. I then opened this file in Notepad, and added the following line (as marked):
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Str"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Property Get Value() As String
Attribute Value.VB_UserMemId = 0 ' <-- THIS is the line I added.
Value = "Hello"
End Property
Save the file again in Notepad: then in the VBE, re-import it. Note however that the extra line (and all the other surrounding information) will NOT appear in the VBE, so it will look like your code hasn't changed at all. However you can now do the following (in a regular module):
Sub ReturnSringClass()
Dim S As New Str
MsgBox S
End Sub
Note now no property is required to be named with "S". It just behaves exactly like a string. You should be able to put a file selector in your class like this.

VBA global class variable

My obstacle is trying to get multiple subs to recognize class variables. When I try to declare them globally, I get a compile error: "Invalid outside procedure". Then, when I run a public function or sub to declare the variables, they remain undefined in the other subs. I want multiple subs to recognize the variables because their values are supposed to be altered via UserForm, and then utilized in a different sub.
If it could work in this manner, great, but I understand that my design could fundamentally be flawed. Please advise!
This is my Class definition, inserted as a Class module named "cRSM":
Option Explicit
Private pName As String
Private pDesiredGrowth As Double
'Name of RSM
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
'Growth property
Public Property Get DesiredGrowth() As Double
DesiredGrowth = pDesiredGrowth
End Property
Public Property Let DesiredGrowth(Value As Double)
If Value > 0 And Value < 1 Then
pDesiredGrowth = Value
End If
End Property
This is invalid procedure error (which I put in the Global Declarations section):
'Bedoya
Dim Bedoya As cRSM
Set Bedoya = New cRSM
Bedoya.Name = "Bedoya"
And this is the "variable not defined error" (within a private sub):
Private Sub Add_Click()
**Bedoya.DesiredGrowth** = Txt2.Value
Thank you for your time
In a standard module (I name mine MGlobals), put
Public Bedoya As cRSM
Then in another standard module (I name mine MOpenClose), put
Sub Initialize()
If Not Bedoya Is Nothing Then
Set Bedoya = New cRSM
End If
End Sub
Any default properties you want set should be set in the Class_Initialize procedure. In any procedure that you want to use Bedoya, use
Initialize
and it will instantiate the global variable if necessary. The only difference between this and the New keyword is that you can't accidentally instantiate the variable with this method. You either call Initialize or you don't. A lot of VBA developers use New, but almost never do for that reason.
If I understood well You want a global object.
You can put the declaration in module like
public Bedoya As cRSM
then you have create the object ... you can use a global event inside the Workbook like
Private Sub Workbook_Open()
Set Bedoya = New cRSM
Bedoya.initialize("Bedoya") 'a method to initialize private variables
End Sub
Now you can use the global object. You have to restart the excel file or run this method manually.
Is not good style to use global variables, but sometimes is the more easy to do :P
What you want to do nowadays is done using singleton Software Pattern, but this is for other day hehehe

vba/catvba class instantiation

i want to declare class globaly
here is my example:
i want to use class clsIEError that looks exactly like this:
Option Explicit
Public Sub m(msg As String, Optional title As String = "Title:")
'Attribute Value.VB_UserMemId = 0
'this method will be used as defualt method
'and here are attributes msg and title used to create some inteface
End Sub
and this is how it works example1:
Sub CATMain()
Dim ie As clsIEError
Set ie = New clsIEError
ie "test", "title"
Set ie = Nothing
End Sub
but my problem is that i want to have it globally example2:
Option Explicit
Public ie As clsIEError
Private Function Init()
Set ie = New clsIEError
End Function
Sub CATMain()
Call Init
' and to use it same as in example 1
ie "test", "title"
' but i am able to use it only like:
' ie.m "test", "title" 'works as expected
Set ie = Nothing
End Sub
why with public default method doesnt work?
I can confirm that this doesn't work just as you describe. I get "Expected procedure, not variable" at run-time, but no compile errors. It must be a bug in the VB parser, but that's the only explanation I can come up with.
I always thought that the attribute had to match the element name. Where you have
Attribute Value.VB_UserMemId = 0
I thought you should have
Attribute m.VB_UserMemId = 0
But it seems to work (with the locally declared variable) either way. It's a terrible answer, but the answer is to explicitly call the method. Sorry.
I am not sure that it will answer your question explicitly, but, as for me I wanted to create an instance of a class that would be accessible throughout my project. Basically I needed to create Static Class or Singleton. I found this post that was quite useful.

Restrict type in a Collection inside a class module

I have a collection inside a class module. I'd like to restrict the object type that is "addable" to this collection, i.e. collection should only ever accept objects of one given type and nothing else.
Is there any way to enforce the type of objects added to a collection?
From what I can tell, there is no built-in way to do this. Is the solution then to make this collection private, and build wrapper functions for the methods usually accessible for Collections, i.e. Add, Remove, Item, and Count?
I kinda hate having to write 3 wrapper functions that add no functionality, just to be able to add some type enforcement to the Add method. But if that's the only way, then that's the only way.
There is no way to avoid wrapper functions. That's just inherent in the "specialization through containment/delegation" model that VBA uses.
You can build a "custom collection class", though. You can even make it iterable with For...Each, but that requires leaving the VBA IDE and editing source files directly.
First, see the "Creating Your Own Collection Classes" section of the old Visual Basic 6.0 Programmer's Guide:
http://msdn.microsoft.com/en-us/library/aa262340(v=VS.60).aspx
There is also an answer here on stackoverflow that describes the same thing:
vb6 equivalent to list<someclass>
However, those are written for VB6, not VBA. In VBA you can't do the "procedure attributes" part in the IDE. You have to export the class module as text and add it in with a text editor. Dick Kusleika's website Daily Dose of Excel (Dick is a regular stackoverflow contributer as you probably know) has a post from Rob van Gelder showing how to do this:
http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/
In your case, going to all that trouble - each "custom collection" class needs its own module - might not be worth it. (If you only have one use for this and it is buried in another class, you might find that you don't want to expose all of the functionality of Collection anyway.)
This is what I did. I liked Rob van Gelder's example, as pointed to by #jtolle, but why should I be content with making a "custom collection class" that will only accept one specific object type (e.g. People), forever? As #jtolle points out, this is super annoying.
Instead, I generalized the idea and made a new class called UniformCollection that can contain any data type -- as long as all items are of the same type in any given instance of UniformCollection.
I added a private Variant that is a placeholder for the data type that a given instance of UniformCollection can contain.
Private mvarPrototype As Variant
After making an instance of UniformCollection and before using it, it must be initialized by specifying which data type it will contain.
Public Sub Initialize(Prototype As Variant)
If VarType(Prototype) = vbEmpty Or VarType(Prototype) = vbNull Then
Err.Raise Number:=ERR__CANT_INITIALIZE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__CANT_INITIALIZE) & _
TypeName(Prototype)
End If
' Clear anything already in collection.
Set mUniformCollection = New Collection
If VarType(Prototype) = vbObject Or VarType(Prototype) = vbDataObject Then
' It's an object. Need Set.
Set mvarPrototype = Prototype
Else
' It's not an object.
mvarPrototype = Prototype
End If
' Collection will now accept only items of same type as Prototype.
End Sub
The Add method will then only accept new items that are of the same type as Prototype (be it an object or a primitive variable... haven't tested with UDTs yet).
Public Sub Add(NewItem As Variant)
If VarType(mvarPrototype) = vbEmpty Then
Err.Raise Number:=ERR__NOT_INITIALIZED, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__NOT_INITIALIZED)
ElseIf Not TypeName(NewItem) = TypeName(mvarPrototype) Then
Err.Raise Number:=ERR__INVALID_TYPE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__INVALID_TYPE) & _
TypeName(mvarPrototype) & "."
Else
' Object is of correct type. Accept it.
' Do nothing.
End If
mUniformCollection.Add NewItem
End Sub
The rest is pretty much the same as in the example (plus some error handling). Too bad RvG didn't go the whole way! Even more too bad that Microsoft didn't include this kind of thing as a built-in feature...
I did almost the same code of Jean-François Corbett, but I adapted because for some reason wasn't working.
Option Explicit
Public pParametro As String
Private pColecao As New Collection
Public Sub Inicializar(ByVal parametro As String)
pParametro = parametro
End Sub
Public Sub Add(NewItem As Object)
If TypeName(NewItem) <> pParametro Then
MsgBox "Classe do objeto não é compatível à coleção"
Else
pColecao.Add NewItem
End If
End Sub
Public Property Get Count() As Long
Count = pColecao.Count
End Property
Public Property Get Item(NameOrNumber As Variant) As Variant
Set Item = pColecao(NameOrNumber)
End Property
Sub Remove(NameOrNumber As Variant)
pColecao.Remove NameOrNumber
End Sub
Then, when i want to create an instance from CCollection I make like the code bellow:
Set pFornecedores = New CCollection
pFornecedores.Inicializar ("CEmpresa")
Where CEmpresa is the class type from the object I want
Yes. The solution is to make your collection private and then make public wrapper functions to add, remove, getitem and count etc.
It may seem like hassle to write the additional code but it is a more robust solution to encapsulate the collection like this.