VBA List of Custom Datastructures - vba

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

Related

Reference to an attribute of a class by means of a string containing the name of the property

Let's take a very short example of a class like this:
Public Class The_Class1
Public Property ID As Integer
Public Property Property1_Integer As Integer
Public Property Property2_Single As Single
End Class
Somewhere else, I have a dictionary containing instances of The_Class1, like this:
Public Dictionary_Class1 As New Dictionary(Of Integer, The_Class1)
I want to perform an operation over Property1_Integer on all of the members inside Dictionary_Class1. Also, I want to perform the very same operation over Property2_Single, so I would like to create a function to perform such operation, and somehow instruct VB to use a given property on every call.
Can you think of an elegant way to do that?
Edit: Let's say, for example, that the operation that I want to perform is the sum of every Property1_Integer or Property2_Single of the members inside the dictionary. What I really really want to do is to determine if all of the values are the same, or if there is at least one that is different.
You can use Reflection, but it's not as clean as you may imagine. Here's some skeleton code you can adapt to your needs:
Public Class The_Class1
Public Property ID As Integer
Public Property Property1_Integer As Integer
Public Property Property2_Single As Single
End Class
Private Sub SetProperty1_Integer()
Dim myClassInstance As New The_Class1
Dim myType As Type = GetType(The_Class1)
Dim propertyInfo As System.Reflection.PropertyInfo = myType.GetProperty("Property1_Integer")
propertyInfo.SetValue(myClassInstance, 1)
MessageBox.Show(myClassInstance.Property1_Integer.ToString)
End Sub
Have fun!

In a generic VB.NET structure, how do I access its explicitly provided constructor?

First of all, what I want to achieve:
I want to extend a value datatype by providing additional properties, especially to validate ranges provided at declaration time. I want the new datatype to be a value type as well.
Compare with Ada:
subtype Day_Number is Integer range 1 .. 31;
Ideal, but obviously not implementable, would be:
Dim DayNumber As Int64 Range 1 To 31
However, I would be happy with:
Dim DayNumber As RangeInt64(1, 31)
It is of no concern, if initialization takes its time. Once ranges are provided, they are considered to be immutable. The datatype from then on is only used to set/get values like with ordinary value types, only that they are subject of being validated against the initially provided range.
My attempt:
Since I cannot inherit from structures in order to expand on them, I tried to incorporate a structure into a structure as a member.
In a module, I have this structure:
Friend Structure SRangeValueType(Of T)
Private lMinimum As T
Private lMaximum As T
Friend Property Minimum As T
Get
Return lMinimum
End Get
Set(tValue As T)
lMinimum = tValue
End Set
End Property
Friend Property Maximum As T
Get
Return lMaximum
End Get
Set(tValue As T)
lMaximum = tValue
End Set
End Property
Friend Sub New(Minimum As T, Maximum As T)
lMinimum = Minimum
lMaximum = Maximum
End Sub
End Structure
I attempt to use this generic structure as a member of another structure (of concrete type Int64):
Public Structure RangeInt64
Private Range As SRangeValueType(Of Int64)
End Structure
However, this is not using the constructor with the two arguments.
Say I want to initialize Range (the only member of the structure RangeInt64) with the values 100 and 200 for Minimum and Maximum, resp.
I am not allowed to use something like:
Private Range As SRangeValueType(Of Int64)(100,200)
What is the correct syntax to provide my values to the generic constructor?
Normally, if you add a constructor to a structure, you can call it using the New keyword:
Dim x As SRangeValueType(Of Int64) ' Calls the default, infered, parameter-less constructor
Dim y As New SRangeValueType(Of Int64)(100, 200) ' Calls the explicitly defined constructor
However, that's not really the problem. The problem is that you are trying to set the default value of a non-shared field in a structure. That is something which is never allowed. All non-shared fields in structures must default to their default value (i.e. Nothing). For instance:
Public Structure RangeInt64
Private x As Integer = 5 ' Error: Initializers on structure members are valid only for 'Shared' members and constants
Private y As New StringBuilder() ' Error: Non-shared members in a structure cannot be declared 'New'
End Structure
And, as you may already know, you cannot override the default, inferred, parameter-less constructor on a structure either:
Public Structure RangeInt64
Public Sub New() ' Error: Structures cannot declare a non-shared 'Sub New' with no parameters
x = 5
y = New StringBuilder()
End Sub
Private x As Integer
Private y As StringBuilder
End Structure
As such, you are stuck. By design, when the default constructor is used, all fields in the structure must always default to Nothing. However, if you really, really need it to be a structure, and you can't just convert it to a class, and you really need to change it's default value, you could theoretically fake it into working by using a property to wrap the field:
Public Structure RangeInt64
Private _Range As SRangeValueType(Of Int64)
Private _RangeInitialized As Boolean
Private Property Range As SRangeValueType(Of Int64)
Get
If Not _RangeInitialized Then
_Range = New SRangeValueType(Of Int64)(100, 200)
_RangeInitialized = True
End If
Return _Range
End Get
Set(value As SRangeValueType(Of Int64))
_Range = value
End Set
End Property
End Structure
It should go without saying, though, that it's pretty gross and should be avoided if possible.
Update
Now that you've provided more details about what you are trying to accomplish, I think I may have a better solution for you. You're right that structures do not support inheritance, but what they do support is interfaces. So, if all you need is a bunch of range types, one per value-type, all having the same minimum and maximum properties, but all returning different predetermined values, then you could do something like this:
Private Interface IRangeValueType(Of T)
ReadOnly Property Minimum As T
ReadOnly Property Maximum As T
End Interface
Private Structure RangeInt64
Implements IRangeValueType(Of Int64)
Public ReadOnly Property Minimum As Long Implements IRangeValueType(Of Int64).Minimum
Get
Return 100
End Get
End Property
Public ReadOnly Property Maximum As Long Implements IRangeValueType(Of Int64).Maximum
Get
Return 200
End Get
End Property
End Structure

Property Let creates an unwanted variable

I can't find anything on this anywhere so here's my problem (although it is mostly a cosmetic one):
I have a class being used as a custom data type, but when I look in the locals or watch window I see that each Property Let has created an extra variable, which clutters the window with redundant variables and information (and potentially takes up extra space).
Example:
In class module Class1:
Private data As Integer
Property Get X() As Integer
X = data
End Property
Property Let X(ByVal Value As Integer)
data = Value
End Property
And to test:
Sub Test1()
Dim TestClass As Class1
Set TestClass = New Class1
TestClass.X = 100
End Sub
In the locals window:
Am I supposed to be recycling this extra variable somehow or am I doing something else wrong?
--- If you look at stock Excel objects (like a Worksheet) there are no duplicate variables whatsoever.
Edit: To clarify, I want to know if there is a way to hide the Property in the locals/watch window to make them easier to navigate.
It's fine. data is your private variable and X is just property. Nothing wrong about that. But you should consider naming convention, setters and getters for private variable should be somehow consistent, for example:
'Member variable, pName - private Name
Private pName As String
'Properties
Property Get Name() As String
Name = pName
End Property
Property Let Name(val As String)
pName = val
End Property

Outputting multiple variables defined in a method

I have series of basic calculations on a form triggered by the form load event:
Dim someVariableA As Integer
Dim someVariableB As Integer
Dim someVariableX As Integer = 1
Dim someVariableY As Integer = 2
someVariableA = someVariableX + someVariableY
someVariableB = someVariableX * someVariableY
I now require the exact calculations for a separate form. Rather than pasting the same again, is there a means by which I can place the calculation in a method that both forms can call upon?
Public Function someFunction()
' Above calculations placed here instead.
End Function
Private Sub someSub()
' Call calculations.
someFunction()
' ...now output and use variables from function.
TextBox1.Text = someVariableA
TextBox2.Text = someVariableB
End Sub
Ultimately, I'm expecting something that behaves like PHP's include function.
You are running into the issue of Scope. Where a variable is declared determines its availability. You probably know how to make variables visible to all methods in a form:
Public Class Form1
Private varA As String
Private var2 As Integer
These will be available to all methods in the form because the are declared at the Form level (unlike a variable declared (Dim) inside a procedure which will exist only locally). To make them visible to all methods in the app's forms, declare them in a module:
Public Module1
Friend varA As String
Friend var2 As Integer
Friend varX As DateTime
Declared in a module (1980s style!), they become global variables for your app. But there is good reason to avoid this. It is so easy to change a value, you can have methods which accidentally or unwittingly do so - remember they are now visible to everything even those procedure which might have no good reason to change them! Then, you spend time trying to locate those methods which are changing the value(s) but should not be.
A gigantic benefit of OOP is the ability to avoid this by using classes to hold the data and contain methods to manage that data - they can do everything from loading and saving to the calcualations you need. A sign that this might be what you need is that you have some variables you want to be global and already have methods which are global, combine them and you have a class:
Public Class Foo
Private varA As String
Private var2 As Integer
' some of these things might be better as Properties
' this allows the subscribers (users of the class) to change the
' values directly:
Public Property SomeDate As DateTime
Public Property Name As String
Public Property Value As Integer
Public Function GetSomething(aVar As Integer) As Integer
var2 += aVar ' update var2 for example
Return var2 ' return new value
End Function
To make the class available to all forms:
Public Module1
Friend myFoo As Foo ' makes it visible to all forms
Then create an instance of your class from your main form:
Public Class Form1
Private Sub Form_Load(....
myFoo = new Foo
Now, myFoo is an instance of the Foo class which not only houses those variables, but the methods to manage them:
Private Sub button_click(....
someVar = myFoo.DoSomething(42)

How to instantiate Class object with varying number of property values

Been working a lot with custom classes lately and I love the power you can have with them but I have come across something that I'm not able to solve and/or find anything helpful online.
I have a list of a class with properties I'm looking to only store information pulled from a database into.
Public Class CustomClass
Public _Values As String
Public _Variables As String
Public ReadOnly Property Values() As String
Get
Return _Values
End Get
End Property
Public ReadOnly Property Variables() As String
Get
Return _Variables
End Get
End Property
Sub New(ByVal values As String, ByVal variables As String)
_Values = values
_Variables = variables
End Sub
End Class
I will be iterating through some database entries, and I'm looking to store them into the appropriate property when I hit them (since I won't have them all available immediately, which is part of my problem). I want to just be able to add either the value or the variable at a time and not both of them, but since I have the sub procedure 'New' passing two arguments, it will always require passing them both. I've found the only way around this is by making them optional fields which I don't feel is the right way to solve this. Is what I'm looking to do possible with a class or would it be simpler by using a structure?
You can overload the constructor:
Friend Class Foo
' using auto-implement props:
Public Property Name As String ' creates a _Name backing field
Public Property Value as Integer
Public Sub New(newN as String, newV as Integer)
' access "hidden" backing fields if you want:
_Name = newN
_Value = newV
End Sub
Public Sub New() ' simple ctor
End Sub
Public Sub New(justName As String)
' via the prop
Name = justName
End Sub
End Class
You now have 3 ways to create the object: with full initialization, partial (name only) or as a blank object. You will often need a "simple constructor" - one with no params - for other purposes: serializers, Collection editors and the like will have no idea how to use the parameterized constructors and will require a simple one.
If rules in the App were that there was no reason for a MyFoo to ever exist unless both Name and Value being defined, implementing only the New(String, Integer) ctor enforces that rule. That is, it is first about the app rules, then about coding convenience.
Dim myFoo As New Foo ' empty one
myFoo.Name = "ziggy" ' we only know part of it
Since the default of string is nothing, you could pass nothing for the value you don't have. IE
Collection.Add(New CustomClass("My Value",Nothing))
Every type has a default, so this works with more than just strings.