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

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.

Related

How can I create a proper Collection in VBA?

I am trying to convert a large 3 dimensioned Array into a series of class modules. I have each next class stored as an array in the previous class. It goes like Brand -> Products -> Lots.
I have successfully created this interaction and can access them by name like:
Sub test()
Dim MyBrand As Brand
Set MyBrand = New Brand
MyBrand.Name = "Company1"
MyBrand.AddProduct "Shoes"
MyBrand.Products("Shoes").AddLot "240502"
MsgBox MyBrand.Products("Shoes").Lots(0) 'Correctly Displays "240502"
End Sub
But then I wanted to create an object group that can save multiple Brand objects and access them like Brands("Company1").
If I used an array inside a class module, I'd end up with Brands.Brand("Company1").
If I used a Collection, I'd have to use indexes like Brands(1).
Is there a way to create a proper object group so that I can mimic the syntax of groups like Application.Workbooks and refer to members by Name?
A lot of the magic behind custom collections depends on hidden attributes that you cannot edit from within the VBE; you need to export (and remove from the project when prompted) the class module, edit its magic member attributes in Notepad/Notepad++, save changes, and then re-import the module into the project.
That's obviously tedious and error-prone, but there's a (much) better way.
In order to support this:
Set shoesProduct = MyBrand.Products("Shoes")
You can define Products as a Dictionary and call it a day, but then encapsulation as a concept is... well, taking a beating here (whether the internal collection is a Dictionary, a Collection, or a .NET ArrayList should typically be an implementation detail that the rest of the code doesn't need to care about).
I suspect the Brand class has too many responsibilities and "is" the product collection; best practices would be to have the Brand.Products property defined as follows:
Public Property Get Products() As Products
So you'll want to have a Products class (very much like the Workbook.Worksheets and Workbook.Sheets properties both return a Sheets collection object) that encapsulates a private, module-level VBA.Collection field (possibly keyed, but you can't access or iterate the keys of a collection).
The Products custom collection class needs an Item default property (the name Item is a convention); the implementation just pulls the item from the private encapsulated Collection:
'#DefaultMember
Public Property Get Item(ByVal Index As Variant) As Product
Set Item = ThePrivateCollection.Item(Index)
End Property
If you are using Rubberduck, this #DefaultMember annotation/comment is going to trigger an inspection result about the annotation and the corresponding hidden attribute(s) being "out of sync"; right-click that inspection result and pick "Adjust attribute values" to have Rubberduck generate the hidden code for you and deal with the annoying export/delete-edit-reimport cycle.
Otherwise, you'll want to manually edit the hidden VB_UserMemId member attribute that makes it the class' default member:
Public Property Get Item(ByVal Index As Variant) As Product
Attribute Item.VB_UserMemId = 0
Set Item = ThePrivateCollection.Item(Index)
End Property
And with that, MyBrand.Products("Shoes") becomes equivalent to MyBrand.Products.Item("Shoes").
Perhaps you want to iterate all the products in the collection, too?
For Each Product In MyBrand.Products
Debug.Print Product.Name
Next
In order to do this, you need a special "enumerator" member that forwards the enumerator from the encapsulated collection:
'#Enumerator
Public Property Get NewEnum() As IUnknown
Set NewEnum = ThePrivateCollection.[_NewEnum]
End Property
Again, Rubberduck annotations greatly simplify doing this, but everything Rubberduck does, you can also do manually if you like:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = ThePrivateCollection.[_NewEnum]
End Sub
And now For Each iteration works for your custom object collection!
If a Lot was more than just a String value (i.e. an actual object type), then the Product class could use a Lots custom collection too - but since a Lot is really just a String value (or is it?), then Product can simply encapsulate a Dictionary, and have a Lots property that exposes the Items array:
Public Property Get Lots() As Variant
Lots = ThePrivateLotsDictionary.Items
End Property
Note, that's simpler than using a Collection, because with a collection you'd need to iterate it and copy each item to an array in order to return the items without exposing the collection itself (exposing Lots() As Collection makes the AddLot member completely redundant).
As for the Brands collection itself, heed Tim Williams' advice and use a Dictionary data structure.
You can use a Scripting.Dictionary with Name as the key:
Sub test()
Dim MyBrand As Brand
Dim Brands As Object
Set Brands = CreateObject("scripting.dictionary")
Set MyBrand = New Brand
MyBrand.Name = "Company1"
MyBrand.AddProduct "Shoes"
MyBrand.Products("Shoes").AddLot "240502"
Brands.Add MyBrand.Name, MyBrand
MsgBox Brands("Company1").Products("Shoes").Lots(0)
End Sub

How to organize a set of similar functions in two parallel chained-classes

I would like to obtain this set of properties with 1 class + 2 chained-classes:
mytable.EntireRange
mytable.DataBodyRange
mytable.HorizontalHeaderRange
mytable.VerticalHeaderRange
mytable.col(strTitle).num
mytable.col(strTitle).firstDataCell
mytable.col(strTitle).lastDataCell
mytable.row(strTitle).num
mytable.row(strTitle).firstDataCell
mytable.row(strTitle).lastDataCell
What's the best way to organize (and name) these classes and properties?
My current solution to add the chained properties is:
- I named the three classes "mytable", "mytable_col", "mytable_row" to keep a visible relationship in the Project Explorer
- inside "mytable", I added this:
Property Get col(strTitle As String) As mytable_col
Set col = New mytable_col
col.Initialize = Me ' passes the parent object
End Property
Property Get row(strTitle As String) As mytable_row
Set row = New mytable_row
row.Initialize = Me ' passes the parent object
End Property
Now... given that the three properties ("num", "firstDataCell", "lastDataCell") of the two parallel chained classes "col" and "row" share the same logic with little variations, I'd prefer to not put their complete code separated in 6 properties, three in the "row" class and three in the "col".
I ideally would to create just 3 properties/functions (passing a "row" or "col" as argument for the variations) instead than six.
How should I do? Is it possible, without to put them in an additional module?

Create a loopable container class in VBA

I have been trying to clean up my code a bit and make it more similar to the Excel object model, and I was wondering if it is possible to create a "loopable" container class in VBA, e.g. similar to how you can do:
Dim Sheet As Worksheet
For Each Sheet In ThisWorkbook.Worksheets
' ...
Next Sheet
I want this functionality for my own container.
Say I create my own class called Container which contains items of some class ItemType (this can simply be an empty class for this example):
' Class Container
' The container contains items of a class I will call ItemType
Private Type MContainer
Items As Collection ' Could also be implemented in terms of an array
End Type
Private This As MContainer
Public Property Get Item(ByVal Index As Long) As ItemType
Attribute Item.VB_UserMemId = 0 'Makes it so I can access elements like a Collection
Set Item = This.Items(Index)
End Property
Public Function Add() As ItemType
This.Items.Add
Set Add = This.Items(This.Items.Count)
End Function
Private Sub Class_Initialize()
Set This.Items = New Collection
End Sub
I then want to loop through the items in my container with the For Each..., but this doesn't work. See the following example for how I ideally want it to work:
Public Sub MyMethod()
Dim Stuff As New Container
Stuff.Add
Dim Element As ItemType
For Each Element In Stuff ' <- This will not work
' Do something
Next Element
End Sub
The final For loop is what I am looking at making work. Is this possible? Basically the issue is that I can't call For Each on my Container class similar to how you can with e.g. the Excel.Sheets class. Is this possible to achieve in VBA?
For Each iteration requires a special member attribute value to work, and a NewEnum property or function returning an IUnknown.
Every collection class that can be iterated with a For Each loop has a hidden [_NewEnum] member (the square brackets are required for accessing the hidden member, since the underscore prefix is illegal for an identifier in VBA.
Tweaking module and member attributes isn't possible to do directly in the VBE, so you need to remove/export the module, modify it in e.g. Notepad++, save the changes, then re-import it into your project.
Or, have Rubberduck (disclaimer: I contribute to this open-source project) do it for you, using annotations (aka "magic comments"):
'#Enumerator
'#Description("Gets an enumerator that iterates through the internal object collection.")
Public Property Get NewEnum() As IUnknown
Set NewEnum = this.Items.[_NewEnum]
End Function
'#DefaultMember
'#Description("Gets/sets the element at the specified index.")
Public Property Get Item(ByVal index As Long) As ItemType
Set Item = this.Items(index)
End Property
Then parse the project (Ctrl+`) and bring up the Inspection Results toolwindow (Ctrl+Shift+i) - there should be a number of "Missing Attribute" results under "Rubberduck Opportunities":
Click "Fix all occurrences in module" in the bottom pane, to synchronize the hidden attributes with the annotation comments.
If you have "Missing Annotation" results, Rubberduck has determined that a module/member has a non-default value for a given attribute, and is able to similarly add an annotation comment that surfaces/documents it with a comment.
The Code Explorer (Ctrl+R), the Rubberduck toolbar, and the VBE's own Object Browser (F2) will display the contents of the VB_Description attribute, so #Description annotations are particularly useful to have on any public procedure.
Object Browser:
Code Explorer:
Rubberduck toolbar:
Add this to your class
Public Function NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = Items .[_NewEnum]
End Function
An alternative approach to this issue is not to use a Collection but a Scripting.Dictionary. One of the advantages of a scripting dictionary is that it can return arrays of the keys and items of the dictionary. Iterating over an array in VBA is a trivial exercise.

Do I need the Me keyword in class modules?

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

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