Why must Set be used to hold objects? - vba

While working on my PowerPoint macro, I noticed the following:
To obtain the current, active slide:
Dim currSlide As Slide
Set currSlide = Application.ActiveWindow.View.Slide
To obtain a newly created Textbox:
Dim textbox As Shape
Set textbox = currSlide.Shapes.AddTextbox(...)
I'm new to VBA, having worked with Java, C++ & C#. Why must Set be used above? Why does using Slide & Shape instead generate errors? In what way is VBA different in how variable declaration work in this respect?

This is taken from Byte Comb - Values and References in VBA
In VBA, the difference between value types and reference types is made explicit by requiring the keyword Set when assigning a reference. In addition, you will often see assigning a reference referred to as “binding”.
Byte Comb goes into great detail about the inter working of the VBA. I highly recommend that any serious about programming using the VBA read through it.
In layman's terms: Set is used so that the compiler knows that the programming wants to have a variable reference an Object type. In this way, the compiler will know to throw an error when you are trying to assign a value to an object.
Another reason is that objects can have default values.
In this example I have a variable named value that is of a Variant type. Variants can be of any type in the VBA. Notice how the compiler knows that to assign the value of the Range when value = Range("A1") and to set a reference to the Range when the keyword Set is used in Set value = Range("A1").

Related

How to handle object declaration in VBA (Error 91)

I'm stuck in VBA and I couldn't find a good answer in the other questions related to error 91. I want to create an object and store variables and arrays inside that object. I tried an approach like I would do in js:
Dim block As Object
...
Set block = Nothing
block.Name = "Unbekannter Prüfblock"
block.Awf = "Unbekannter Anwendungsfall"
block.Conditions = Array()
block.Checks = Array()
I use the "Set block = Nothing" because I will use it multiple times in a loop.
But all I get is error 91 - Object variable not set
How can I set the object?
Do I really have to declare everything in vba?
Isn't there a "stop annoying me with declaration notices" toggle? ;-)
Update
Thank you all so much for the detailed answers!
As suggested I created a class for "block" and also a class for "condition" and "check". Block for example:
Option Explicit
Public name As String
Public awf As String
Public conditions As Collection
Public checks As Collection
Then I use it inside my code like this:
Dim bl As Block
Dim co As Condition
Dim ce As Check
Set bl = New Block
bl.name = ws.Range("B" & i).value
bl.awf = ws.Range("B" & i).value
Set co = New Condition
co.attr = ws.Range("B" & i).value
co.value = ws.Range("C" & i).value
bl.conditions.Add co
VBA isn't Javascript; objects and their members cannot be created inline, they need a class definition.
When you make a member call against an object, you refer to it by name, and whenever that name refers to a null reference (Nothing) you'll get error 91.
To fix it, you need to ensure every member call is made against a valid object reference. Using the Set keyword you can assign such a reference, and to create a new instance of an object you can use the New keyword followed by the name of the class that defines the type you want a new instance of:
Dim Block As Object
Block.Something = 42 ' Error 91
Set Block = New SomeClass ' set reference
Block.Something = 42 ' OK
Note that because the object is declared As Object, every member call is late-bound (resolved at run-time); if the member doesn't exist (or if there's a typo), you'll get error 438 at run-time.
You can move this error to compile-time with early binding by using a more specific data type for the declaration:
Dim Block As SomeClass
Because the members of SomeClass are known at compile-time, the IDE will now provide you with a member completion list when you type up a member call, and typos will no longer be valid at compile-time: strive to remain in the early-bound realm whenever possible! Note: As Variant (explicit or not) is also similarly late-bound.
So we add a new class module and call it SomeClass and we add a couple of public fields:
Option Explicit
Public Name As String
Public Case As String
Public Condition As Variant
Public Check As Variant
And now you can create and consume a new instance of that class, and add instances of it to a collection to process later (note: you can't do that with a UDT/Type).
The VBIDE settings have an annoying option ("automatic syntax check", IIRC) that immediately pops a message box whenever there's a compilation error on the current line; uncheck it (invalid lines will appear in red, without a message box), but do have the "require variable declaration" setting checked: it will add Option Explicit to every module, and that will spare you from a number of easily avoidable run-time errors, moving them to compile-time.
In JS, you can add properties (together with values) on the fly to an object. That's not possible in VBA (and most other languages).
Your declaration Dim block As Object is defining a variable that is supposed to point to an Object. But it isn't pointing to anything yet, per default it is initialized with Nothing, which is, literally, nothing, and has neither properties nor methods, it's just a placeholder to signal "I don't point to anything yet". Furthermore, Object cannot be instantiated.
in VBA, you assign something to an object variable with Set (this is different to most other languages). If you want to create a new object, you use the keyword New.
However, before you do that, you need to know what kind of object (which class) you need. This can be an existing class, eg a Worksheet or a Range in Excel, or it can be an own defined class (by creating a new class module in your code). In any case, you need to define the properties and the methods of that class. Considering the most simple class module Class1 (of course you should think about a more usefull name):
Option Explicit
Public name as String
Public case as String
(I will not start to talk about private members and getter and setter).
You then write
Dim block As Class1
Set block = New Class1
block.name = "Unbekannter Prüfblock"
(But block.data = "Hello world" will not be possible as data is not a member of your class.)
Big advantage of this attempt is that the compiler can show you when you for example mistyped a property name before you even start your code (Debug->Compile). In JS, you will get either a runtime error or a new property on the fly - hard to find nasty bugs.
If you later need a new (empty) object, just create a new object using New. If the old object is not referenced anywhere else, the VBA runtime will take care that the memory is freed (so no need to write Set block = Nothing).
In case you really don't know the properties in advance (eg when you read XML or JSON files or a key-value list from an Excel sheet...), you can consider to use a Collection or a Dictionary. Plenty of examples on SO and elsewhere.
One remark to block.Condition = Array(). Arrays in VBA are not really dynamic, you cannot add or remove entries easily during runtime. In VBA, you have static and dynamic arrays:
Dim a(1 to 10) as String ' Static array with 10 members (at compile time)
Dim b() as String ' Dynamic array.
However, for dynamic members you need to define how many members you need before you write something into it, you use the ReDim statement for that. Useful if you can calculate the number of members in advance:
Redim b(1 to maxNames) ' Dynamic array, now with maxNames members (whatever maxNames is)
You can change the array size of a dynamic array with Redim Preserve, but that should be an exception (bad programming style, inperformant). Without Preserve, you will get a new array, but the former data is lost.
Redim Preserve b(1 to maxNames+10) ' Now with 10 more members.
If you really don't know the number of members and it can change often during runtime, again a Collection or a Dictionary can be the better alternative. Note that for example a Dictionary can itself a Dictionary as value, which allows to define Tree structures.
Regarding your issue adding to the collection:
You need to add this code to your class module "Block" - only then you can add objects to the collections
Private Sub Class_Initialize()
Set conditions = New Collection
set checks = new Collection
End Sub

Using brackets when creating object in vba excel?

I have copied code from a vba project that I found to go from xml to excel but it gives me an error in my vba project, I have checked the reference libraries.
ruta = LCase(CreateObject([explorador]).BrowseForFolder(0, "selecciona la carpeta a procesar", 0, "").items.Item.Path)
I made the following change and it worked
ruta = LCase(CreateObject("shell.application").BrowseForFolder(0, "selecciona la carpeta a procesar", 0, "").Items.Item.Path)
but then it came back to this line
With CreateObject([openFile])
I get the error 13 that the execution times do not match. Variables do not match
I check the variables and they are correctly: unsure:
I don't understand why with the original file it runs smoothly and the replica doesn't. It has been very little I have found it with respect to the syntax of those lines of code when writing it [explorer] and [openFile]
Square brackets in VBA are used for what the language specification calls "foreign identifiers"; they're how you can explicitly invoke a Range object's default member, which is a hidden member named _Default - since VBA identifiers cannot begin with an underscore, doing MsgBox SomeRange._Default would be illegal. So we do MsgBox SomeRange.[_Default] instead and now the code can compile and run. Same with the SomeCollection.[_NewEnum] hidden member, when creating custom collection classes.
When the bracketed identifier doesn't contain any illegal-in-an-identifier characters, then they are purely redundant.
Various VBA hosts also implement some form of Evaluate mechanism; when hosted in Excel, you can do this MsgBox [A1] and you'll get the value of cell A1 from the active worksheet - in this context [A1] is an expression VBA takes and passes to the host application (here Excel), which evaluates it and returns a result to VBA - in this case a Range object reference.
So what CreateObject([explorador]) really does, is really this:
CreateObject(ActiveSheet.Range("explorador").Value)
Except Excel doesn't understand what explorador is referring to (is the sheet supposed to define a explorador named range? I can't imagine why you'd want to do that though), and comes back with what would show up as a #NAME? error on a worksheet, wrapped up in a Variant/Error value:
CreateObject(CVErr(XlErrName)) 'type mismatch error every time!
Lose the square brackets, you'll lose the headache with them!
Assuming explorador and openfile are String variables containing a valid/registered ProgID, CreateObject(explorador) should work as intended.
Consider using either string literals or declared constants with CreateObject: having a layer of indirection here is obscuring exactly what type of COM object is being created. With CreateObject("Scripting.FileSystemObject") can only fail if the Scripting.FileSystemObject isn't in the registry.

What VBA variable type to use when reading values from a cell in Excel?

According to this answer one should always use Variant when assigning values in a cell to a variable in the code. Is this correct? I seem to recall reading elsewhere that using Variant indiscriminately is not a good practice.
You can read a cell value into any type you want, VBA will (try to) implicitly convert it to that type for you.
There are dozens of questions on this site involving run-time errors raised from reading cell values into a specific data type - perhaps you've seen this error message before?
Type mismatch
That's the error you get when you try to read a cell containing an error value (e.g. #REF!) into anything other than a Variant.
So if you read a cell value into, say, a Double, everything will work fine as long as you're reading something that VBA can coerce into that data type. The problem is that, well, data is never 100% clean, worksheets do break down, users delete columns and break formulas, lookups fail and the person that wrote the formula didn't bother wrapping it with IFERROR, etc.
That's why you read cell values into a Variant.
That doesn't mean you work with a Variant.
Dim cellValue As Variant
cellValue = someRange.Value
If IsError(cellValue) Then Exit Sub 'bail out before we blow up
Dim workingValue As String
workingValue = CStr(cellValue)
By assigning to another data type, you effectively cast the Variant to that more specific type - here a String. And because you like explicit type conversions, you use VBA's conversion functions to make the conversion explicit - here CStr.
Now, in real code, you probably wouldn't even bother reading it into a Variant - you can use IsError to test the cell value:
If IsError(someRange.Value) Then Exit Sub 'bail out before we blow up
Dim cellValue As String
cellValue = someRange.Value ' or cellValue = CStr(someRange.Value)
The flipside here is that you're accessing the cell twice. Whether or not that's better that reading it into a Variant is for you to decide; performance-wise, it's usually best to avoid accessing ranges as much as possible though.
The value you get from a cell (which is a Range) is a Variant according to the documentation:
Range.Value Property (Excel)
Returns or sets a Variant value that represents the value of the specified range.
Since a Variant can represent different data types, you could loose information if you would assign a cell's value to -- for instance -- a variable of type String.
The mere fact that there is data type information in a Variant already means you lose that type of information. If for instance the original type was numeric and you store it in a String variable, there is no way to know from that string value what the original data type was. You could also lose precision (on Date milliseconds for instance).
Furthermore, a Variant type value cannot always be cast to the data type of your variable, and so you could get a Type mismatch error. In practice this often happens with the Error sub data type.
Only when you know beforehand what the data type is of a certain cell's value, it would be good to define your receiving variable in that data type.
Not strictly answering your question, but thought I'd add this for reference anyway.
With native Excel functions you can usually provide either a range object or a value directly to a function. For example, you can either write =AVERAGE(A1,A2,A3) or =AVERAGE(10,20,30). If you want to do something similar for any user defined functions, you will need to check the type of object passed to your function:
Function test(input As Variant)
Dim var As Variant
If TypeName(input) = "Range" Then
var = input.Value
Else
var = input
End If
You may also want to check for other objects if your function can accept them, but doing this will make your functions behave more like users expect them to.

Returning the Default Property

I fill A1 and A2 as follows:
I then run:
Sub WhatIsGoingOn()
Dim r As Range, sh As Worksheet
Set r = Range(Cells(1, 1))
Set sh = Sheets(Cells(2, 1))
End Sub
I expected that in both cases, VBA would use the default property of Cells (the Value) property to Set each variable. However I get a runtime error 13 on the last line of code!
In order to avoid errors, I must use:
Sub WhatIsGoingOn2()
Dim r As Range, sh As Worksheet
Set r = Range(Cells(1, 1))
Set sh = Sheets(Cells(2, 1).Value)
End Sub
What is going on here ??
The difference is in how the input to their default properties is handled by the implementation of the Range and Sheets objects.
The default property of both the Range and the Sheets object takes a parameter of type Variant. You can pass anything to it, so no type coercion will be necessary. In your first example you pass a Range object to both.
How the default properties handle the input is up to themselves. Apparently the property of the Range tries to retrieve the default value of the passed parameter, in your example an address as String. The Sheets object doesn't seem to be so forgiving and raises an error because you neither passed a number nor a String.
Inconsistency is one of the strengths of VBA...
Btw., passing CStr(Cells(2, 1)) would also work, because you explicitly cast to String before passing as a parameter.
Perhaps Leviathan's comment that "Inconsistency is one of the strengths of VBA..." may ring true, but there are some contextual details that his answer neglects (and is technically incorrect on some subtle points). He is correct that for the given code that all of the parameters are variants, but the statement that "no type coercion will be necessary" can be misleading and perhaps just wrong in many cases. Even if many objects and methods are programmed to handle multiple types and default values, this very question reveals that it is a mistake to avoid purposely ensuring (or coercing) the correct data type. Avoiding default properties altogether (by always typing out the full reference) can avoid many headaches.
A significant difference between the lines of code for this particular question is this: Range is a property that takes parameters, while Sheets is also a property but has no parameters. Range and Sheets are NOT objects in this context, even though they are properties which do return Range and Sheets objects, respectively. They are properties of an (automatic global) object defined for the particular module or Excel workbook instance. This detail is not trivial for understanding what the code is actually doing.
The Obect Browser in the VBA window reveals the following metadata for the two properties:
Property Range(Cell1, [Cell2]) As Range
Property Sheets As Sheets
For Range(Cells(1, 1)), the argument Cells(1,1) is passed to the parameter Cell1. Cells itself is a property of the Excel.Global hidden object and it returns a Range object, so that Cells(rowindex, colindex) is calling a hidden default property of the Range class equivalent to Cells._Default(rowindex, colindex). The return type of the property _Default() is not declared, so technically it could return any type in a variant, but inspection shows that it returns a Range object. Apparently passing a Range object to its default property will attempt to take the default value and if it is a valid range value, like a string with a range expression, then it will execute without error.
The default property for the Sheets class is the parameterized hidden _Default(Index) method. Thus, Sheets(Cells(2, 1)) is equivalent to Sheets._Default(Cells(2, 1)). More importantly, it means that Sheets._Default(Cells(2, 1)) is passing a Range object as an index value, but documentation says that it expects an integer or string value. We already mentioned that the index parameter is variant... and when passing an object to a variant, it always passes the actual object and never its default property. So we know that Sheets.Item obtains a Range object in that call. Here is were Levithan was correct in that Sheets.Item can decide what to do with it. It is likely that it could have been smart enough to get the single string value and continue without error. Other collection objects (with a default Item(index) property) in MS Office objects do not seem to exhibit this same "pickiness", so it appears that Sheets._Default() (and perhaps Sheets.Item()) is being rather strict on validating its arguments. But this is just a particular design issue with this method only... not necessarily an overall issue with VBA.
What can be difficult is determining exactly what the source objects of the properties are. Inside the ThisWorkbook module, Me.Sheets reveals that Sheets is a property of the particular Workbook for the module. But Me.Range is not valid in the Workbook module, but right-clicking on the Range property (without the Me qualifier) and choosing "Definition" results in the message "Cannot jump to Range because it is hidden". However, once in the Object Browser, righ-clicking within the browser one can choose "Show Hidden Members" which will then allow navigating to the hidden Global objects and other hidden members.
Why the inconsistency and the hidden properties? In an attempt to make the current instance of Excel and all of its components accessible in a "natural" way, Excel (and all Office applications) implement these various hidden properties to avoid the "complexity" of having to repeatedly discover and type out full references. For instance, the automatic global Application object also has both Range and Sheets properties. Actual documentation for Application.Sheets, for example, says "Using this property without an object qualifier is equivalent to using ActiveWorkbook.Sheets". Even that documentation fails to say is that ActiveWorkbook is in turn a property of the global Excel Application object'.

Difference between Long and Object data type in VBA

In VBA, the Long and Object data type are both 4-bytes, which is the size of a memory address. Does this mean that, technically, the Object data type doesn't do anything that a Long couldn't do? If yes, then is it safe to say that the Object data type exists simply to make it easier for the programmer to distinguish between the purpose of the variable?
This question came up as I was considering Win32 API function declarations. They are often times declared as Long, and, unless I am mistaken, their return value is simply a memory address. Seems like defining these functions as Object would have been more appropriate, then.
Am I totally off? Thanks in advance.
Based on VBA/MSDN help:
Long (long integer) variables are stored as signed 32-bit (4-byte)
numbers ranging in value from -2,147,483,648 to 2,147,483,647.
and the other definition:
Object variables are stored as 32-bit (4-byte) addresses that refer to
objects. Using the Set statement, a variable declared as an Object can
have any object reference assigned to it.
From practical point of view they are both different and used in different situation. Which are essential: Long >> refers to numbers and Object >> refers to object.
Look into the following VBA code (for Excel) where I added comments which is allowed and which is not:
Sub test_variables()
Dim A As Object
Dim B As Long
'both below are not allowed, throwing exceptions
'A = 1000
'Set B = ActiveSheet
'both are appropriate
Set A = ActiveSheet
B = 1000
End Sub
Finally, in terms of API it's better to stay with original declaration and not manipulate with that to avoid any risk on unexpected behaviour of API functions.