Implement a string - vba

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.

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.

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.

Why doesn't .Font.Reset work in Find/Replace VBA Macro?

I have a particular word in my document that has various inline formatting from previous authors. I want to find that word and strip out its inline formatting. If I were using the keyboard, I would select the items one at a time, applying CTRL+Spacebar to each. Instead, I wrote a macro. In general I don't want to assume the nature of the inline formatting; I don't want to worry if it's bold or italics or anything else; I just want to execute a generic CTRL+Spacebar, which translates to .Font.Reset in VBA. This doesn't work, though. Here's my code, which does work, but please see the comments I added, which collectively ask why .Font.Reset doesn't work:
Sub Inline_Formatting_Replacement()
Dim MyDocRange As Range
Set MyDocRange = ActiveDocument.Range
With MyDocRange.Find
.ClearFormatting
.Text = "word_with_inline_formatting"
With .Replacement
.ClearFormatting
.Text = "word_with_inline_formatting"
.Font.Bold = False 'I shouldn't have to use this.
.Font.Italic = False 'I shouldn't have to use this.
.Font.Reset 'This should work instead of using .Bold and .Italic, _
'but this line does nothing. Why?
End With
.Forward = True
.Wrap = wdFindStop
.Execute Replace:=wdReplaceAll
End With
End Sub
As Cindy Meister pointed out, Font.Reset is a method, not a property.
You're configuring Range.Find.Replacement.Font, telling the object model what you want to replace and how it should be formatted. That works when you invoke read/write properties, because read/write properties store state. Methods act on that state.
It makes more sense when you understand how properties work - here's an example:
Option Explicit
Private internalState As Boolean
Public Property Get State() As Boolean
'invoked when .State is on the right-hand side of an expression, e.g. foo = obj.State
State = internalState
End Property
Public Property Let State(ByVal value As Boolean)
'invoked when .State is on the left-hand side of an expression, e.g. obj.State = foo
'the "value" parameter is the result of the right-hand side expression.
internalState = value
End Property
Imagine this internalState contains the information about whether the font should be bold or not: the Font property on the object returned by Replacement is a Font object, i.e. an instance of the Font class. So when you do .Bold = False you're encapsulating that value inside the internal state of that Font object.
Now say you also have a method in that imaginary class module:
Public Sub Reset()
internalState = False
End Sub
Now I don't know what Reset really does - one would have to peek at the source code for that... or see if the documentation has anything useful:
Removes manual character formatting (formatting not applied using a style). For example, if you manually format a word as bold and the underlying style is plain text (not bold), the Reset method removes the bold format.
https://msdn.microsoft.com/en-us/vba/word-vba/articles/font-reset-method-word
So why doesn't it reset the Font of the Replacement object? It's actually a very good question: one could reasonably expect that invoking Reset on the Replacement.Font would effectively "reset" that font.
I'm not very familiar with the ms-word object model (Cindy is a pro though!), but in order for the .Reset to work as a replacement, I believe the Font class would need to do something like this:
Option Explicit
Private doReset As Boolean
Public Property Get ShouldReset() As Boolean
ShouldReset = doReset
End Property
Public Sub Reset()
'does whatever it does
'...
doReset = True
End Sub
And then when the replacement is actually carried out, Word can inspect ShouldReset and invoke the method on the Font instance of the affected Range - which is a different instance than the Font instance on the Replacement object.
Except it wouldn't make any sense for a Font to do something like above, because of course if you invoke Reset then it should reset - what's missing is something like a ReplacementFont class that could reasonably hold that state/information... and it doesn't exist.
If VBA had functions as first-class citizens, then you could perhaps do something like this bastardized nonsensical pseudo-code:
Set .ReplacementAction = New Function(ByVal r As Range)
r.ClearFormatting
r.Text = "word_with_inline_formatting"
r.Font.Reset
End Function
And then when you'd go Find.Execute, the hypothetical ReplacementAction function would pass the actual Range as a parameter to this ...delegated action, and .Reset would operate against the target Range.Font when it's invoked, as opposed to operating on the Replacement.Font when it's setup.
Alas, VBA can't do delegate stuff... or can it?.
I'm pretty sure that calling Find.Replacement.Font.Reset just clears out any replacement font information, so when you do the replace, the font is not changed.
I'm running into a similar effect, where I want to remove character styling from text globally using find/replace and I haven't found any way to do that.
Replacement.Font, .Style, etc seem to have no way of setting them to "clear out any specific font/style/etc". The only choices are to do nothing or set specific values.
As a result in my case I'll have to write a loop that finds each search hit individually and does the equivalent to ctrl-space on the resulting range. This unfortunately would be much slower than the global replace.

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

How can I evaluate a string into an object in VBA?

In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.