3

While this code works and I can assign and retrieve values across all levels, intellisense only displays the methods or properties 1 level deep. How would I go about coding this so that I can follow my "Path" all the way down using intellisense and not necessarily have to just remember the methods or properties?

for instance if I type Wip. I get
Intellisense Showing Methods/Properties

but when I type Wip.Parts("Test"). , the SequenceNumbers member and its Methods/Properties are not displayed No Intellisense Menu

I have the following code

clsSeq:

Option Explicit
Private iSeq As String
Private iQty As Double

Public Property Get Qty() As Double
    Qty = iQty
End Property

Public Property Let Qty(lQty As Double)
    iQty = lQty
End Property

Public Property Get Sequence() As String
    Sequence = iSeq
End Property

Public Property Let Sequence(lSeq As String)
    iSeq = lSeq
End Property

clsPart:

Option Explicit

Private iPart As String
Public SequenceNumbers As Collection

Public Property Get PartNumber() As String
    PartNumber = iPart
End Property

Public Property Let PartNumber(lPart As String)
    iPart = lPart
End Property

Public Sub AddSequence(aSeq As String, aQty As Double)
    Dim iSeq As clsSeq
        If SeqExists(aSeq) Then
            Set iSeq = SequenceNumbers.Item(aSeq)
            iSeq.Qty = iSeq.Qty + aQty
        Else
            Set iSeq = New clsSeq
            With iSeq
                .Sequence = aSeq
                .Qty = aQty
            End With
            SequenceNumbers.Add iSeq, iSeq.Sequence
        End If
        Set iSeq = Nothing
End Sub

Private Sub Class_Initialize()
    Set SequenceNumbers = New Collection
End Sub


Private Function SeqExists(iSeq As String) As Boolean
    Dim v As Variant
        On Error Resume Next
        v = IsObject(SequenceNumbers.Item(iSeq))
        SeqExists = Not IsEmpty(v)
End Function

clsParts:

Option Explicit

Public Parts As Collection

Public Sub AddPart(iPart As String)
    Dim iPrt As clsPart
        If Not PartExists(iPart) Then
            Set iPrt = New clsPart
            With iPrt
                .PartNumber = iPart
            End With
            Parts.Add iPrt, iPrt.PartNumber
        End If
End Sub

Private Function PartExists(iPT As String) As Boolean
    Dim v As Variant
        On Error Resume Next
        v = IsObject(Parts.Item(iPT))
        PartExists = Not IsEmpty(v)
End Function


Private Sub Class_Initialize()
    Set Parts = New Collection
End Sub

modTest:

Sub TestWipCls()
    Dim Wip As clsParts
    Dim Part As clsPart

        Set Wip = New clsParts
        Wip.AddPart ("Test")
        Set Part = Wip.Parts("Test")
        Part.AddSequence "Proc7", 1505
        Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
        Part.AddSequence "Proc7", 100
        Debug.Print Wip.Parts("Test").SequenceNumbers("Proc7").Qty
End Sub
Glenn G
  • 667
  • 10
  • 24

1 Answers1

3

That is because Parts is a Collection and its Default Member Call (or .Item) will return a value/object depending what was stored. While editing your code VBA does not know what kind of value/object is stored in the collection (as this is only established during run-time, eg. late-bound), so it can not give you any Intellisense-suggestions.
To circumvent this, you need a method (property/function) that returns a defined type of value/object (early-bound).
btw. (myCollection.("Foo") is the same as myCollection.Item("Foo"))

The solution is to create a custom collection that returns a value of a defined type.
The following example also explains how to implement a custom Collection so you can use the default member call instead of using .Item.
How to use the Implements in Excel VBA

While we're at it, please never use public variables in classes, make them accessible via Property Let/Set/Get methods!
More on this here: https://rubberduckvba.wordpress.com/2019/07/08/about-class-modules/

Edit:

Example for a custom Collection for classes that implement ICustomElement (Interfaces are explained in the link above)

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "CustomCollectionTemplate"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
'@Folder("Classes")
Option Explicit


Private Type  TCustomCollection
    CustomCollection as Collection
End Type
Dim this as TCustomCollection


Private Sub Class_Initialize()
        Set this.CustomCollection = New Collection
End Sub

Private Sub Class_Terminate()
        Set this.CustomCollection = Nothing
End Sub

Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Attribute NewEnum.VB_MemberFlags = "40"
    Set NewEnum = this.CustomCollection.[_NewEnum]
End Property

Public Sub Add(ByVal newCustomElement As ICustomElement)
    this.CustomCollection.Add newCustomElement
End Sub

Public Sub Remove(ByVal Index As Long)
    this.CustomCollection.Remove Index
End Sub

Public Function Item(ByVal Index As Long) As ICustomElement
    Set Item = this.CustomCollection.Item(Index)
End Function

Public Function Count() As Long
    Count = this.CustomCollection.Count
End Function

Thanks to M.Doerner & Mathieu Guindeon for the edits/comments

L8n
  • 728
  • 1
  • 5
  • 15
  • 2
    Doesn't `Collection.Item` actually return a value of type `Variant`? – M.Doerner Aug 07 '19 at 15:48
  • 2
    Member calls against both `Object` and `Variant` are, through different mechanisms, resolved at run-time (i.e. late-bound); the key to making IntelliSense work everywhere, is to avoid late-bound member calls of any kind. – Mathieu Guindon Aug 07 '19 at 15:56
  • @M.Doerner It is not defined what is returned according to: https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/item-method-visual-basic-for-applications Is there even a way to test if something is "wrapped" into a variant without looking at the memory/RAM directly? – L8n Aug 07 '19 at 16:40
  • 1
    There are two simple things that tell you that it must be `Variant`. First, the object browser in the VBE does not state a type, which always means that the type is `Variant`, just like in VBA code. Second, you can store strings and integers in collections, which are no objects. – M.Doerner Aug 07 '19 at 16:58
  • @L8n so in the following `Public Function Item(ByVal Index As Long) As ICustomElement` would I be replacing `ICustomElement` with my own class module name for that element? Example: `Public Function Item(ByVal Index As Long) As clsSeq` – Glenn G Aug 09 '19 at 16:26
  • 1
    @Glenn G exactly, although in my case it was not a class but an Interface, (basically a class that has method declaration but no code), so I could have multiple classes implement that interface and be used in this collection. (The rubberduck link explains it well) – L8n Aug 10 '19 at 07:45
  • Now if I could just get the enum to work :/ I might make some real progress on my utility – Glenn G Aug 10 '19 at 07:48
  • Have you followed these steps? https://stackoverflow.com/questions/19373081/how-to-use-the-implements-in-excel-vba/19379641#19379641 The `Attribute` Lines are not meant to be written in the VBE, you have to do this externally in Notepad++ or anoother editor. – L8n Aug 10 '19 at 14:45
  • @L8n I have and I have gotten a single enum to work in each of the class modules that I needed it for. I'm currently attempting to enumerate two different collections in the same class module, not sure if this is even possible but if it is, I'd much rather that for my solution than having to go at it a different route – Glenn G Aug 13 '19 at 12:52
  • If you need two iterable collections inside a class, why don't you just pack two CustomCollections inside the class and expose them? If you do this via a Getter and no Setter you also prevent it from being overwritten. I'll also chek if two enumerables are possible, I' write again once I know more. – L8n Aug 13 '19 at 14:22
  • @L8n I'm thinking this will work and is what I'm going to try (enum & class modules are very new for me) `Private Type tSeqs SequenceNumbers As Collection RouterSequences As Collection End Type Private cc1 As tSeqs Public Property Get NewEnum() As IUnknown Set NewEnum = cc1.SequenceNumbers.[_NewEnum] End Property Public Property Get RouterList() As IUnknown Set RouterList = cc1.RouterSequences.[_NewEnum] End Property ` – Glenn G Aug 14 '19 at 17:55
  • @GlennG I did a quick test with a similiar Method and it did not work, I'm a bit strapped for time and will update my answer once I have more time available. – L8n Aug 14 '19 at 19:18