Constant With Dot Operator (VBA) - vba

I want to have a catalog of constant materials so I can use code that looks like the following:
Dim MyDensity, MySymbol
MyDensity = ALUMINUM.Density
MySymbol = ALUMINUM.Symbol
Obviously the density and symbol for aluminum are not expected to change so I want these to be constants but I like the dot notation for simplicity.
I see a few options but I don't like them.
Make constants for every property of every material. That seems like too many constants since I might have 20 materials each with 5 properties.
Const ALUMINUM_DENSITY As Float = 169.34
Const ALUMINUM_SYMBOL As String = "AL"
Define an enum with all the materials and make functions that return the properties. It's not as obvious that density is constant since its value is returned by a function.
Public Enum Material
MAT_ALUMINUM
MAT_COPPER
End Enum
Public Function GetDensity(Mat As Material)
Select Case Mat
Case MAT_ALUMINUM
GetDensity = 164.34
End Select
End Function
It doesn't seem like Const Structs or Const Objects going to solve this but maybe I'm wrong (they may not even be allowed). Is there a better way?

Make VBA's equivalent to a "static class". Regular modules can have properties, and nothing says that they can't be read-only. I'd also wrap the density and symbol up in a type:
'Materials.bas
Public Type Material
Density As Double
Symbol As String
End Type
Public Property Get Aluminum() As Material
Dim output As Material
output.Density = 169.34
output.Symbol = "AL"
Aluminum = output
End Property
Public Property Get Iron() As Material
'... etc
End Property
This gets pretty close to your desired usage semantics:
Private Sub Example()
Debug.Print Materials.Aluminum.Density
Debug.Print Materials.Aluminum.Symbol
End Sub
If you're in the same project, you can even drop the explicit Materials qualifier (although I'd recommend making it explicit):
Private Sub Example()
Debug.Print Aluminum.Density
Debug.Print Aluminum.Symbol
End Sub

IMO #Comintern hit the nail on the head; this answer is just another possible alternative.
Make an interface for it. Add a class module, call it IMaterial; that interface will formalize the get-only properties a Material needs:
Option Explicit
Public Property Get Symbol() As String
End Property
Public Property Get Density() As Single
End Property
Now bring up Notepad and paste this class header:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "StaticClass1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Save it as StaticClass1.cls and keep it in your "frequently needed VBA code files" folder (make one if you don't have one!).
Now add a prototype implementation to the text file:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Material"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Implements IMaterial
Private Const mSymbol As String = ""
Private Const mDensity As Single = 0
Private Property Get IMaterial_Symbol() As String
IMaterial_Symbol = Symbol
End Property
Private Property Get IMaterial_Density() As Single
IMaterial_Density = Density
End Property
Public Property Get Symbol() As String
Symbol = mSymbol
End Property
Public Property Get Density() As Single
Density = mDensity
End Property
Save that text file as Material.cls.
Now import this Material class into your project; rename it to AluminiumMaterial, and fill in the blanks:
Private Const mSymbol As String = "AL"
Private Const mDensity As Single = 169.34
Import the Material class again, rename it to AnotherMaterial, fill in the blanks:
Private Const mSymbol As String = "XYZ"
Private Const mDensity As Single = 123.45
Rinse & repeat for every material: you only need to supply each value once per material.
If you're using Rubberduck, add a folder annotation to the template file:
'#Folder("Materials")
And then the Code Explorer will cleanly regroup all the IMaterial classes under a Materials folder.
Having "many modules" is only a problem in VBA because the VBE's Project Explorer makes it rather inconvenient (by stuffing every single class under a single "classes" folder). Rubberduck's Code Explorer won't make VBA have namespaces, but lets you organize your VBA project in a structured way regardless.
Usage-wise, you can now have polymorphic code written against the IMaterial interface:
Public Sub DoSomething(ByVal material As IMaterial)
Debug.Print material.Symbol, material.Density
End Sub
Or you can access the get-only properties from the exposed default instance (that you get from the modules' VB_PredeclaredId = True attribute):
Public Sub DoSomething()
Debug.Print AluminumMaterial.Symbol, AluminumMaterial.Density
End Sub
And you can pass the default instances around into any method that needs to work with an IMaterial:
Public Sub DoSomething()
PrintToDebugPane AluminumMaterial
End Sub
Private Sub PrintToDebugPane(ByVal material As IMaterial)
Debug.Print material.Symbol, material.Density
End Sub
Upsides, you get compile-time validation for everything; the types are impossible to misuse.
Downsides, you need many modules (classes), and if the interface needs to change that makes a lot of classes to update to keep the code compilable.

You can create a Module called "ALUMINUM" and put the following inside it:
Public Const Density As Double = 169.34
Public Const Symbol As String = "AL"
Now in another module you can call into these like this:
Sub test()
Debug.Print ALUMINUM.Density
Debug.Print ALUMINUM.Symbol
End Sub

You could create a Class module -- let's call it Material, and define the properties a material has as public members (variables), like Density, Symbol:
Public Density As Float
Public Symbol As String
Then in a standard module create the materials:
Public Aluminium As New Material
Aluminium.Density = 169.34
Aluminium.Symbol = "AL"
Public Copper As New Material
' ... etc
Adding behaviour
The nice thing about classes is that you can define functions in it (methods) which you can also call with the dot notation on any instance. For example, if could define in the class:
Public Function AsString()
AsString = Symbol & "(" & Density & ")"
End Function
...then with your instance Aluminium (see earlier) you can do:
MsgBox Aluminium.AsString() ' => "AL(169.34)"
And whenever you have a new feature/behaviour to implement that must be available for all materials, you only have to implement it in the class.
Another example. Define in the class:
Public Function CalculateWeight(Volume As Float) As Float
CalculateWeight = Volume * Density
End Function
...and you can now do:
Weight = Aluminium.CalculateWeight(50.6)
Making the properties read-only
If you want to be sure that your code does not assign a new value to the Density and Symbol properties, then you need a bit more code. In the class you would define those properties with getters and setters (using Get and Set syntax). For example, Symbol would be defined as follows:
Private privSymbol as String
Property Get Symbol() As String
Symbol = privSymbol
End Property
Property Set Symbol(value As String)
If privSymbol = "" Then privSymbol = value
End Property
The above code will only allow to set the Symbol property if it is different from the empty string. Once set to "AL" it cannot be changed any more. You might even want to raise an error if such an attempt is made.

I like a hybrid approach. This is pseudo code because I don't quite have the time to fully work the example.
Create a MaterialsDataClass - see Mathieu Guindon's knowledge about setting this up as a static class
Private ArrayOfSymbols() as String
Private ArrayOfDensity() as Double
Private ArrayOfName() as String
' ....
ArrayOfSymbols = Split("H|He|AL|O|...","|")
ArrayOfDensity = '....
ArrayOfName = '....
Property Get GetMaterialBySymbol(value as Variant) as Material
Dim Index as Long
Dim NewMaterial as Material
'Find value in the Symbol array, get the Index
New Material = SetNewMaterial(ArrayOfSymbols(Index), ArrayofName(Index), ArrayofDensity(Index))
GetMaterialBySymbol = NewMaterial
End Property
Property Get GetMaterialByName(value as string) ' etc.
Material itself is similar to other answers. I have used a Type below, but I prefer Classes over Types because they allow more functionality, and they also can be used in 'For Each' loops.
Public Type Material
Density As Double
Symbol As String
Name as String
End Type
In your usage:
Public MaterialsData as New MaterialsDataClass
Dim MyMaterial as Material
Set MyMaterial = MaterialsDataClass.GetMaterialByName("Aluminium")
Debug.print MyMaterial.Density

Related

VBA: Class Module: Get and Let

I have no experience with custom classes and a really simple question, but I found it difficult to google this:
I've come across an example (source) for using custom classes.
Module 1
Sub clsRectAreaRun()
'This procedure instantiates an instance of a class, sets and calls class properties.
Dim a As Double
Dim b As Double
Dim areaRect As New clsRectArea
a = InputBox("Enter Length of rectangle")
b = InputBox("Enter Width of rectangle")
areaRect.Length = a
areaRect.Width = b
MsgBox areaRect.rArea
End Sub
class module 'clsRectArea'
'Example - Create Read-Only Class Property with only the PropertyGet_EndProperty block.
Private rectL As Double
Private rectW As Double
Public Property Let Length(l As Double)
rectL = l
End Property
Public Property Get Length() As Double
Length = rectL
End Property
Public Property Let Width(w As Double)
rectW = w
End Property
Public Property Get Width() As Double
Width = rectW
End Property
Public Property Get rArea() As Double
'Read-Only property with only the PropertyGet_EndProperty block and no PropertyLet_EndProperty (or PropertySet_EndProperty) block.
rArea = Length * Width
End Property
My question is regarding this part of the code:
areaRect.Length = a
areaRect.Width = b
MsgBox areaRect.rArea 'rArea = Length * Width
From what I've read, that Get and Let properties have the same name is kind of the point. But I have to ask, how does the code know if it's supposed to call Get or Let? Is it simply down to if, in this case, Length and Width are to the left or to the right of the equal sign? As in, when you want to assign a value to the property, it automatically recognizes it's Let and if it's on the right, like for rArea here, the code is supposed to retrieve the value, so it's Get?
I know, extremely basic, but I'm not 100% sure and I simply want to know if I'm not messing up the something basic.
You can convince yourself which property method is being called by adding MsgBox's to the code in the class module.
For example:
Public Property Let Length(l As Double)
rectL = l
MsgBox "Let Length called."
End Property

How do I apply method of the class to the property of the class?

I have a class ClsAnimal containing the string property species, and also method plural which just returns a string with added "s" at the end of a string. I wonder if it's possible to apply .Plural to Animal.Species directly, as shown in the example below:
Sub Test()
Dim Animal As New ClsAnimal
Animal.Species = "cat"
debug.print Animal.Species
'expected result "cat"
debug.print Animal.Species.Plural
'expected result "cats"
End Sub
ClsAnimal Code:
Option Explicit
Private PSpecies As String
Property Let Species(val As String)
PSpecies = val
End Property
Property Get Species() As String
Species = PSpecies
End Property
'returns the name of an animal + "s"
Private Function Plural(val) As String
Plural = val & "s"
End Function
You can kind of hack your way to the behavior you are describing. They way I could implement this is to create a new class that "extends" strings. I've called mine StringExt and it looks like this:
Option Explicit
Private pValue As String
'#DefaultMember
Public Property Get Value() As String
Value = pValue
End Property
Public Property Let Value(val As String)
pValue = val
End Property
Public Function Pluralize() As String
Dim suffix As String
'Examine last letter of the string value...
Select Case LCase(Right(pValue, 1))
Case "" 'empty string
suffix = ""
Case "s" 'words that end in s are pluralized by "es"
suffix = "es"
'Test for any other special cases you want...
Case Else ' default case
suffix = "s"
End Select
Pluralize = pValue & suffix
End Function
This is a wrapper class that wraps around an inner string value. It has a single method which will try to return the plural of the inner string value. One thing to note here is the use of a DefaultMember. I used a really handy vba editor COM addin called RubberDuck to do all the behind-the-scenes work for me with the Default Member. You can do it manually though. You would need to export the class module and modify it in a text editor, adding the Attribute Value.VB_UserMemId = 0 tag inside the property getter:
...
Public Property Get Value() As String
Attribute Value.VB_UserMemId = 0
Value = pValue
End Property
Public Property Let Value(val As String)
pValue = val
End Property
...
Then, import the module back into your vba project. This attribute is not visible in the vba editor. More on default members here but it basically means this property will be returned if no property is specified.
Next, we change up your animal class a bit, using our new StringExt type for the Species property:
Option Explicit
Private pSpecies As StringExt
Public Property Set Species(val As StringExt)
pSpecies = val
End Property
Public Property Get Species() As StringExt
Set Species = pSpecies
End Property
Private Sub Class_Initialize()
Set pSpecies = New StringExt
End Sub
Note here that you'll now need to make sure the pSpecies field gets instantiated since it is an object type now. I do this in the class Initializer to enure it always happens.
Now, your client code should work as expected.
Sub ClientCode()
Dim myAnimal As Animal
Set myAnimal = New Animal
myAnimal.Species = ""
Debug.Print myAnimal.Species.Pluralize
End Sub
Disclamer:
Substituting a basic string type for an object type might cause unexpected behavior in certain fringe situations. You are probably better off just using some global string helper method that takes a string parameter and returns the plural version. But, my implementation will get the behavior you asked for in this question. :-)

Type mismatch trying to set data in an object in a collection

I am getting Runtime Error 13 when trying to update an object stored in a collection. Here is a minimal example.
The class (Class2) of the objects to be stored in the collection.
Option Explicit
Private pHasA As Boolean
Private pHasB As Boolean
Private pSomeRandomID As String
Property Get HasA() As Boolean
HasA = pHasA
End Property
Property Get HasB() As Boolean
HasB = pHasB
End Property
Property Let HasA(propValue As Boolean)
pHasA = propValue
End Property
Property Let HasB(propValue As Boolean)
pHasB = propValue
End Property
Property Let RandomID(propValue As String)
pSomeRandomID = propValue
End Property
Sub SetHasValues(key As String)
Select Case key
Case "A"
pHasA = True
Case "B"
pHasB = True
End Select
End Sub
Minimal code that reproduces the error:
Option Explicit
Private Sub TestCollectionError()
Dim classArray As Variant
Dim classCollection As Collection
Dim singleClass2Item As Class2
Dim iterator As Long
classArray = Array("A", "B", "C")
Set classCollection = New Collection
For iterator = LBound(classArray) To UBound(classArray)
Set singleClass2Item = New Class2
singleClass2Item.RandomID = classArray(iterator)
classCollection.Add singleClass2Item, classArray(iterator)
Next iterator
Debug.Print "Count: " & classCollection.Count
singleClass2Item.SetHasValues "A" ' <-- This code works fine.
Debug.Print "New Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
For iterator = LBound(classArray) To UBound(classArray)
classCollection(classArray(iterator)).RandomID = classArray(iterator)
classCollection(classArray(iterator)).SetHasValues classArray(iterator) '<-- Type mismatch on this line.
Next iterator
'***** outputs
'''Count: 3
'''New Truth values: True False
' Error dialog as noted in the comment above
End Sub
While the code above appears a little contrived, it is based on some real code that I am using to automate Excel.
I have searched for answers here (including the following posts), but they do not address the simple and non-ambiguous example that I have here. The answers that I have found have addressed true type mismatches, wrong use of indexing or similar clear answers.
Retrieve items in collection (Excel, VBA)
Can't access object from collection
Nested collections, access elements type mismatch
This is caused by the fact, that the parameter of your procedure SetHasValues is implicitely defined ByRef.
Defining it ByVal will fix your problem.
#ADJ That's annoying, but perhaps the example below will allow you to start making a case for allowing RubberDuck.
I've upgraded your code using ideas and concepts I've gained from the rubberduck blogs. The code now compiles cleanly and is (imho) is less cluttered due to fewer lookups.
Key points to note are
Not relying on implicit type conversions
Assigning objects retrieved from collections to a variable of the type you are retrieving to get access to intellisense for the object
VBA objects with true constructors (the Create and Self functions in class2)
Encapsulation of the backing variables for class properties to give consistent (and simple) naming coupled with intellisense.
The code below does contain Rubberduck Annotations (comments starting '#)
Updated Class 2
Option Explicit
'#Folder("StackOverflowExamples")
'#PredeclaredId
Private Type Properties
HasA As Boolean
HasB As Boolean
SomeRandomID As String
End Type
Private p As Properties
Property Get HasA() As Boolean
HasA = p.HasA
End Property
Property Get HasB() As Boolean
HasB = p.HasB
End Property
Property Let HasA(propValue As Boolean)
p.HasA = propValue
End Property
Property Let HasB(propValue As Boolean)
p.HasB = propValue
End Property
Property Let RandomID(propValue As String)
p.SomeRandomID = propValue
End Property
Sub SetHasValues(key As String)
Select Case key
Case "A"
p.HasA = True
Case "B"
p.HasB = True
End Select
End Sub
Public Function Create(ByVal arg As String) As Class2
With New Class2
Set Create = .Self(arg)
End With
End Function
Public Function Self(ByVal arg As String) As Class2
p.SomeRandomID = arg
Set Self = Me
End Function
Updated test code
Private Sub TestCollectionError()
Dim classArray As Variant
Dim classCollection As Collection
Dim singleClass2Item As Class2
Dim my_item As Variant
Dim my_retrieved_item As Class2
classArray = Array("A", "B", "C")
Set classCollection = New Collection
For Each my_item In classArray
classCollection.Add Item:=Class2.Create(my_item), key:=my_item
Next
Debug.Print "Count: " & classCollection.Count
Set singleClass2Item = classCollection.Item(classCollection.Count)
Debug.Print "Initial Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
singleClass2Item.SetHasValues "A" ' <-- This code works fine.
Debug.Print "New Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
For Each my_item In classArray
Set my_retrieved_item = classCollection.Item(my_item)
my_retrieved_item.RandomID = CStr(my_item)
my_retrieved_item.SetHasValues CStr(my_item)
Next
End Sub
The 'Private Type Properties' idea comes from a Rubberduck article encapsulating class variable in a 'This' type. My take on this idea is to use two type variable p and s (Properties and State) where p holds the backing variables to properties and s hold variables which represent the internal state of the class. Its not been necessary to use the 'Private Type State' definition in the code above.
VBA classes with constructors relies on the PredeclaredID attribute being set to True. You can do this manually by removing and saving the code, using a text editor to set the attributer to 'True' and then reimporting. The RUbberDuck attribute '#PredeclaredId' allows this to be done automatically by the RubberDuck addin. IN my own code the initialiser for class2 would detect report an error as New should not be used when Classes are their own factories.
BY assigning and intermediate variable when retrieving an object from a class (or even a variant) you give Option Explicit the best change for letting you n=know of any errors.
An finally the Rubberduck Code Inspection shows there are still some issues which need attention

VBA List of Custom Datastructures

One of the main problems in VBA are custom data structures and lists.
I have a loop which generates with each iteration multiple values.
So as an example:
Each loop iteration generates a string "name" an integer "price" and an integer "value".
In C# for example I'd create a class which can hold these three values and with each loop iteration I add the class object to a list.
How can I do the same thing in VBA if I want to store multiple sets of data when not knowing how many iterations the loop will have (I cant create an array with a fixed size)
Any ideas?
The approach I use very frequently is to use a class and a collection. I also tend to use an interface model to make things more flexible. An example would look something like this:
Class Module IFoo
Option Explicit
Public Sub Create(ByVal Name as String, ByVal ID as String)
End Property
Public Property Get Name() as String
End Property
Public Property Get ID() as String
End Property
This enforces the pattern I want for my Foo class.
Class Module Foo
Option Explicit
Private Type TFoo
Name as String
ID as String
End Type
Private this as TFoo
Implements IFoo
Private Sub IFoo_Create(ByVal Name as String, ByVal ID as String)
this.Name = Name
this.ID = Name
End Sub
Private Property Get IFoo_Name() as String
IFoo_Name = this.Name
End Property
Private Property Get IFoo_ID() as String
IFoo_ID = this.ID
End Property
We get intellisense from the Private Type TFoo : Private this as TFoo where the former defines the properties of our container, the latter exposes them privately. The Implements IFoo allows us to selectively expose properties. This also allows you to iterate a Collection using an IFoo instead of a Foo. Sounds pointless until you have an Employee and a Manager where IFoo_BaseRate changes depending on employee type.
Then in practice, we have something like this:
Code Module Bar
Public Sub CollectFoo()
Dim AllTheFoos as Collection
Set AllTheFoos = New Collection
While SomeCondition
Dim Foo as IFoo
Set Foo = New Foo
Foo.Create(Name, ID)
AllTheFoos.Add Foo
Loop
For each Foo in AllTheFoos
Debug.Print Foo.Name, Foo.ID
Next
End Sub
While the pattern is super simple once you learn it, you'll find that it is incredibly powerful and scalable if implemented properly. It also can dramatically reduce the amount of copypasta that exists within your code (and thus reduce debug time).
You can use classes in VBA as well as in C#: Class Module Step by Step or A Quick Guide to the VBA Class Module
And to to the problem with the array: you can create an array with dynamic size like this
'Method 1 : Using Dim
Dim arr1() 'Without Size
'somewhere later -> increase a size to 1
redim arr1(UBound(arr1) + 1)
You could create a class - but if all you want to do is hold three bits of data together, I would define a Type structure. It needs to be defines at the top of an ordinary module, after option explicit and before any subs
Type MyType
Name As String
Price As Integer
Value As Integer
End Type
And then to use it
Sub test()
Dim t As MyType
t.Name = "fred"
t.Price = 12
t.Value = 3
End Sub

VBA 7.1 Setting multiple class properties by looping through a collection

self-taught VBA noob here. If I'm breaching ettiquette or asking something that everyone else knows already I'm sorry. Also, if I'm doing things that appear insane, it's because it is the only way I can either think of or actually make work. There's a department here at my work than can turn my makeshift code into something decent, but I have to give them a workable model first.
I have two programs with native VBA. One is a terminal emulator which I'm using to scrape mainframe data and to construct a custom class object, and then intend to pass it to MS Excel for number crunching. I am stuck with VBA until I can convince the IT folks that I am worthy of a Visual Studio license and scripting access. Also I have to pass the class in memory and not a spreadsheet in case of a program crash; no loose, easily recoverable data in lost files allowed.
The data is an invoice that has up to 99 lines, each line can bill an item or a service. The invoice is a custom invoice class, and each line is a custom line class contained in a collection of lines. I have everything built and working, but I'm stuck trying to set the line objects to their invoice line properties. Something with the effect of this:
For x = 1 To intLines
Invoice.Linex = cLines.Item(x)
Next x
hoping that in Excel I can use the invoice like this:
currTotalChrg = Invoice.Line01.Charge + Invoice.Line02.Charge
I've looked at the CallByName function but couldn't get it to work, and couldn't find an online example to show me how to set it up properly. Without that, I don't know how to make what I've seen others call a wrapper to construct and execute the lines. If I must, I can construct a SelectCasenstein to do the job, but there's got to be a better way. Since I can't post code (proprietary issues and government regulations), I am perfectly fine with vague answers; I can figure out the nuts and bolts if pointed in the right direction.
Thanx for the time and help!
Seems you want an Invoice collection class that holds InvoiceLineItem objects and exposes a TotalAmount property.
You can't edit module/member attributes directly in the VBE, but if you want to be able to iterate the line items of an invoice with a nice For Each loop, you'll have to find a way. One way is to export the class and edit it in your favorite text editor to add the attributes, save it, and then re-import it into your VBA project. Next release of Rubberduck will let you do that with "annotations" (magic comments), which I'm also including here:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Invoice"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Public Const MAX_LINE_ITEMS As Long = 99
Private Type TInvoice
InvoiceNumber As String
InvoiceDate As Date
'other members...
LineItems As Collection
End Type
Private this As TInvoice
Private Sub Class_Initialize()
this.LineItems = New Collection
End Sub
'#Description("Adds an InvoiceLineItem to this invoice. Raises an error if maximum capacity is reached.")
Public Sub AddLineItem(ByVal lineItem As InvoiceLineItem)
Attribute AddLineItem.VB_Description = "Adds an InvoiceLineItem to this invoice."
If this.LineItems.Count = MAX_LINE_ITEMS Then
Err.Raise 5, TypeName(Me), "This invoice already contains " & MAX_LINE_ITEMS & " items."
End If
this.LineItems.Add lineItem
End Sub
'#Description("Gets the line item at the specified index.")
'#DefaultMember
Public Property Get Item(ByVal index As Long) As InvoiceLineItem
Attribute Item.VB_Description = "Gets the line item at the specified index."
Attribute Item.VB_UserMemId = 0
Set Item = this.LineItems(index)
End Property
'#Description("Gets an enumerator that iterates through line items.")
'#Enumerator
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_Description = "Gets an enumerator that iterates through line items."
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = this.LineItems.[_NewEnum]
End Property
'...other members...
You could implement the sum outside the class, but IMO that would be feature envy; an invoice wants to be able to tell you its total amount & quantity.
So I would expose properties for that:
'#Description("Gets the total amount for all line items.")
Public Property Get TotalAmount() As Double
Dim result As Double
Dim lineItem As InvoiceLineItem
For Each lineItem In this.LineItems
result = result + lineItem.Amount
Next
TotalAmount = result
End Property
'#Description("Gets the total quantity for all line items.")
Public Property Get TotalQuantity() As Double
Dim result As Double
Dim lineItem As InvoiceLineItem
For Each lineItem In this.LineItems
result = result + lineItem.Quantity
Next
TotalQuantity = result
End Property
And then you might as well...
'#Description("Gets the total net amount for all line items (including taxes, discounts and surcharges).")
Public Property Get TotalNetAmount() As Double
TotalNetAmount = TotalAmount - TotalDiscounts + TotalSurcharges + TaxAmount
End Property
From your post and the nature of your question I suspect your class has.. what, 99 properties, one for each line on the invoice?
I am stuck with VBA until I can convince the IT folks that I am worthy of a Visual Studio license and scripting access.
VBA is just as object-oriented a language as any other "real" language you could use with Visual Studio. The above solution is fairly similar to how I would have implemented it in C#, or VB.NET. If your VBA class has a member for every single possible invoice line, your thinking is wrong - not the language you're using.
Stop hating VBA for the wrong reasons. The editor sucks, get over it.
I have a partial answer for you: not exactly what you asked for, but it shows you a syntax that can do it.
I have a 'totals' class - a simple wrapper for a dictionary - that allows you to specify named fields and start adding up values. It's trivial, and there isn't much to be gained by doing it... But bear with me:
Dim LoanTotals As clsTotals
Set LoanTotals = New clsTotals
For Each Field In LoanFileReader.Fields
LoanTotals.CreateField Field.Name
Next Field
For Each LineItem In LoanFileReader
LoanTotals.Add "LoanAmount", LineItem!LoanAmount
LoanTotals.Add "OutstandingBalance", LineItem!OutstandingBalance
LoanTotals.Add "Collateral", LineItem!Collateral
Next LineItem
The implementation details in the class aren't terribly interesting - you can work out that it all ends in Debug.Print LoanTotals.Total("LoanAmount")
...But what if I implemented this?
Dim LoanTotals As clsTotals
Set LoanTotals = New clsTotals
For Each Field In LoanFileReader.Fields
LoanTotals.CreateCommand Field.Name, Field.MainframeCommand
Next Field
...With an internal implementation like this:
Public Sub ExecuteCommand(CommandName, ParamArray() Args())
' Wrapper for objMainService, ends a command to the COM interface of the Mainframe client
CallByName objMainService, CommandName, vbMethod, Args
End Sub
Altenatively, you can concatenate Shell commands to execute those mainframe functions.
...And now you've populated a VB class that encapsulates a primitive API for a set of functions supplied at runtime.
As I say: it's not quite what you wanted, but it might get you somewhat closer to the solution you need.
For completeness, here's the code for the 'Totals' class:
A VBA Class for aggregating totals on named fields specified at runtime:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "clsTotals"
Attribute VB_Description = "Simple 'Totals' class based on a Scripting.Dictionary object"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
' Simple 'Totals' class based on a Scripting.Dictionary object
' Nigel Heffernan, Excellerando.Blogspot.com April 2009
' As it's based on a dictionary, the 'Add' and 'Reset' methods
' support implicit key creation: if you use a new name (or you
' mistype an existing name) a new Totals field will be created
' Coding Notes:
' This is a wrapper class: 'Implements' is not appropriate, as
' we are not reimplementing the class. Or not very much. Think
' of it as the 'syntactic sugar' alternative to prefixing all
' our method calls in the extended class with 'Dictionary_'.
Private m_dict As Scripting.Dictionary
Attribute m_dict.VB_MemberFlags = 40
Attribute m_dict.VB_VarDescription = "(Internal variable)"
Public Property Get Sum(FieldName As String) As Double
Attribute Sum.VB_Description = "Returns the current sum of the specified field."
Attribute Sum.VB_UserMemId = 0
' Returns the current sum of the specified field
Sum = m_dict(FieldName)
End Property
Public Sub CreateField(FieldName As String)
Attribute CreateField.VB_Description = "Explicitly create a new named field"
' Explicitly create a new named field
If m_dict.Exists(FieldName) Then
Err.Raise 1004, "Totals.CreateField", "There is already a field named '" & FieldName & "' in this 'Totals' object."
Else
m_dict.Add FieldName, 0#
End If
End Sub
Public Sub Add(FieldName As String, Value As Double)
Attribute Add.VB_Description = "Add a numeric amount to the field's running total \r\n Watch out for implicit field creation."
' Add a numeric amount to the field's running total
' Watch out for implicit field creation.
m_dict(FieldName) = m_dict(FieldName) + Value
End Sub
Public Sub Reset(FieldName As String)
Attribute FieldName.VB_Description = "Reset a named field's total to zero \r\n Watch out for implicit key creation"
' Reset a named field's total to zero
' Watch out for implicit key creation
m_dict(FieldName) = 0#
End Sub
Public Sub ResetAll()
Attribute ResetAll.VB_Description = "Clear all the totals"
' Clear all the totals
m_dict.RemoveAll
Set m_dict = Nothing
End Sub
Public Property Get Fields() As Variant
Attribute Fields.VB_Description = "Return a zero-based vector array of the field names"
'Return a zero-based vector array of the field names
Fields = m_dict.Keys
End Property
Public Property Get Values() As Variant
Attribute Values.VB_Description = "Return a zero-based vector array of the current totals"
'Return a zero-based vector array of the current totals
Fields = m_dict.Items
End Property
Public Property Get Count() As Long
Attribute Count.VB_Description = "Return the number of fields"
'Return the number of fields
Count= m_dict.Count
End Property
Public Property Get Exists(FieldName As String) As Boolean
Attribute Count.VB_Description = "Return a zero-based vector array of the field names"
'Return True if a named field exists in this instance of clsTotals
Exists = m_dict.Exists(FieldName)
End Property
Private Sub Class_Initialize()
Set m_dict = New Scripting.Dictionary
m_dict.CompareMode = TextCompare
End Sub
Private Sub Class_Terminate()
m_dict.RemoveAll
Set m_dict = Nothing
End Sub
Comment out the Attribute statements if you can't import them into your project.