VBA OOP How can I make subproperties - vba

How can I program my class module so that I can call properties on properties?
I'm not sure I'm using the right terminology, so I will try to clarify. In MsAccess whenever I want to manipulate elements on a form I can reference them using a period to separate each objects. For example, if I wanted to change the value of a text box I can call:
form("formname").txtboxname.value = "new value"
So it's like I have a form object that has a textbox object that has a value object.
How could I achieve this in my own class module.
My specific example is that I have stored an array in a private variable in the class however I cannot simply use a Property GET to return an array. (And I don't want to make it public because the array is programmatically populated) But if I want to iterate I need to know the Ubound and Lbound value of that array.
I would rather avoid having to store the Ubound and Lbound values in their own variable as that seems a waste.
How could I somehow program the class in order to get a ?subclass?
so that if I want the ubound or lbound I could call something like
set x = mycls
debug.? x.pArrayVariable.getLBound
Even the right terminology for what I'm trying to do could get me closer to an answer, I've tried searching for properties and sub properties but I'm not sure that's getting me somewhere.
Example of my class: mycls
Private pArrayVariable() as string
public property get pArrayVariable() as string
'Run Code to Populate array here
Array() = pArray()
end property
Is something called "Collections" what I'm asking about?

So a property can return an object (like a user class) which has its own properties. Example below:
Here is the code for a class called MinMax
Private m_min As Integer
Private m_max As Integer
Public Property Get MinValue() As Integer
MinValue = m_min
End Property
Public Property Let MinValue(ByVal x As Integer)
m_min = x
End Property
Public Property Get MaxValue() As Integer
MaxValue = m_max
End Property
Public Property Let MaxValue(ByVal x As Integer)
m_max = x
End Property
Public Sub SetMinMax(ByVal min_value As Integer, ByVal max_value As Integer)
m_min = min_value
m_max = max_value
End Sub
Private Sub Class_Initialize()
m_min = 0
m_max = 1
End Sub
and here is the code for a class named MyClass. Notice how it exposes a property of type MinMax
Private m_target As MinMax
Private m_name As String
Public Property Get Target() As MinMax
Target = m_target
End Property
Public Property Get Name() As String
Name = m_name
End Property
Private Sub Class_Initialize()
Set m_target = New MinMax
m_name = vbNullString
End Sub
Public Sub SetValues(ByVal a_name As String, ByVal min_value As Integer, ByVal max_value As Integer)
m_name = a_name
m_target.SetMinMax min_value, max_value
End Sub
Now the main code can have a statement like
Public Sub Test()
Dim t As New MyClass
t.SetValues "Mary", 1, 100
Debug.Print t.Target.MinValue, t.Target.MaxValue
End Sub

I still am curious about my original question above, however it came out of a problem of not being able to access the array. Seems I am incorrect.
You can use
Public Property Get ArrayVariable() As String()
Call 'code to populate array
ArrayVariable= pArrayVariable() 'Notice the paren here
End Property
And then to reference the array
debug.? ubound(clsvar.ArrayVariable()) 'Notice paren here too
or
debug.? clsvar.ArrayVariable()(1) 'Notice the parens here too

Related

How to save all class hardcoded values in one place

I have created a class in VBA which I would like to have some pre-set values associated with it. I am new to classes, and am wondering what is the best (/a good) way of structuring my VBA code within the class object, so that I can access these default values easily as I type. An answer should preferably:
Require relatively few additional lines of code over and above the lines which I assume will be required for the actual hard-coding of values
ie. something like an additional Sub for each hardcoded value would not be ideal
this is to prevent my class from becoming too cluttered
Allow me to use intellisense in some way to access these hard coded values
It's worth noting that my main use of these hard coded values is in setting default values to variables of my class (by looping in the initialize event), but I may also want to access them in other portions of code
What I've tried:
Declaring an Enum to save my hard coded values
'Declarations
Private Enum startingVals
Top = 10
Column_Count = 4
Left = 15
...
End Enum
Private topVal As Long 'variables which I assign default values to
Private colCnt As Long
Private leftVal As Long
Private Sub Class_Initialize()
topVal = startingVals.Top
colCnt = startingVals.Column_Count
'etc.
End Sub
This has 2 limitations;
Enums can only store Longs
Get around this by using a load of Consts instead, but then you have to remember every constant's name, plus it looks cluttered in code
Although I get Intellisense for .Top and .Column_Count, I still have to type startingVals out in full
That's significantly better than having to remember all the hardcoded constant names though
Ideally I would be able to do this
Private Sub Class_Initialize()
With startingVals 'or Dim v As startingVals, With v
topVal = .Top
colCnt = .Column_Count
'etc.
End With
End Sub
But I can't
Another approach would be to use a function to save the values,that way you could declare different types to just long.
'Declarations
Private Enum startingVals
Top = 1
Column_Count = 2
Left = 3
...
End Enum
Private topVal As Long 'variables which I assign default values to
Private colCnt As Long
Private leftVal As Long
Private Sub Class_Initialize()
topVal = getval(Top)
colCnt = getval(Column_Count)
'etc.
End Sub
Then to access the hard coded data, you have a function which takes an enum input (allowing for intellisense)
Private Function getval(dataType As startVals) As String
Const savedData As String = "1,2,1.17171717,hey,me,you" 'save the return values for the index specified by dataType
getval = Split(savedData, ",")(dataType) 'use datatype as a direct index of the array
End Function
or another way of saving the values
Private Function getval(dataType As startVals) As String
Const colV As Long = 10 'index 1
Const topV As String = "This is the top" 'index 2
'...
If dataType = ColumnCount Then getval = colV 'use dataType to check what to return
If dataType = Top Then getval = colV 'could use a select case too
'etc
End Function
But either way we still can't access the constants unless we type the function name out.
Also this approach requires me to update both the enum declaration at my class declarations portion, and the const declaration within the function itself, making the code harder to maintain.
TL;DR
What's the best method to save hardcoded values in a class object, where best is defined as
Uses VBA intellisense (autofill) so I can quickly select the value I want as I type
Is neat, self contained and concise within my class module, to avoid clutter
Can preferably hold any kind (data type) of hardcoded value (although I am only using Long in the project I'm currently working on)
Can be accessed without the need of typing an initialisation portion each time (such as a function or enum name)
Of course a With block or function equivalent would be fine, as that only requires the one instance of specifying the enum/data collection name
...to prevent my class from becoming too cluttered
I would separate the class from its initialization process adding another class lets call it Initializer. Initializer will know how to initialize my objects, will contain the default values and will fill my object with this defaults. But in the initializer you will have to write the assignments, no magic intellisense, but just simply write m_ and select from the list. HTH
Class Foo
Option Explicit
'variables which I assign default values to
Private m_topVal As Long
Private m_colCnt As Long
'Private m_leftVal As Long
Private Sub Class_Initialize()
Dim initializer As FooInitializer
Set initializer = New FooInitializer
initializer.Initialize Me
End Sub
Public Property Get TopVal() As Long
TopVal = m_topVal
End Property
Public Property Let TopVal(ByVal vNewValue As Long)
m_topVal = vNewValue
End Property
Public Property Get ColCnt() As Long
ColCnt = m_colCnt
End Property
Public Property Let ColCnt(ByVal vNewValue As Long)
m_colCnt = vNewValue
End Property
' Add Get/Let(Set) for other member variables as well
Class FooInitializer
Option Explicit
' Default startingVals values
Private m_topValDefault As Integer
Private m_columnCountDefault As Integer
'etc.
Public Sub Initialize(ByRef fooInstance As Foo)
fooInstance.TopVal = m_topValDefault
fooInstance.ColCnt = m_columnCountDefault
'etc.
End Sub
Private Sub Class_Initialize()
m_topValDefault = 10
m_columnCountDefault = 4
'etc.
End Sub
Standard module
Option Explicit
Sub test()
Dim f As Foo
Set f = New Foo
' f is now initizlized via initializer with default values
Debug.Print f.TopVal
Debug.Print f.ColCnt
End Sub
You can use constants to define the default values in a single place.
You can then easily access them with Ctrl + Space + Default...
Const Default_Top = 10
Const Default_Text = "abcd"
Private m_topVal As Long
Private m_text As String
Private Sub Class_Initialize()
m_topVal = Default_Top
m_text = Default_Text
End Sub
Public Property Get TopVal() As Long
TopVal = m_topVal
End Property
I can't claim ownership to this solution, but when I ran into it over on Code Review it was genius enough for me to have incorporated it into quite a lot of my code since.
As used in some other object-oriented languages, accessing class-internal instance variables using a this construct is very familiar. The concept is extended into VBA using the example here.
I've created a class module called CustomClass, and within it created a private custom type for use only within that class.
Option Explicit
Private Type CustomType
Top As Long
Name As String
Temperature As Double
anotherCustomObject As CustomClass
End Type
Private this As CustomType
Working this way, you can create any number of internal variables of any combination of types (including objects). Accessing and initializing each of these values is now as simple as using the this structured variable. The Class_Initialize sub shows how:
Private Sub Class_Initialize()
this.Top = 150
this.Name = "Wayne"
this.Temperature = 98.6
Set this.anotherCustomObject = New CustomClass
End Sub
Set and initialize all your values to your heart's content.
Further, you can establish each with property accessors if you like. Some of these can be Read Only:
'--- Read Only Properties
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Get Temperature() As Double
Temperature = this.Temperature
End Property
Public Property Get ContainedObject() As CustomClass
Set ContainedObject = this.anotherCustomObject
End Property
And you can create some that are Read/Write:
'--- Read/Write Properties
Public Property Let Top(ByVal newValue As Long)
this.Top = newValue
End Property
Public Property Get Top() As Long
Top = this.Top
End Property
Plus, you can still use the properties easily within the class using the Me keyword:
'--- Internal Private Methods
Private Sub TestThisClass()
Debug.Print "current temperature is " & Me.Temperature
Debug.Print "the Top value is " & Me.Top
End Sub
Of course, this all works when you declare an object of CustomClass in a different module as well.
Hopefully this helps goes a ways to helping regularize your code a bit.
(For convenience, here's the whole class:)
Option Explicit
Private Type CustomType
Top As Long
Name As String
Temperature As Double
anotherCustomObject As CustomClass
End Type
Private this As CustomType
Private Sub Class_Initialize()
this.Top = 150
this.Name = "Wayne"
this.Temperature = 98.6
Set this.anotherCustomObject = New CustomClass
End Sub
'--- Read Only Properties
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Get Temperature() As Double
Temperature = this.Temperature
End Property
Public Property Get ContainedObject() As CustomClass
Set ContainedObject = this.anotherCustomObject
End Property
'--- Read/Write Properties
Public Property Let Top(ByVal newValue As Long)
this.Top = newValue
End Property
Public Property Get Top() As Long
Top = this.Top
End Property
'--- Internal Private Methods
Private Sub TestThisClass()
Debug.Print "current temperature is " & Me.Temperature
Debug.Print "the Top value is " & Me.Top
End Sub

Detecting or preventing assignment operator to a class

Is there any way to make a class can be only initialized at declaration.
Public Class AnyValue
Private value As Int32
Public Sub New(ByVal aValue As Int32)
value = aValue
End Sub
End Class
'I want to be able to do this:
Dim val As New AnyValue(8)
'But not this.
val = New AnyValue(9)
Or it is possible to stop the assignment or detect when the operator = is used.
Lets just say this - No, you can't do what you want. The closest thing to it that I can think of, is to hide the constructor and give static access to the consumer as follows:
Public Class AnyValue
Private value As Int32
Private Sub New(ByVal aValue As Int32) ' Note - private constructor
value = aValue
End Sub
Public Shared Function Create(ByVal aValue As Int32) As AnyValue
Return New AnyValue(aValue)
End Function
End Class
'This will not work
Dim val As New AnyValue(8)
'This will not work
val = New AnyValue(9)
' This will work
Dim val As AnyValue = AnyValue.Create(8)
Now, if you look at this method of object creation, you can see that you can set all sort of rules for object construction. So, the client has very little input on the construction itself because how you construct the object is totally controlled by the object itself.

VB.Net check for duplicate items in a collection base

I have a class that inherits from CollectionBase. I tried to use the contains method to detect whether the Key already exists before inserting a new one. Here is what I have tried.
<Serializable()> Public Class validationList
Inherits CollectionBase
Public Function Add(ByVal Item As validationItem) As Integer
Return Me.List.Add(Item)
End Function
Default Public ReadOnly Property Item(ByVal index As Integer) As validationItem
Get
Return CType(List.Item(index), validationItem)
End Get
End Property
Public Sub Remove(ByVal index As Integer)
Me.List.RemoveAt(index)
End Sub
Protected Overrides Sub OnInsert(ByVal index As Integer, ByVal value As Object)
If Me.List.Contains(value) Then MsgBox("Already exist")
MyBase.OnInsert(index, value)
End Sub
Public Function IndexOf(ByVal key As validationItem)
Return List.IndexOf(key)
End Function
Public Sub AddRange(ByVal item() As validationItem)
For counter As Integer = 0 To item.GetLength(0) - 1
List.Add(item(counter))
Next
End Sub
End Class
<Serializable()> Public Class validationItem
Implements IEquatable(Of validationItem)
Private _key As validationTypes
Private _value As String
Public Sub New()
' Empty constructor is needed for serialization
End Sub
Public Sub New(ByVal k As validationTypes, ByVal v As String)
_key = k
_value = v
End Sub
Public Enum validationTypes
Madatory = 0
[Integer] = 1
Numeric = 2
[Decimal] = 3
MaxValue = 4
MinValue = 5
MinLength = 6
Email = 7
End Enum
Public Property Value As String
Get
Return _value
End Get
Set(ByVal Value As String)
_value = Value
End Set
End Property
Public Property Key As validationTypes
Get
Return _key
End Get
Set(ByVal value As validationTypes)
_key = value
End Set
End Property
Protected Overloads Function Equals(ByVal eqItem As validationItem) As Boolean Implements IEquatable(Of Testing_Project.validationItem).Equals
If eqItem Is Nothing Then Return False
Return Me._key = eqItem.Key
End Function
Public Overrides Function Equals(ByVal eqItem As Object) As Boolean
If eqItem Is Nothing Then Return False
Dim eqItemObj As validationItem = TryCast(eqItem, validationItem)
If eqItemObj Is Nothing Then Return False
Return Equals(eqItemObj)
End Function
Public Overrides Function GetHashCode() As Integer
Return Me._key.GetHashCode()
End Function
End Class
The validationList will be exposed from a usercontrol as a property, so that items could be added from the designer. When adding items I need to detect whether they already exist. I tried overriding the OnInsert but this sometime return that duplicates exists even when their aren't and doesn't report that duplicate exist when I try to add existing keys.
This indirectly answers the question after dealing with the issue which emerged in comments about Collection(Of T):
Add a reference to System.Collections.ObjectModel if needed, then
Imports System.Collections.ObjectModel
' a ValidationItem collection class
Public Class ValidationItems
Inherits Collection(Of ValidationItem)
Public Shadows Sub Add(NewItem As ValidationItem)
' test for existence
' do not add if it is not unique
Dim dupe As Boolean = False
For n As Int32 = 0 To Items.Count - 1
If Items(n).Key = NewItem.Key Then
dupe = True
Exit For
End If
Next
If dupe = False then
items.Add(newitem)
End if
' I would actually use an IndexOfKey function which might
' be useful elsewhere and only add if the return is -1
If IndexOfKey(NewItem.Key) <> -1 Then
Items.Add(newItem)
End If
End Sub
Some NET collection types implement Add as a function and return the item added. This sounds weird since you pass it the item to add. But returning Nothing if the item cannot be added is a neat semaphore for "I cant/wont do that". I cant recall if the std NET Collection Editor recognizes that or not.
One problem with using Contains is that it will test if item passed as param is the same object as one in the list. They never will be the same object, even if they have the same values. Testing the key in a loop is simpler than calling a method which implements an interface. (That previous answer was totally valid in the context presented, but the context has changed).
Even if you stay with CollectionBase, you want to handle it in the Add. If you try to remove it in OnInsert, VS will have problems deserializing the collection.
Also, your validationitem needs a Name property or the Collection Editor will display "Myassembly+MyType" as the Name (or a ToString override).
Other issues:
I am not sure your IndexOf will work. The list contains ValidationItems (objects), but you check it for _key (string). This will not matter if you change to Collection(Of T) which implements it for you.
The simple ctor is needed by the Collection Editor, not serialization. But the important thing is that it is there.
As for the comment about all Zeroes coming back - that is because your ValidationItem is not yet decorated for designer serialization. Maybe not the Collection Property either, that isnt shown.

List.add overwrites previus items in list VB.NET

Hi I have a little problem with a list of custom objects i wrote. When I use the list.add(xxx) method it doesn't simply append the xxx object to my list, but it turns each items to xxx and I don't know how to fix it.
Here is the declaration of my custom class:
Public Class User
Private Shared n As String
Public Shared Property Name() As String
Get
Return n
End Get
Set(ByVal value As String)
n = value
End Set
End Property
Sub New(ByVal name As String)
User.Name = name
End Sub
End Class
And here's where I call the list.add method
Public Class Form1
Private Sub subname
Dim temp As New User
Dim data As New List(Of User)
For Each item As String In ListBox1.Items
data.Add(New User(item))
Next
End Sub
End Sub
P.S. Yes, I've already read some posts about people having the same problem but did not understand how to apply their solution to my project.
Your problem is the Shared property and private member. A Shared member is shared among all instances of the class. If you set it in one instance it will be the same for all instances.
Private Shared n As String
Public Shared Property Name() As String
...
End Property
Remove the keyword Shared and it should work as expected.
Private n As String
Public Property Name() As String
...
End Property

Cast array in Object variable to type in System.Type variable

I have this function:
Public Sub DoStuff(ByVal type as System.Type, ByVal value as Object)
End Sub
The 'value' argument is always an array of the same type as 'type'. How can I loop through the values of the array?
I'd like to be able to do something like this:
DoStuff(GetType(Integer), New Integer(){1,2,3})
Public Sub DoStuff(ByVal type as System.Type, ByVal value as Object)
//Strongly types arr as Integer()
Dim arr = SomeCast(type, value)
For Each i in arr
//Do something with i
Next
End Sub
Edit Ok, I think I'll add more details so you can see what I'm trying to do. I have an object that captures values coming back from another page. Once I have them captured, I want to loop through the 'Values' property. So DoStuff() above would be called for each dictionary object in 'Values'. If the value in the dictionary objct is an array I want to loop through it as well. I was saving the type added through the generic function call as a System.Type, but maybe that's not the way to go. How can I write this so I can save the type of the array and loop through the array later?
Public Class PopUpReturnValues
Implements IPopUpReturnValues
Public Sub AddValue(Of T As Structure)(ByVal name As String, ByVal value As T) Implements IPopUpReturnValues.AddValue
_values.Add(name, New PopUpReturnValue() With {.UnderlyingType = GetType(T), .Value = value, .IsArray = False})
End Sub
Public Sub AddArray(Of T As Structure)(ByVal name As String, ByVal values As T()) Implements IPopUpReturnValues.AddArray
_values.Add(name, New PopUpReturnValue() With {.UnderlyingType = GetType(T), .Value = values, .IsArray = True})
End Sub
Public Sub AddStringValue(ByVal name As String, ByVal value As String) Implements IPopUpReturnValues.AddStringValue
_values.Add(name, New PopUpReturnValue() With {.UnderlyingType = GetType(String), .Value = value, .IsArray = False})
End Sub
Public Sub AddStringArray(ByVal name As String, ByVal values As String()) Implements IPopUpReturnValues.AddStringArray
_values.Add(name, New PopUpReturnValue() With {.UnderlyingType = GetType(String), .Value = values, .IsArray = True})
End Sub
Private _values As New Dictionary(Of String, PopUpReturnValue)
Public ReadOnly Property Values() As IDictionary(Of String, PopUpReturnValue)
Get
Return _values
End Get
End Property
Public Class PopUpReturnValue
Public UnderlyingType As Type
Public Value As Object
Public IsArray As Boolean
End Class
End Class
Comments moved to answers per OP
Your "do something" code in based on the type I assume, String vs Int vs Apple, it would need to handle all three types with an If statement. Just include an overload for those three types, you don't actually need to pass the type information. However, if its just calling ToString() then just use an Object array.
And if you don't like overloads, just use the TypeOf operator to inspect the values of the array. When you throw an Integer into an Object array, its still an Integer, just a boxed one.
Is the type known at compile time? If so, perhaps you could use Generics.
You can provide an Action, like this:
Public Sub DoStuff(ByVal value As Array, ByVal process As Action(Of Object) )
For Each item In value
process(item)
Next item
End Sub
Then you just need a method that takes one parameter for each of the types you care about and knows how to cast object to that type. Then call DoStuff() passing in the address of that method. You could even use a lambda if you wanted.