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

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

Related

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. :-)

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

VBA - Query collection of custom classes

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

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

Why does string.join return list object in VB.Net

I am having trouble understanding the difference between these two commands that in my mind should do the same thing. I have posted the entire code below in case anything is unclear.
I have created two functions in class Person, one that returns a list containing first,middle and last names and one that returns a concatenated string of the name. I reference the function that returns the list to concatenate the string with the line below:
FullName = String.Join(" ", Me.Get_NameList())
However, when I call:
Console.WriteLine(Person1.Print_Name())
I get what looks like the list object instead of the string:
System.Collections.Generic.List`1[System.String]
If I change the code to look like this:
Public Function Print_Name()
Dim FullNameList As List(Of String) = Me.Get_NameList()
Dim FullName As String
FullName = String.Join(" ", FullNameList)
Return FullName
End Function
The console prints:
John Q Doe
Why am I getting a different answer by first assigning the list to a variable and then joining it? Does this have something to do with how the list is stored in memory?
Thanks in advance for the help.
Here is the full code:
Imports System
Module Module1
Sub Main()
Dim Person1 As New Person("John", "Q", "Doe")
Console.WriteLine("Get_Name Values")
Dim g1 As List(Of String) = Person1.Get_NameList()
Console.WriteLine(String.Join(" ", g1))
Console.WriteLine("Print_Name Values")
Console.WriteLine(Person1.Print_Name())
End Sub
End Module
Class Person
Private FirstName As String
Private MiddleName As String
Private LastName As String
Public Sub New(ByVal Fn As String, ByVal Mn As String, ByVal Ln As String)
FirstName = Fn
MiddleName = Mn
LastName = Ln
End Sub
Public Function Get_NameList()
Dim NameList As New List(Of String)
NameList.Add(FirstName)
NameList.Add(MiddleName)
NameList.Add(LastName)
Return NameList
End Function
Public Function Print_Name()
'Dim FullNameList As List(Of String) = Me.Get_NameList()
Dim FullName As String
FullName = String.Join(" ", Me.Get_NameList())
Return FullName
End Function
End Class
GetNameList returns an Object (because you don't specify the return type).
So the Join method is getting an object. So the VB.Net is turning the Object into a String() with one element that is Object.ToString(). Sometimes the method, especially if it is an old school VB holdover, would check to see if the object passed was an IEnumerable and just iterate over the Objects in the passed object. But not always. So having Strict and Explicit OFF can lead to very strange and hard to find bugs. Those two things should only be OFF in very specific cases where you want all the flexibility turning them off gives you AND you are ready to deal with the oddities that result.
Change the return type of Get_NameList to List(Of String)
And turn on option Strict ON and Option Explicit On to see your other problems.
if you change this line:
Public Function Get_NameList()
to
Public Function Get_NameList() AS List(Of String)
And this line
Public Function Print_Name()
to
Public Function Print_Name() as string
it will work