Why does an object returning itself as the default property hang excel and crash the debugger? - excel

I recently came accross `Attribute Values.VB_UserMemId = 0'. I like lists so I thought I'd build a bespoke collection type object.
The minimal code for the class that can reproduce the error is:
Class Lst
Option Explicit
Public c As New Collection
'this is the default property
Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0
If IsMissing(index) Then
Set item = Me
'DoEvents
Else
item = c(index)
End If
End Property
Public Property Let item(Optional index, itm)
If IsMissing(index) Then 'assume itm is list
If IsObject(itm) Then Set c = itm.c Else c.add itm
Else
c.add itm, , index
c.Remove index + 1
End If
End Property
Essentially, lst(i) returns the ith element of the private collection, Lst(i)=6 sets the ith element. (errorhandling and index checking code stripped for clarity).
I noticed that objects that return themselves from the default property can be returned from a function in a variant (e.g LstFunc=L below), without the need for a set removing complexity from my students eyes...(you cant do that with a collection object)
Unfortunately, I encountered two challenges...the minimum code for these is:
The Problem
Function LstFunc() As Variant
Dim L As New Lst
L = 4 'replaces L.item=3
LstFunc = L 'this is not normally allowed, but desirable (for me!)
End Function
Sub try()
Dim L As New Lst
L = LstFunc 'replaces L.item=LstFunc-->L.c: [4]
L = 3 'L.c: [4,3]
If L = 6 Then DoEvents
End Sub
Here is what happens
1) when the expression L = 6 is evaluated excel hangs. Some times ESC gets you it back in, but my experience is that excel stops responding and needs a restart.
To evaluate the expression the L.item function is called initially, returning a Lst, for which item is called, etc.etc. resulting in unwanted, and undetected infinite repetition (not quite recursion). Uncommenting the DoEvents statement in the get item property allows you to stop without a crash
2) after uncommenting the DoEvents, I run in debugger mode step by step. If i now hover (by accident..) over the variable L, the debugger crashes, and I get the green triangle of death, which I fear will be very confusing for the students:
Note this behaviour is recoverable if the DoEvents statement in the class is commented out again. A veritable catch 22...
Bit of an intricate one this, but any sugesstions as to how I can trap the unwanted repetition in (1) at low computational cost and without losing the ability to pass the object like a variant would be greatfully received.
PS this is a code snipped that provides an unsafe workaround discussed in a comment below:
Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0
static i
If IsMissing(index) Then
Set item = Me
i=i+1:if i>1000 then item="":exit property
'DoEvents
Else
item = c(index)
i=0
End If
End Property

The recursion can't be avoided.
From section 5.6.2.2 of the VBA language specification:
If the expression’s value type is a specific class:
If the source object has a public default Property Get or a public default function, and this default member’s parameter list is
compatible with an argument list containing 0 parameters, the simple
data value’s value is the result of evaluating this default member as
a simple data value.
Note that with your sample class, this line of code meets all of those conditions:
If L = 6 Then DoEvents
The type of the expression L = 6 is Boolean, with an Lst on the left hand side and an Integer on the right hand side. That means the type of the comparison is Integer, so the run-time checks to see if there is a default Property Get, which you provide here:
Public Property Get item(Optional index)
'Attribute Values.VB_UserMemId = 0
The parameter list is compatible with an argument list containing 0 parameters, because the index is optional. So, it evaluates to L.item() = 6. The only test you do inside the property is If IsMissing(index), which is guaranteed to be true if it's called as the default member - remember, it can't require a parameter to be passed. As you found out, this leads you to...
5.6.2.3 Default Member Recursion Limits
Evaluation of an object whose default Property Get or default function
returns another object can lead to a recursive evaluation process if
the returned object has a further default member. Recursion through
this chain of default members may be implicit if evaluating to a
simple data value and each default member has an empty parameter list,
or explicit if index expressions are specified that specifically
parameterize each default member.
How this is handled is implementation specific. Office VBA implementations, however, do not cap the recursion depth and will simply crash the host when it runs out of stack space.
That said, the rest of your question is simply an x-y problem, although my suggestion is to scrap this. Using default members hides the intent of your code and robust, maintainable code should be readable.

Related

Accessing a VBA dictionary entry with a non-existent key [duplicate]

I am using a dictionary object from the MS Scripting Runtime library to store a series of arrays and perform operations on the array cells as necessary. There is a for loop to go through the process of creating all of these entries. My issue is that when using the .exists property, it is returning True even before the item has been added.
Closer debugging indicates that the key is being added to the dictionary at the beginning of the for loop, even though no .add command is used and will not be used until the end of the loop.
I have tried a few different configurations, but here is a simple example that fails:
Dim dTotals As Dictionary
Set dTotals = New Dictionary
dTotals.CompareMode = BinaryCompare
For Each cell In rAppID
If Not dTotals.Exists(cell) Then
Set rAppIDCells = Find_Range(cell, rAppID)
Set rAppIDValues = rAppIDCells.Offset(0, 6)
dAppIDTotal = WorksheetFunction.Sum(rAppIDValues)
dTotals.Add Key:=cell.Value, Item:=dAppIDTotal
End If
Next cell
Where each cell contains a string / unique id. At the If statement, the code is returning false, even on the first iteration.
In the official documentation‌​ for the scripting runtime it says "If key is not found when attempting to return an existing item, a new key is created and its corresponding item is left empty."
...and yea, when you're debugging in a loop, it appears to pop right out of the sky before the '.exists' function is even called. All is well...
Instead of attempting to add the item that just got added, as in:
dTotals.Add Key:=cell.Value, Item:=dAppIDTotal
...just set the empty object currently at your key to your new one:
dTotals(cell.Value) = dAppIDTotal
So your code block becomes:
If Not dTotals.Exists(cell) Then
Set rAppIDCells = Find_Range(cell, rAppID)
Set rAppIDValues = rAppIDCells.Offset(0, 6)
dAppIDTotal = WorksheetFunction.Sum(rAppIDValues)
dTotals(cell.Value) = dAppIDTotal
End If
Voila. I tend to rediscover this "feature" on every revisit to VBA. You may also notice the effects of it if you are having a memory leak caused by adding new keys that you do not intend to store.
I had this problem manifest itself while debugging when I had a watch that attempted to return the "missing" key's item. Actually, further frustrated debugging had the same problem when I literally had a watch for the [scriptingdictonaryObject].exists() condtional); I suggest that the "missing" key is added because of the watch. When I removed the watch and instead created a temporary worksheet to copy the array to while running, the unwanted keys were no longer added.

VBA: Can't set a variable of a Structure within a class

I have a class, implementing two interfaces, one interface for normal use, and one interface that only reveals a function to create the class with initial parameters. The class has a field that is a Structure with two variables within, I am using the Get property of the class to get the structure, and then using dot notation to access the field within the structure, and then I am trying to set it to a number, but it never works. If I try to access the structure with the private variable within the class it works, but I want to be consistent and only use the properties to modify it within the Create function.
Public Function Create(WorksheetName As String, Optional CurrentRow As Long = 4) As ISheetInfo
With New clsSheetInfo
Set .WS = ThisWorkbook.Worksheets(WorksheetName)
Set .Cols = CreateColumnDictionary(.WS, 3)
Let .Rows.Current = CurrentRow
Let .Rows.Final = .WS.Cells(.WS.Rows.Count, 1).End(xlUp).Row
Set Create = .Self
End With
End Function
When I step through, it first goes to this property (RowData is the UDT with two fields, .Current and .Final:
Property Get Rows() As RowData
Rows = pRows
End Property
But then after the assignment, .Rows.Current is still 0, I'm not sure why.

Excel crash when typing open parenthesis

Here's one I don't understand.
Given this class module (stripped down to the bare minimum necessary to reproduce the crash):
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "TestCrashClass"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Public Function Init() As TestCrashClass
Attribute Init.VB_UserMemId = 0
Dim tcc As New TestCrashClass
Set Init = tcc
End Function
Public Property Get Data() As String
Data = "test data"
End Property
Can anyone tell me why Excel totally craps out when I type in this code:
Sub MakeExcelCrash()
With TestCrashClass(
At this point, I this lovely message:
Even if I type in a full procedure without the offending parentheses and then try to add them later, I get the same crash.
The only way I can get Excel not to crash is to copy/paste a set of () from somewhere else to this line of code.
Sub MakeExcelCrash()
With TestCrashClass()
Debug.Print .Data
End With
End Sub
If the Init() method has a parameter—even an optional one—it won't crash when the opening paren is typed.
I'm more curious about why this happens than ways around it; it doesn't actually come up that often in my code and when it does I can fix it with a change in approach, but I'm really frustrated that I don't know what's causing these crashes. So maybe someone who knows more about the inner working of VBA can explain it to me?
You don't even need the With block. Any attempt to type ( after the class name takes Excel down.
The problem is that you have the VB_PredeclaredId set to true and the default member is trying to return itself. When you attach a debugger to the dying Excel instance, you can see that the underlying issue is a stack overflow:
Unhandled exception at 0x0F06EC84 (VBE7.DLL) in EXCEL.EXE: 0xC00000FD:
Stack overflow (parameters: 0x00000001, 0x00212FFC).
When you type With TestCrashClass(, what happens is that VBA starts looking for an indexer on the default property, because Init() doesn't have any properties. For example, consider a Collection. You can use the default property's (Item) indexer like this:
Dim x As Collection
Set x = New Collection
x.Add 42
Debug.Print x(1) '<--indexed access via default member.
This is exactly equivalent to Debug.Print x.Items(1). This is where you start running into problems. Init() doesn't have parameters, so VBA starts drilling down through the default members to find the first one that has an indexer so IntelliSense can display the parameter list. It starts doing this:
x.[default].[default].[default].[default].[default]...
In your case, it's creating an infinite loop because [default] returns x. The same thing happens in the Collection code above (except it finds one):
Throw in the fact that you have a default instance, and the end result is something like this:
Private Sub Class_Initialize()
Class_Initialize
End Sub
As #TimWilliams points out, having a default member that returns an instance of the same class (or a class loop eg. ParentClass.ChildClass.ParentClass.ChildClass... where ParentClass and ChildClass both have default members), and when used in certain syntax cases, such as a With block, will cause VBE to try and resolve the default member.
The first parenthesis makes VBE assume there must be a method, indexed get or array index that will take an argument, so it sets off to resolve the ultimate target member.
So the incomplete line, with a cursor located after the parenthesis:
With TestCrashClass(
Is effectively the same as:
With TestCrashClass.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init.Init '....You're inquisitive scrolling this far over, but you get the point.
At some point, your system or VBE runs out of resources and exits with the grace and poise of a thermonuclear group-hug.
+1 for improvising with a copy/pasta of a parentheses pair.
Sounds like some sort of corruption. I've had Excel behave irrationally like this before, normally in large projects, and the only way to get around it is to drag all of your classes etc into a new project.
I suspect it happens because Excel doesn't truly delete classes, modules, worksheets etc that have been removed. You can tell this because of the file size.
There is no Compact and Repair functionality, as in Access, as far as i'm aware

Creating a Container Property in a VBA Class which returns Indexed Items (Excel VBA 2003)

I started learning VBA for my job at the end of last summer, and I can proudly say this is the first time I haven't be able to find the answer on Google. I started teaching myself about Classes this week, and I have come across a situation where I would like to be able to identify an "indexed property" for my class.
Since that probably isn't the clearest explanation, here is a hypothetical example:
The class which I have created for my super awesome sandwich shop (clsSASS) contains properties for Calories, Weight in Grams, Price, and Ingredients. The first three are variables with very straight forward let and get statements. I.E.:
Public pCal As Integer
Public Property Get Calories() As Integer
Calories= pCal
End Property
Public Property Let Calories(Value As Integer)
pCal = Value
End Property
Ingredients however is designed to contain, in order of entry, the list of ingredients. My initial instinct was to do something like this:
Public pIngd As Collection
Public Property Get Ingredients(Value As Integer) As Collection
Ingredients = pIngd(Value)
End Property
Public Property Set Ingredients(Object As Collection)
Set pIngd = Object
End Property
So if Bacon were the first ingredient in the list (and let's be honest it always would be), something like clsNewSandwich.Ingredients(1) would return the string 'Bacon'.
The problem arose when I added a container property to a class, and then couldn't figure out how to identify the individual items in the container. So this may just be a simple syntax issue that has nothing to do with classes whatsoever.
Many Thanks!
*edited for clarity/continuity
OK - I will retract my advice about always naming let/set and Get the same, since in this case you cannot, since the "input" and "output" types are not the same. So, in the sample below I've named the property which just returns one ingredient as Ingredient
Class "clsSASS":
Dim pIngd As Collection
Property Set Ingredients(c As Collection)
Set pIngd = c
End Property
Property Get Ingredient(v As Integer) As String
Ingredient = pIngd(v)
End Property
Regular module:
Sub Tester()
Dim c As New Collection
Dim s As New clsSASS
c.Add "bacon"
c.Add "lettuce"
c.Add "tomato"
Set s.Ingredients = c
Debug.Print s.Ingredient(1) 'bacon
Debug.Print s.Ingredient(2) 'lettuce
Debug.Print s.Ingredient(3) 'tomato
End Sub

How to emulate the ScriptingContext's "ASPTypeLibrary.Application" Object

I have been tasked with modifying some legacy ActiveX DLLs written in Visual Basic 6. One of the things I need to do is to emulate the "ScriptingContext" object, (so that we can support other mechanisms for running the DLLs other than IIS without having to re-write large chunks of the code).
Something that has been causing me some grief is the "ASPTypeLibrary.Application" object which has two very different ways to access its stored values, eg:
.Application("KeyName")
or
.Application.Value("KeyName")
How can I create my own VB6 class which supports both of these access mechanisms? I can do one or the other but not both?
(a simple code example would be great thanks, I'm not a VB6 programmer)
I have found a way to do this, see the code snippet below taken from two classes, "clsContext" and "clsContextApp". The latter implements the ".Value" functionality and the former has the ".Application" property...
I have now discovered an even more difficult problem. The ScriptingContext's "ASPTypeLibrary.Request" object has three different ways to access its ".Request.QueryString" property:
.Request.QueryString("KeyName")
or
.Request.QueryString.Value("KeyName")
or
.Request.QueryString
The last method returns a string comprised of all the Key/Value pairs concatenated by "&" characters. I have no idea how to implement this?
' clsContext
Public ContextApp As clsContextApp
Public Property Get Application(Optional ByRef Key As Variant = Nothing) As Variant
If (Key Is Nothing) Then
Set Application = ContextApp
Else
If (Not ContextApp.p_Application.Exists(Key)) Then
Application = ""
Else
Application = ContextApp.p_Application.Item(Key)
End If
End If
End Property
Public Property Let Application(ByRef Key As Variant, ByVal Value As Variant)
If (VarType(Key) = vbString) Then
If (VarType(Value) = vbString) Then
If (Not ContextApp.p_Application.Exists(Key)) Then
ContextApp.p_Application.Add Key, Value
Else
ContextApp.p_Application.Item(Key) = Value
End If
End If
End If
End Property
' clContextApp
Public p_Application As Scripting.Dictionary
Public Property Get Value(Key As String) As String
If (Not p_Application.Exists(Key)) Then
Value = ""
Else
Value = p_Application.Item(Key)
End If
End Property
Public Property Let Value(Key As String, Value As String)
If (Not p_Application.Exists(Key)) Then
p_Application.Add Key, Value
Else
p_Application.Item(Key) = Value
End If
End Property
Well I've managed to answer the additional question regarding ScriptingContext's "ASPTypeLibrary.Request" object which has three different ways to access its ".Request.QueryString" property.
I've included a code snippet below that is based on the code from my previous answer for the "ASPTypeLibrary.Application" object. If I add a new Property to the "clsContextApp" class and make it the default property for that class, then it will be called when the ".Application" property is called without any qualification eg:
MyString = Context.Application
Setting a particular property as the default property in VB6 is a little obscure, but I followed the directions I found here.
' clsContextApp Default Property
Property Get Values(Optional ByVal Index As Integer = -1) As String ' This is the Default Value for clsContextApp
Attribute Values.VB_UserMemId = 0
Dim KeyName As String, Value As String
Values = ""
If (Index < 0) Then
For Index = 0 To p_Application.Count - 1
KeyName = p_Application.Keys(Index)
Value = p_Application.Item(KeyName)
If (Index > 1) Then
Values = Values + "&"
End If
Values = Values + KeyName + "=" + Value
Next Index
Else
If (Index < p_Application.Count) Then
KeyName = p_Application.Keys(Index)
Value = p_Application.Item(KeyName)
Values = KeyName + "=" + Value
End If
End If
End Property
Adding a reference to Microsoft Active Server Pages Object Library, and to COM+ Services Type Library, and then using the object browser reveals some basic things you seem to be missing.
GetObjectContext is a global method in COMSVCSLib with no arguments used to retrieve the current ObjectContext as its return value.
ObjectContext is a Class. It has a read-only default property, named Item that takes a String argument and is of type Variant.
Passing "Application" as an argument to Item returns the current instance of the Application Class.
ScriptingContext is a Class. It is obsolete.
Application is another Class. It has a default property named Value that takes a String argument and is of type Variant.
Value is a property of the Application Class and provides access to a read-write key/value pair store where keys are always Strings. Since it is of type Variant you can store objects as well as simple values and arrays of various types.
None of this looks difficult to replicate in VB6. The key/value store could be a Collection or Scripting.Dictionary.

Resources