Using a Sub with Arguments in a class module [duplicate] - vba

This question already has answers here:
VBA does not accept my method calling and gives Compile error: Syntax error [duplicate]
(2 answers)
Closed 4 years ago.
I have a Sub in a regular Module where my code is mainly taking place. In a class module i have a sub, requiring Arguments which are not properties of the class module itself. Now i have trouble with getting the code started.
I made an example code with a minimum of lines, so you can see what i planned to do.
First the class module "clsCity":
Option Explicit
Public area As Single
'Public l As Single 'Working, but not desired solution:
'Public h As Single 'Working, but not desired solution:
Sub calcArea(length As Single, height As Single)
area = length * height
End Sub
Now the program itself:
Sub Country()
Dim BikiniBottom As New clsCity
Dim l As Single
Dim h As Single
Bikinibottom.calcArea(l,h) 'Error: "vba: Compile Error: expected: ="
'Working, but not desired solution:
'Bikinibottom.calcArea 'Where l and h are properties of clsCity
End Sub
As you can see, i plan to calculate the area of Bikinibottom with variables "l" and "h", which are not properties of clsCity. I get the error "vba: Compile Error: expected: =" when pressing enter. What do i do wrong? This is a sub and not a function, a "=" should not be necessary.
I know it would work to use the commented code. However, this is just a short example, please keep in mind that in my real code it is not a sophistic solution to make "l" and "h" a property of clsCity (they are properties of other class modules!)

Ok i managed to do it this way:
in class module:
Function calcArea(length As Single, height As Single)
calcArea = length * height
End Function
and in my main code:
BikiniBottom.area = BikiniBottom.calcArea(l, h)
So basicly my problem is solved.
Still curious how it is possible to use Subs with Arguments in a class module. Ideas?

I'd write this
Option Explicit
Sub Country()
Dim BikiniBottom As New clsCity
BikiniBottom.Length = 2
BikiniBottom.Height = 3
Debug.Print BikiniBottom.area
End Sub
and then for the class I'd use property procedures ...
Option Explicit
Private m_Length As Single
Private m_Height As Single
Public Property Let Length(rhs As Single)
m_Length = rhs
End Property
Public Property Get Length() As Single
Length = m_Length
End Property
Public Property Let Height(rhs As Single)
m_Height = rhs
End Property
Public Property Get Height() As Single
Height = m_Height
End Property
Public Function area() As Single
area = m_Length * m_Height
End Function

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

Constant With Dot Operator (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

VBA compile error: Method or data member not found

everyone. here's my code. When debugging it says "VBA compile error: Method or data member not found" and highlights line: Familienkutsche.strFarbe = "Blau"
If I outcomment it, it says the same thing about the line that follows. What does it not like? Everything is written in one block, so why doesn't he recognize either "strFarbe" or "Geschwindigkeit"? having said that, if I remove Familienkutsche and just leave .strFarbe = "Blau" everything works. Thank you in advance.
Option Explicit
Public strFarbe As String
Private bytTempo As Byte
Private blnTempoSperre As Boolean
Public Property Let Geschwindigkeit(Speed As Long)
If (Speed > 250) Then
bytTempo = 250
blnTempoSperre = True
Else
bytTempo = Speed
blnTempoSperre = False
End If
End Property
Public Property Get Geschwindigkeit() As Long
Geschwindigkeit = bytTempo
End Property
Public Property Get abgeriegelt() As Boolean
abgeriegelt = blnTempoSperre
End Property
Public Sub Autos()
Dim Familienkutsche As Auto
Let Familienkutsche = New Auto
Familienkutsche.strFarbe = "Blau"
Familienkutsche.Geschwindigkeit = 320
Debug.Print Familienkutsche.Geschwindigkeit
Debug.Print Familienkutsche.abgeriegelt
End Sub
The first part of your code must be in a class module Auto.
Public Sub Autos() must be in a standard module. Then it works (with changing Let to Set).
Output:
250
Wahr

VBA Object module must Implement ~?

I have created two classes, one being an interface for the other. Each time I try to instantiate Transition_Model I get:
Compile error: Object Module needs to implement '~' for interface'~'
To my understanding Implementing class is supposed to have a copy of all public subs, function, & properties. So I don't understant what is the problem here?
Have seen similar questions come up but either they refer to actual Sub or they include other complications making answer too complicated for me to understand.
Also note I tried changing Subs of Transition_Model to Private and add 'IModel_' in front of sub names(Just like top answer in second question I linked) but I still receive the same error.
IModel
Option Explicit
Public Enum Model_Types
Transition
Dummy
End Enum
Property Get M_Type() As Model_Types
End Property
Sub Run(Collat As Collateral)
End Sub
Sub Set_Params(key As String, value As Variant)
End Sub
Transition_Model
Option Explicit
Implements IModel
Private Transitions As Collection
Private Loan_States As Integer
Private Sub Class_Initialize()
Set Transitions = New Collection
End Sub
Public Property Get M_Type() As Model_Types
M_Type = Transition
End Property
Public Sub Run(Collat As Collateral)
Dim A_Transition As Transition
Dim New_Balance() As Double
Dim Row As Integer
For Row = 1 To UBound(Collat.Curr_Balance)
For Each A_Transition In Transitions
If A_Transition.Begining = i Then
New_Balance = New_Balance + Collat.Curr_Balance(Row) * A_Transition.Probability
End If
Next A_Transition
Next
End Sub
Public Sub Set_Params(key As String, value As Double)
Dim Split_key(1 To 2) As String
Dim New_Transition As Transition
Split_key = Split(key, "->")
Set New_Transition = New Transition
With New_Transition
.Begining = Split_key(1)
.Ending = Split_key(2)
.Probability = value
End With
Transitions.Add New_Transition, key
End Sub
Lastly the Sub I am using to test my class
Sub Transition_Model()
Dim Tested_Class As New Transition_Model
Dim Collat As New Collateral
'Test is the model type is correct
Debug.Assert Tested_Class.M_Type = Transition
'Test if Model without transition indeed does not affect balances of its collateral
Collat.Curr_Balance(1) = 0.5
Collat.Curr_Balance(2) = 0.5
Tested_Class.Run (Collat)
Debug.Assert ( _
Collat.Curr_Balance(1) = 0.5 And _
Collat.Curr_Balance(2) = 0.5)
End Sub
Actaully Per the second question I linked has the correct answer which I missed.
All subs need to start with 'IModel_' and rest ot the name has to match the name in IModel.
AND
This is the part i missed, you cannot use underscore in the Sub name.

How to assign a value to a variable of type Double, that has been passed as Object?

I am trying to assign a value to global variable, which has a Property of type Double. This Property is passed as Object and the assignment fails.
In the example code below, the value is never assigned to the actual object, but only locally:
Public Class Form1
Friend Home As New Building
Private Sub AssignValues() Handles Me.Load
'Objects of different types are added to a list
Dim listObjects As New List(Of Object)
listObjects.Add(Home.Surface)
'All the Objects in listObjects are assigned a value that
'is stored as String
For Each o As Object In listObjects
SetProperty(o, "45.6")
Debug.Print("Surface = " & Home.Surface.ToString)
Next
End Sub
Private Sub SetProperty(ByRef Variable As Object, ByVal Value As String)
Select Case Variable.GetType
Case GetType(Double)
Variable = CDbl(Value)
Case Else
'...
End Select
End Sub
End Class
Public Class Building
Dim _surface As Double = 0
Public Property Surface As Double
Get
Return _surface
End Get
Set(ByVal value As Double)
_surface = value
End Set
End Property
End Class
The program invariably outputs Surface = 0 instead of 45.6. What am I doing wrong?
I tried to pass the Variable as reference, as suggested here, but without success. I also read about using Reflection, but there ought to be something simpler than that...
When your adding home.surface to the list, your adding a copy of the double to the list and then adjusting that copy. Stick a watch on "o" and see how it changes whilst home.surface remains the same.
If you want to use reflection, try something along these lines.
Dim prop As Reflection.PropertyInfo = o.GetType().GetProperty("Surface")
prop.SetValue(o, 45.6)
With Variable.GetType you will get always Object, because this is the type of Variable. What you can do with an Object is converting/casting it into a different type (like Double).
The best way to determine the "original type" from where the Object comes would be including an additional variable telling it. Another option might be converting the given Object into the target Type and see if it is not nothing/does not trigger an error. But this second option is not too accurate, mainly when dealing with "equivalent types" like Doubles/Integers.