VBA - Query collection of custom classes - vba

Is there a way in VBA to query a collection of custom classes named People. Lets say I have a custom class that has First Name, Last Name, and Title.
‘private attributes
Private pFirstName as String
Private pLastName as String
Private pTitle as String
‘Get/Let Methods
Public Property Get FirstName() as String
FirstName = pFirstName
End Property
Public Property Let FirstName (Value as String)
pFirstName = Value
End Property
Public Property Get LastName() as String
LastName = pLastName
End Property
Public Property Let LastName(Value as String)
pLastName = Value
End Property
Public Property Get Title() as String
Title = pTitle
End Property
Public Property Title Let (Value as String)
pTitle = Value
End Property
I then, in my main SUB, create a collection of people. Is there a way to query that collection, Ie, return me all People with first name == Jack.
Thanks

You could go out of your way to implement something like this so you could do crazy stuff like:
Dim items As LinqEnumerable
Set items = LinqEnumerable.FromCollection(myCollection) _
.Where("x => x.FirstName = ""Jack""")
Dim p As Person '"People" is plural, you don't want a pluralized class name here.
For Each p In items
Debug.Print p.FirstName
Next
But that is very very much overkill, and inefficient. All you need is one loop, and a condition:
For Each p In myCollection
If p.FirstName = "Jack" Then
'we have a winner
End If
Next

Related

Distinguish between class members and arguments with same name in VBA?

Ist there a way to distinguish between class members and property/sub/function arguments with same name in VBA? For example:
Class1:
Private Name As String
Property Let LastName(Name As String)
this.Name = Name
End Property
Property Get LastName() As String
Dim Name As String
Name = "Mr. "
LastName = Name & this.Name
End Property
In other languages you can use the keyword this to reference to a class/instance(?) member. But how is it in VBA solved? I know you could use to different names. But that is not what I want.
Thanks for your help!
You can create a This with a Type like Mathieu teaches us in private-this-as-tsomething
Just create a Type with the vars and assign it to This.
Private Type TPerson
Name As String
End Type
Private This As TPerson
Property Let LastName(Name As String)
This.Name = Name
End Property
Property Get LastName() As String
Dim Name As String
Name = "Mr. "
LastName = Name & This.Name
End Property
Don't forget to read the other articles on RubberDuckVBA as they provide many great insights to VBA-OOP.
In VBA, keyword to use instead of this. is Me. However this throws a compile error if the variable referenced inside the class is private (I am not sure why). A possible workaround is to change it to public.
'''' Class1
Private PrName As String
Public PbName As String
Property Let LastName(Name As String)
Me.PrName = Name ' throws a compile error
Me.PbName = Name ' OK
End Property
UPDATE:
You could also create extra layer of wrapper functions / properties - set_name and getName. The Name variable then wouldn't require renaming or change of scope. Also the input parameters of the original properties are left intact.
Private Name As String
Property Let LastName(Name As String)
Call set_name(Name) ' throws a compile error
End Property
Private Sub set_name(new_name As String)
' wrapper
Name = new_name
End Sub
Property Get getName() As String
' wrapper
getName = Name
End Property
Property Get LastName() As String
Dim Name As String
Name = "Mr. "
LastName = Name & Me.getName
End Property

Get value of a property with propertyinfo object

Is there a way to get value of a object properties with a propertyinfo object?
psudo code:
propertyinfoObject = Text
myobject.toCommand(propertyinfoObject)
The psudo code above should do the same as
myobject.Text
My goal is to create a simpel Properties form that will work on any object (Later I will use keywords to filter out what options I want the use to see).
My real code
Public Class PropertiesForm
Dim propertyInfoVar() As PropertyInfo
Dim Properties As New Form2
Dim listItem As New ListViewItem
Dim stringarray() As String
Public Sub New(ByRef sender As Object)
propertyInfoVar = sender.GetType().GetProperties()
For Each p In propertyInfoVar
stringarray = {p.Name.ToString, #INSERT VALUE SOMEHOW HERE#}
listItem = New ListViewItem(stringarray)
Properties.ListView1.Items.Add(listItem)
Next
Properties.Visible = True
End Sub
EDIT
Just use propertyGrid as suggested below!
The standard PropertyGrid already does all that for you. Filtering properties is not so obvious, here's how:
The control includes a BrowsableAttributes property which allows you to specify that only properties with the specified attribute value should be shown. You can use existing attributes, or custom ones. This is specifically for tagging visible props:
<AttributeUsage(AttributeTargets.Property)>
Public Class PropertyGridBrowsableAttribute
Inherits Attribute
Public Property Browsable As Boolean
Public Sub New(b As Boolean)
Browsable = b
End Sub
End Class
Apply it to an Employee class to hide pay rates or anything else:
Public Class Employee
<PropertyGridBrowsable(True)>
Public Property FirstName As String
...
<PropertyGridBrowsable(False)>
Public Property PayRate As Decimal
<PropertyGridBrowsable(False)>
Public Property NationalInsuranceNumber As String
Test code:
Dim emp As New Employee With {.Dept = EmpDept.Manager,
.FirstName = "Ziggy",
.PayRate = 568.98D,
...
.NationalInsuranceNumber = "1234567"
}
propGrid.BrowsableAttributes = New AttributeCollection(New PropertyGridBrowsableAttribute(True))
propGrid.SelectedObject = emp
BrowsableAttributes is a collection, so you can add several.

Assign direct value to object

I have several properties, for example
Public Property FIRSTNAME As New SQLString("FirstName", 50)
Public Property FULLNAME As New SQLString("Name", 50)
The SQLString object is defined as:
Public Class SQLString
Property SQL_Column As String
Property Limit As Integer
Property Value As String
Public Sub New(SQLcolumn As String, limit_ As Integer)
SQL_Column = SQLcolumn
Limit = limit_
End Sub
Public ReadOnly Property SQL_value() As String
Get
Return "'" & clean(Value, Limit) & "'"
End Get
End Property
End Class
Notice that through this method, each of my properties (e.g. FIRSTNAME) is able to have several sub properties, which is necessary.
To access them, it's simply for example FIRSTNAME.SQL_Column.
This works, however what I would like is to also be able to store a value (e.g. string data type) on the FIRSTNAME property itself, which would make accessing it like:
Dim MyFirstName As String = FIRSTNAME
Rather than:
Dim MyFirstName As String = FIRSTNAME.Value
Which is what I currently have to do.
The only way I can see to do this is to have the SQLString object be set to string (or another data type) by default, like:
Public Class SQLString As String
Obviously the above code does not work, but I'm wondering if there is an equivalent that does?
The default access modifier to a property (ie: Public, Private, etc) is the most restrictive when no access modifier is provided. In SQLString class, since there is not a Public access modifier in front of the properties in the class, they are essentially Private and not accessible from outside of the class.
Adding the access modifier to the properties should fix the issue you see:
Public Property SQL_Column As String
Public Property Limit As Integer
Public Property Value As String
Please tell me the problem for the vote downs - here is a working .NET fiddle of the proposed code changes above (https://dotnetfiddle.net/96o8qm).
Imports System
Dim p as Person = new Person()
p.FIRSTNAME = new SQLString("Test", 1)
p.FIRSTNAME.Value = "Test Value"
Console.WriteLine("Person Value: {0}", p.FIRSTNAME.Value)
Public Class Person
Public Property FIRSTNAME AS SQLString
End Class
Public Class SQLString
Public Property SQL_Column As String
Public Property Limit As Integer
Public Property Value As String
Public Sub New(SQLcolumn As String, limit_ As Integer)
SQL_Column = SQLcolumn
Limit = limit_
End Sub
Public ReadOnly Property SQL_value() As String
Get
Return ""
End Get
End Property
End Class
This yields the output:
Person Value: Test Value
The answer to your question is quite simple; add a CType widening operator.
Example:
Public Class SQLString
Public Shared Widening Operator CType(ByVal s As SQLString) As String
Return If((s Is Nothing), Nothing, s.Value)
End Operator
Public Property Value As String
End Class
Test:
Dim firstName As New SQLString() With {.Value = "Bjørn"}
Dim myName As String = firstName
Debug.WriteLine(myName)
Output (immediate window):
Bjørn

Invalid Use of property vba class

I have the Student class in VBA (Excel) implemented as follows
Option Explicit
Private name_ As String
Private surname_ As String
Private marks_ As New Collection
Public Property Get getMean() As Single
Dim sum As Double
Dim mark As Double
Dim count As Integer
For Each mark In marks_
sum = sum + mark
count = count + 1
Next mark
getMean = sum / count
End Property
Public Property Let setName(name As String)
name_ = name
End Property
Public Property Get getName() As String
getName = name_
End Property
Public Property Let setSurname(surname As String)
surname_ = surname
End Property
Public Property Get getSurname() As String
getSurname = surname_
End Property
Then I have a main sub where I write:
Dim stud1 As New Student
stud1.setName "Andy"
I got a compile error on stud1.setName "Andy" : Invalid use of property.
I don't understand why. Any Idea, please?
Since it's a property (not method) you should use = to apply a value:
Dim stud1 As New Student
stud1.setName = "Andy"
BTW, for simplicity, you can use the same name for get and set properties:
Public Property Let Name(name As String)
name_ = name
End Property
Public Property Get Name() As String
Name = name_
End Property
and then use them as follows:
Dim stud1 As New Student
'set name
stud1.Name = "Andy"
'get name
MsgBox stud1.Name

Let property of VBA class modules - is it possible to have multiple arguments? [duplicate]

This question already has answers here:
How do optional Parameters in Let/Get Properties work?
(3 answers)
Closed 7 years ago.
My understanding of using the Let property in a class module so far is that you set it up in the class modules like this:
Dim pName as String
Public Property Let Name(Value As String)
pName = Value
End Property
And then you after you've created an object of this class you can set this property like so:
MyObject.Name = "Larry"
Question: Is it possible to somehow enter multiple arguments into a class property? For instance:
Dim pFirstName as String, pLastName as String
Public Property Let Name(FirstName As String, LastName As String)
pFirstName = FirstName
pLastName = LastName
End Property
How would you then go about setting this property outside the class?
MyObject.Name = ??
Or is this just plain not possible to do?
I realise there already are 2 workarounds but I thought answering your original question was worth giving a shot due to the amount of views this question is receiving.
The answer to
Is it possible to have multiple arguments in the Let Property in VBA?
is
YES! It's possible.
First let's talk about the GET property. Consider this being the Class1
Private firstName as String
Private lastName as String
Public Property Get Name() As String
Name = firstName & " " & lastName
End Property
The Name property will return the full name, that's great but how to use the Let property to assign firstName & lastName in one go?
Side note: You could pass a single string separated with a special character and split it inside the body of the Let and assign the first and last names but forget that, let's get it done properly...
OK, in VBA the default Let property for the current setup would take 1 parameter and assign it to either first or last name...
Something like:
Public Property Let Name(value As String)
firstName = value
End Property
Rule: the Get takes no parameters and the Let takes one. That is very logical because the Get returns the underlying value but the Let needs to grab a value from somewhere in order to assign it to the data it's representing. Worth remembering at this point that the Let property is assigned via the = sign ie. myObject.Name = "IdOnKnuu"
we know that if we stay consistent with the above rule, theoretically we should be able to add one parameter to the Get and one more to the Let.
Let's forget the Let - comment it out for now - and add a parameter to the Get property.
Private firstName As String
Private lastName As String
Public Property Get Name(first As String) As String
Name = firstName & " " & lastName
End Property
'Public Property Let Name(value As String)
' firstName = value
'End Property
Going back to the Module1 create an instance of Class1 and type c.Name(
oh, intelli-sense expecting something? Awesome!
At this point it's worth understanding that our Get property returns first + last which are both empty at the moment, so it doesn't matter what you are going to pass to the c.Name() it will return an empty string.
Ok, let's uncomment and tweak the Let property now...
Side node: if you jump back to Module1 and type c. and don't get intelli-sense it pretty much means that something in Class1 is broken... We already know - it's the Let property that's causing it
We have added a parameter to the Get property, let's do the same with the Let property...
Public Property Let Name(first As String, value As String)
firstName = value
End Property
Let's go back to the Module1 and try to use the Let property:
Remember, how you have used the Let property before? You needed to assign it a value
c.Name = "John"
But now, your Let property is expecting an extra parameter first as String.
Why don't we try this:
c.Name("John") = "Smith"
Hey! that compiles and runs (a quick F5)
Great, let's check the results!
Debug.print c.Name("John")
Uhm... that only shows Smith in the Immediate Window (Ctrl+G)... Not exactly what we were looking for....
Going back to the Let property we notice that we are grabbing 2 values via arguments but we only make use of one of them? We have the 2 different values coming in to the function, right? Let's treat the first one as the firstName and the second one as lastName
Public Property Let Name(first As String, last As String)
firstName = first
lastName = last
End Property
Going back to Module1
Sub Main()
Dim c As New Class1
c.Name("John") = "Smith"
Debug.Print c.Name("John")
End Sub
and re-running out current code gives us what we need ... it prints John Smith but wait!!! why do we have to pass the first name in order to retrieve the full name?
Ha! The trick to this is to make the first parameters of both properties Optional
Summarising, the code:
Class1.cls
Private firstName As String
Private lastName As String
Public Property Get Name(Optional first As String) As String
Name = firstName & " " & lastName
End Property
Public Property Let Name(Optional first As String, last As String)
firstName = first
lastName = last
End Property
Module1.bas
Sub Main()
Dim c As New Class1
c.Name("John") = "Smith"
Debug.Print c.Name ' prints John Smith
End Sub
So basically the assignment of two (or more) values through the Let property is possible in VBA. The thing that may throw you off a bit is the Syntax for it but I hope my explanation in this answer has helped you understand where things come from and why.
The Get property takes an optional parameter - it's really just a dummy parameter... it's not used anywhere within the Get property but it's allowing us to get the desired Let signature and allows us to pass two parameters to it. It also grants the easy to remember c.Name syntax.
The call
Debug.Print c.Name("first")
is still possible, however the "first" parameter just acts like a dummy and has no effect on the actual underlying data. It's a dummy and has no effect on the actual data - dump it and use:
Debug.print c.Name
definitely more convenient :)
As per your comment if you would prefer to encapsulate this logic then you can use something similar to the below.
Below includes the sub and function. The function returns a Person object:
Public Sub testing()
Dim t As Person
Set t = PersonData("John", "Smith")
End Sub
Public Function PersonData(firstName As String, lastName As String) As Person
Dim p As New Person
p.firstName = firstName
p.lastName = lastName
Set PersonData = p
End Function
Person Class:
Dim pFirstName As String, pLastName As String
Public Property Let FirstName(FirstName As String)
pFirstName = FirstName
End Property
Public Property Get FirstName() As String
FirstName = pFirstName
End Property
Public Property Let LastName(LastName As String)
pLastName = LastName
End Property
Public Property Get LastName() As String
LastName = pLastName
End Property
Use as Public Sub procedure within the class to perform this:
Public Sub testing()
Dim myPerson As New Person
myPerson.SetName "KaciRee", "Software"
End Sub
Person Class:
Dim pFirstName As String, pLastName As String
Public Property Let FirstName(FirstName As String)
pFirstName = FirstName
End Property
Public Property Get FirstName() As String
FirstName = pFirstName
End Property
Public Property Let LastName(LastName As String)
pLastName = LastName
End Property
Public Property Get LastName() As String
LastName = pLastName
End Property
Public Sub SetName(FirstName As String, LastName As String)
pFirstName = FirstName
pLastName = LastName
End Sub