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
Related
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.
These two subs do the same thing when inside a class.
Sub DemoMe( )
Me.AboutMe ' Calls AboutMe procedure.
End Sub
Sub DemoMe( )
AboutMe ' Does the same thing.
End Sub
What is the point? Does the Me keyword do anything? What is the preferred way of an object accessing its own members?
tldr; No, although there are situations where it can be useful.
From the VBA language specification (5.3.1.5):
Each procedure that is a method has an implicit ByVal parameter called
the current object that corresponds to the target object of an
invocation of the method. The current object acts as an anonymous
local variable with procedure extent and whose declared type is the
class name of the class module containing the method declaration. For
the duration of an activation of the method the data value of the
current object variable is target object of the procedure invocation
that created that activation. The current object is accessed using the
Me keyword within the <procedure-body> of the method but cannot be
assigned to or otherwise modified.
That's all it is, just a "free" local variable that refers to the specific instance that the method is being called on. This also happens to be the default context for the procedures during their invocation, so it can be omitted if the code is intended to operate on the current instance. Although as #HansPassant points out in the comment above, it also allows the editor to bind to the interface and provide IntelliSense.
That said, there are a couple instances where you would either want to or have to use it (this is by no means an exhaustive list):
Naming collisions:
If your class has a member that "hides" a built-in VBA function, it can be used to make the scope explicit:
Public Property Get Left() As Long
'...
End Property
Public Property Get Right() As Long
'...
End Property
Public Property Get Width() As Long
Width = Me.Right - Me.Left
End Property
Equity Checks:
Public Function Equals(other As Object) As Boolean
If other Is Me Then
Equals = True
Exit Function
End If
'...
End Function
Fluent Functions:
This can be a useful pattern for compositing objects - you perform an action, then return the instance of the class so they can be "chained". Excel's Range interface does this in a lot of cases:
Public Function Add(Value As Long) As Class1
'Do whatever.
Set Add = Me
End Function
Public Sub Foo()
Dim bar As New Class1
bar.Add(1).Add(1).Add 1
End Sub
Not any more than there are reasons to use this in Java, C#, or any other language: it's a reserved identifier that represents the current instance of the class - what you do with that is up to your imagination.
What is the preferred way of an object accessing its own members?
Indeed, an object doesn't need the Me keyword to access it own public interface. Same as this in other languages, I'd even call it redundant. However it can sometimes be a good idea to explicitly qualify member calls with Me, especially when the class has a VB_PredeclaredId attribute (e.g. any UserForm): referring to UserForm1 in the code-behind of UserForm1 yields a reference to the default instance of the class, whereas qualifying member calls with Me yields a reference to the current instance of that class.
Accessing Inherited Members
VBA user code can't do class inheritance, but a lot of VBA classes do have a base class. The members of UserForm when you're in the code-behind of UserForm1, and those of Worksheet when you're in the code-behind of Sheet1, aren't necessarily easy to find. But since the inherited members show up in IntelliSense/auto-complete, you can type Me. and browse a list of members inherited from the base class, members that you would otherwise need to know about in order to invoke.
A class creating an instance of itself inside itself? That I've never seen.
You're missing out! I do this all the time, to enable referring to the object instance held by a With block, inside a Factory Method - like this GridCoord class.
Public Function Create(ByVal xPosition As Long, ByVal yPosition As Long) As IGridCoord
With New GridCoord
.X = xPosition
.Y = yPosition
Set Create = .Self
End With
End Function
Public Property Get Self() As IGridCoord
Set Self = Me
End Property
Note that while the GridCoord class exposes a getter and a setter for both X and Y properties, the IGridCoord interface only exposes the getters. As a result, code written against the IGridCoord interface is effectively working with read-only properties.
Another use is to get the name of the class module, without needing to hard-code it. This is particularly useful when raising custom errors: just use TypeName(Me) for the Source of the error.
The Builder Pattern notoriously returns Me, which enables a "fluent API" design that makes it possible to write code that incrementally builds complex objects through chained member calls, where each member returns Me (except the final Build call, which returns the type of the class being built):
Dim thing As Something
Set builder = New ThingBuilder
Set thing = builder _
.WithFoo(42) _
.WithBar("test") _
.WithSomething _
.WithSomethingElse
.Build
#PBeezy : In addition to my comment :
Me, refers to the object it's coming from so AboutMe resides in the class. If you had another instance, say this is Class1, you'd have dim c as Class1, as soon as you create an instance of Class1 in Class1, you need to tell the compiler which class you are using, the holding class or the instance created in, where, me.class1.aboutme would be logically valid. You can also create, a class for each cell in a workbook, then you could refer to A1's class from B1's class. Also, if there is a public function/sub called AboutMe, this also helps.
Class (clsPerson)
Public c1 As clsPerson
Public strPersonName As String
Public Function NAME_THIS_PERSON(strName As String)
strPersonName = strName
End Function
Public Function ADD_NEW_CHILD(strChildName As String)
Set c1 = New clsPerson
c1.strPersonName = strChildName
End Function
Normal module
Sub test()
Dim c As New clsPerson
c.NAME_THIS_PERSON "Mother"
c.ADD_NEW_CHILD "Nathan"
Debug.Print c.strPersonName
Debug.Print c.c1.strPersonName
End Sub
Gives these results
Mother
Nathan
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.
I have an access database and I'm attempting to write some VBA to increase automation.
I have a module I've entitled Global Variables which I've successfully used to define global constants (file paths etc) and a module ReportCode which has two main subrouties, one to run a query with ADODB (scraping form params where needed - returning a recordset), and a second which takes this record set and writes the data out to an excel template.
Given I may want to have multiple queries write to multiple tabs I thought the best way was to define a ExportDocument object to contain common parameters and a OrgReport object, containing query and tab specific parameters - then gather multiple OrgReport objects in a collection.
I'd hope to then pass just these two parameters into the main subroutine. This turns out to be a pain in VBA (or at least compared to ruby!).
Here you can see how I've defined by custom objects
Option Private Module
' Define Custom Doc Object
Public Type ExportDocument
TeamName As String
TemplatePath As String
SaveName As String
SavePath As String
End Type
' Define Custom Report Object
Public Type OrgReport
Query As String
Fields As Variant
Sheet As String
StartCol As Integer
StartRow As Integer
Headers As Boolean
End Type
And here is the code in my form which then called an additional module which does the heavy lifting - I know that part works because it did before I tried to go all OOP on this...
Private Sub my_report_from_form_Click()
' Prep Query Inputs
Dim TeamX_Report As OrgReport
TeamX_Report.Query = "qry_TeamReporting Query"
TeamX_Report.Sheet = "RawData"
TeamX_Report.StartCol = 1
TeamX_Report.StartRow = 2
TeamX_Report.Headers = True
TeamX_Report.Fields = Nothing
' Prep Document Inputs
Dim Teamx_Doc As ExportDocument
Teamx_Doc.TeamName = "MyTeam"
Teamx_Doc.TemplatePath = strReportTemplatePath & "MyTeam.xltm"
Teamx_Doc.SaveName = ""
Teamx_Doc.SavePath = strReportSavePath & Teamx_Doc.TeamName
' Init and set collection for CHAIN reports
Dim TeamReports As New Collection
TeamReports .Add Item:=TeamX_Report, Key:=TeamX_Report.Query
Call export_data_dump(Teamx_Doc, TeamReports)
End Sub
This gives me the issue of:
Only public user defined types defined in public object modules can be
used as parameters or return types for public procedures of class
modules or as fields of public user defined types
Following advice here I changed
Dim Teamx_Doc As ExportDocument
to
Teamx_Doc = CreateObject("ExportDocument")
But alas now i get
Run-time error '429': ActiveX component can't create object VBA
All references to this problem seem to be related to calling code from the Word., Excel. or Outlook. codebases, so perhaps I'm just missing a prefix for my own module stored within my database?
Best lead I've found is this one, which seems to suggest there's deeper issues with what i'm trying to do, or that i may get around parts by calling Friend, though I'm lost to where and how.
Is there a way I can late bind my UDT Objects within my form code, stash one in a collection then pass both to a subroutine that will be able to grab params from the first 'Doc' object and then iterate through the second 'report' object?
VBA >_<
There's no reason I can see why this doesn't work:
Dim Teamx_Doc As ExportDocument
Especially if you're not getting an error on line
Dim TeamX_Report As OrgReport
I've used custom Public Types before - no need for CreateObject
Though the docs seem to say it's just fine, can you try removing the
Option Private Module
The error message is kinda misleading. You simply can't put variables with an User-defined Type into a collection.
Option 1: Use an array instead. This actually sounds like it would work well for what you want to do.
Option 2: Create a class module instead of an UDT for OrgReport. Then instantiate objects of that class, those you can add to a collection.
See
Excel VBA Collections and Custom Data Types
and
http://www.mrexcel.com/forum/excel-questions/16849-adding-user-defined-types-collection.html
I am using late binding (in C#, using Reflection, etc.) to access Excel via COM. In other words, I start by getting the Excel.Application object using
Type excelType = Type.GetTypeFromProgId("Excel.Application");
object excelApplication = Activator.CreateInstance(excelType);
(Actually, it's more generic than that, but that gives the idea).
As many people have noted in StackOverflow, one good way to see how to use the COM interfaces to Office is to record a macro in Excel and then look at the VBA code to see which class members to invoke.
When I do that, I see that if you have a reference to the active cell (obtained from the ActiveCell property of the Excel.Application object), then VBA references ActiveCell.Offset(x,y) to reference a cell relative to that ActiveCell (x and y being the row and column indices). However, if I try to access the Offset method of a Cell with late binding, the InvokeMember method fails with an exception saying "Member not found".
If I use the Visual Studio Object Browser to inspect the Microsoft.Office.Interop.Excel namespace and the ApplicationClass class (which I assume gives a good look into the COM interface obtained by the above C# code getting it via the ProgId), it shows that ApplicationClass has an ActiveCell property which is of type Microsoft.Office.Interop.Excel.Range. And inspecting that class shows that indeed it does NOT have a member named "Offset", hence the "Member not found" when I try to Invoke it.
That seems to mean that the VBA macros are using a different object model for Excel from that which is exposed via COM! Is that right, or am I missing something? And if it is the case, how can I get at that same VBA object model via late binding in COM?
Or, is there some way, using methods other than Offset, to be able to move the ActiveCell to the beginning of the next row, which is really what I want?
You could try this:
dim i as integer
i = ActiveCell.Row + 1
Excel.ActiveSheet.Cells(i,1).value = "Whatever your value is."
The answer is, as usual, easy when you know how.
My code a bit further down from the sample in my question looked a bit like this:
object activeCell;
object result;
// in here code to get the correct value into activeCell
if(activeCell.GetType().GetProperty("Offset") == null){
result = activeCell.GetType().InvokeMember("Offset", BindingFlags.InvokeMethod, null, activeCell, new object[] {1, 0});
}
It was that InvokeMember that threw an exception with the message "Member not found".
When I changed the parameters to InvokeMember to read as follows:
result = activeCell.GetType().InvokeMember("Offset", BindingFlags.InvokeMethod | BindingFlags.GetProperty, null, activeCell, new object[] {1, 0});
then it worked.
Just why it works is still a mystery, which I am sure will earn someone some reputation if they answer it.