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

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.

Related

How to limit scope of functions in a standard module

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.

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 - Class property with reserved keyword as the name

In VBA, is there any known mechanism to fool the compiler into allowing the use of reserved keywords as names for class properties? For example, I would like to create a property called Select in one of my class modules. However, the compiler flags my declaration as an error. Below is the syntax I used:
Public Property Get Select() As Database.SQLStatements
End Property
Database is my VBA project name and SQLStatements is one of the class modules I created. Also, I'm running the code in MS Access 2010.
You can do that and use any keyword/reserved word in your VBA. But then it will make your code a lot messy and very hard to read/debug/maintain.
If you have a bool property named If in your class you will end up with something like this If .If  Then, well, good luck reading that. Also code maintenance like Find/Replace/rename etc. will need extra caution and more work.
Anyhow, if you are willing to go through all this pain, here is how you do it.
After the keywords/reserved words add a invisible blank space using ALT+0160 and that's it. VBA will consider it a perfectly legal name. e.g. If .
Also, you will have to either use intellisense for using these propertynames or manually type the altcode everywhere. That's extra typing.
clsTest
Option Explicit
Private m_sSelect As String
Private m_bIF As Boolean
Public Property Get Select () As String '~~> Select () is actually typed as SelectALT+0160()
Select  = m_sSelect
End Property
Public Property Let Select (ByVal sNewValue As String)
m_sSelect = sNewValue
End Property
Public Property Get If () As Boolean
If  = m_bIF
End Property
Public Property Let If (ByVal bNewValue As Boolean)
m_bIF = bNewValue
End Property
Test Module
Option Explicit
Sub demo()
Dim objTestClass As clsTest
Set objTestClass = New clsTest
With objTestClass
.Select  = "It works. But it will, for sure, create readibility/maintenance issues."
.If  = False
End With
MsgBox objTestClass.Select 
'/ See how hard it will to read/debug this sort of code
With objTestClass
If .If  Then '~~> This line here :)
MsgBox "If prop value is TRUE"
Else
MsgBox "If prop value is FALSE"
End If
End With
End Sub
ALT+0160 <> Space

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

How do I declare an object as public?

VB.net gets mad when I say:
Public thisTicket as new ticket
and insists I need to use Dim instead of Public, but this prevents me from accessing the object outside of the sub I declared it in.
Basically I am trying to do the following in one sub:
Public thisTicket as new ticket
thisTicket.completed = true
and then this in another sub:
if thisTicket.completed = true then
'do this
else
'do this instead
end if
Class:
Public Class ticket
Property hasStatusChange As Boolean
Property initialAssignmentStatus As String
Property initialApprovalStatus As String
Property initialCompletionStatus As String
Property newApprovalStatus As String
Property newCompletionStatus As String
Property newAssignmentStatus As String
Property wasUpdated As Boolean
End Class
Another question,
If I made a new property under ticket called "completionChanged as boolean"
and the criteria for that being true or false would be if the initialCompletionStatus was not equal to the newCompletion status (then it would be true),
how would I get it so that I could say:
if thisTicket.completionchanged = true then
and have it know on it's own whether it's true or not, without me having to spell out the if/then each time?
A Local variable in a Sub or Function or property getter or setter (i.e. in a method) lives only while the method is being executed. Before or after the call of this method, the local variable does not exist. Therefore modifiers like Public make no sense there.
If you need to access a variable from outside a method, make it a class member (either a field or a property).
Answer to second question. This is why properties exist. You can add logic to them that is executed when they are accessed.
Private m_completionStatus As String
Property CompletionStatus() As String
Get
Return m_completionStatus
End Get
Set(ByVal Value As String)
If Value <> m_completionStatus Then
m_completionStatus = Value
completionChanged = True
End If
End Set
End Property
And then please write
If thisTicket.completionchanged Then
and drop the " = true ". What the If-statement wants is an expression yielding a Boolean result, and a Boolean variable is such an expression in itself. There is not need to have any comparison in an If-statement.
If completionchanged is True then completionchanged = True is True also. If completionchanged is False then completionchanged = True is False also. Comparing with "= True" is a bit like multiplying a number with 1. It doesn't change anything.
If you would like it to be a member of the class, such as a property.
Public Property ThisTicket As Ticket
And in your class' constructor initialize it.
Public Sub New()
ThisTicket = new Ticket
End Sub
Now you will be able to access this ticket from inside of your class. However, consider whether other places in your code really need to know about this member. If they do not, make it private. If you may extend this class in the future where the subtypes would need access, make it protected.
If you decide to make it private, you can bypass making it a property and just create it as a field.
Private _thisTicket As Ticket
You should also ask yourself if thisTicket really belongs to the state of the containing object. Does the containing object process a lot of tickets? Maybe the ticket should be passed into the processing function instead of being contained. If the containing object's state does concern itself with only that one ticket throughout its life, then making it a property is valid.
As for your second question..
Create a function in Ticket to evaluate the business logic you mentioned and return the boolean value for if it has changed or not.
Public Function HasStatusChanged As Boolean
logic...
End Function