How to create a compound object in VBA? - vba

I cannot make my way through the Microsoft help, which is great provided you know what the answer is already, so I'm stuck.
Is it possible for me to create my own compound object (I assume that this is the term) such that, for example, the object could be a person and would have the following sub-classes:
Firstname - String
Surname - String
Date of birth - Datetime
Gender - String (M/F accepted)
Height - Real number
Sorry if it seems like a very basic question (no pun intended) but I haven't used Visual Basic for a long time, and Microsoft Visual Basic was never my forté.

You should consider using class modules instead of types. Types are fine, but they're limited in what they can do. I usually end up converting my types to classes as soon as I need some more function than a type can provide.
You could create a CPerson class with the properties you want. Now if you want to return a FullName property, you can write a Property Get to return it - something you can't do with a type.
Private mlPersonID As Long
Private msFirstName As String
Private msSurname As String
Private mdtDOB As Date
Private msGender As String
Private mdHeight As Double
Private mlParentPtr As Long
Public Property Let PersonID(ByVal lPersonID As Long): mlPersonID = lPersonID: End Property
Public Property Get PersonID() As Long: PersonID = mlPersonID: End Property
Public Property Let FirstName(ByVal sFirstName As String): msFirstName = sFirstName: End Property
Public Property Get FirstName() As String: FirstName = msFirstName: End Property
Public Property Let Surname(ByVal sSurname As String): msSurname = sSurname: End Property
Public Property Get Surname() As String: Surname = msSurname: End Property
Public Property Let DOB(ByVal dtDOB As Date): mdtDOB = dtDOB: End Property
Public Property Get DOB() As Date: DOB = mdtDOB: End Property
Public Property Let Gender(ByVal sGender As String): msGender = sGender: End Property
Public Property Get Gender() As String: Gender = msGender: End Property
Public Property Let Height(ByVal dHeight As Double): mdHeight = dHeight: End Property
Public Property Get Height() As Double: Height = mdHeight: End Property
Public Property Get FullName() As String
FullName = Me.FirstName & Space(1) & Me.Surname
End Property
Then you can create a CPeople class to hold all of your CPerson instances.
Private mcolPeople As Collection
Private Sub Class_Initialize()
Set mcolPeople = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolPeople = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = mcolPeople.[_NewEnum]
End Property
Public Sub Add(clsPerson As CPerson)
If clsPerson.PersonID = 0 Then
clsPerson.PersonID = Me.Count + 1
End If
mcolPeople.Add clsPerson, CStr(clsPerson.PersonID)
End Sub
Public Property Get Person(vItem As Variant) As CPerson
Set Person = mcolPeople.Item(vItem)
End Property
Public Property Get Count() As Long
Count = mcolPeople.Count
End Property
Public Property Get FilterByGender(ByVal sGender As String) As CPeople
Dim clsReturn As CPeople
Dim clsPerson As CPerson
Set clsReturn = New CPeople
For Each clsPerson In Me
If clsPerson.Gender = sGender Then
clsReturn.Add clsPerson
End If
Next clsPerson
Set FilterByGender = clsReturn
End Property
With this class, you can For Each through all the instances (google custom class and NewEnum to see how to do that). You can also use a Property Get to return a subset of the CPerson instances (females in this case).
Now in a standard module, you can create a couple of CPerson instances, add them to your CPeople instance, filter them, and loop through them.
Public Sub FillPeople()
Dim clsPerson As CPerson
Dim clsPeople As CPeople
Dim clsFemales As CPeople
Set clsPeople = New CPeople
Set clsPerson = New CPerson
With clsPerson
.FirstName = "Joe"
.Surname = "Blow"
.Gender = "M"
.Height = 72
.DOB = #1/1/1980#
End With
clsPeople.Add clsPerson
Set clsPerson = New CPerson
With clsPerson
.FirstName = "Jane"
.Surname = "Doe"
.Gender = "F"
.Height = 62
.DOB = #1/1/1979#
End With
clsPeople.Add clsPerson
Set clsFemales = clsPeople.FilterByGender("F")
For Each clsPerson In clsFemales
Debug.Print clsPerson.FullName
Next clsPerson
End Sub
There's defintely more learning curve to creating classes, but it's worth it in my opinion.

I think you need to use TYPE syntax, like this:
TYPE person
Firstname As String
Surname As String
Date_of_birth As Date ' instead of Datetime
Gender As String '(M/F accepted)
Height As Single 'instead of Real number
END TYPE
Sub Test()
Dim aTest As person
End Sub

Related

How can I revert a property to its default value?

Public Property Name() As String = "default_name"
Sub InitializeFields()
Name = String.Empty
'
'
'
Name = Name.GetDefaultValue.ToString ' an example
End Sub
Programmatically, how can I revert the default value "default_name" of any property?
Here's an example of how you ought to do this with an actual default value:
Imports System.ComponentModel
Public Class Thing
<DefaultValue("Hello World")>
Public Property WithDefault As String
Public Property WithoutDefault As String
End Class
Imports System.ComponentModel
Imports System.Reflection
Module Module1
Sub Main()
Dim something As New Thing
something.WithDefault = "First"
something.WithoutDefault = "Second"
If TrySetDefaultValue(something, NameOf(something.WithDefault)) Then
Console.WriteLine($"{NameOf(something.WithDefault)} reset to ""{something.WithDefault}""")
Else
Console.WriteLine($"No default value for {NameOf(something.WithDefault)}")
End If
If TrySetDefaultValue(something, NameOf(something.WithoutDefault)) Then
Console.WriteLine($"{NameOf(something.WithoutDefault)} reset to ""{something.WithoutDefault}""")
Else
Console.WriteLine($"No default value for {NameOf(something.WithoutDefault)}")
End If
End Sub
Public Function TryGetDefaultValue([object] As Object, propertyName As String, ByRef value As Object) As Boolean
Dim attribute = [object].GetType().GetProperty(propertyName).GetCustomAttribute(Of DefaultValueAttribute)()
If attribute Is Nothing Then
Return False
End If
value = attribute.Value
Return True
End Function
Public Function TrySetDefaultValue([object] As Object, propertyName As String) As Boolean
Dim [property] = [object].GetType().GetProperty(propertyName)
Dim attribute = [property].GetCustomAttribute(Of DefaultValueAttribute)()
If attribute Is Nothing Then
Return False
End If
Dim value = attribute.Value
[property].SetValue([object], value)
Return True
End Function
End Module
There's no out-of-the-box way to somehow revert a property to its (custom) initial value. Once it's changed, it's over; the original value is lost.
Assuming the property belongs to a class (and that it doesn't get modified in the constructor), you could retrieve the value by creating a temporary instance of the class. For example:
Class SomeClass
Public Property Name As String = "default_name"
Sub InitializeFields()
Name = String.Empty
'
'
'
Name = New SomeClass().Name
End Sub
End Class
However, that's not very robust and could get ugly really fast. The way I would do this is by storing the default value in a constant:
Private Const DefaultName As String = "default_name"
Public Property Name As String = DefaultName
Sub InitializeFields()
Name = String.Empty
'
'
'
Name = DefaultName
End Sub
And then you can do that for each property that you need to later access its original value.
Answering myself:
Imports System.Reflection
Imports System.ComponentModel
Public Class clsThisClass
' each property have attribute of default value
<DefaultValue(0)>
Public Property RecordCount() As Long = 0
<DefaultValue("a")>
Public Property SQL() As String = String.Empty
<DefaultValue(0)>
Public Property IndexID() As Long = 0
<DefaultValue("b")>
Public Property Name() As String = String.Empty
<DefaultValue("c")>
Public Property Title() As String = String.Empty
<DefaultValue("d")>
Public Property Document_No() As String = String.Empty
<DefaultValue("abc")>
Public Property Company_Code() As String = String.Empty
Sub InitializeFields()
With Me
RecordCount = 28
IndexID = 10
Name = "name"
Title = "title"
Document_No = "doc_no"
Company_Code = "com_code"
Debug.WriteLine(RecordCount)
Debug.WriteLine(IndexID)
Debug.WriteLine(Name)
Debug.WriteLine(Document_No)
Debug.WriteLine(Company_Code)
' revert or reset all properties to default value upon initialization
Dim aType As Type = GetType(clsThisClass)
' each property of class
For Each pi As System.Reflection.PropertyInfo In aType.GetProperties()
' grab assigned default value of the property
Dim attribute = Me.GetType().GetProperty(pi.Name.ToString).GetCustomAttribute(Of DefaultValueAttribute)()
Try
Dim value As Object = Nothing
' convert value type according to the type of the property
value = CTypeDynamic(value, GetType(Attribute))
If Not attribute Is Nothing Then
value = attribute.Value
' set default value
pi.SetValue(Me, value)
End If
Catch ex As Exception
End Try
Next
Debug.WriteLine(RecordCount)
Debug.WriteLine(IndexID)
Debug.WriteLine(Name)
Debug.WriteLine(Document_No)
Debug.WriteLine(Company_Code)
End With
End Sub
End Class
Output:
28
10
name
doc_no
com_code
0
0
a
b
c
d
abc
initialized all properties with a default value at once.
in my scenario, I have numerous classes and have several properties.
by doing these few lines of code. I can copy-paste in each class at initializing method. it saved lots of time for writing code.

Casting from class to interface in Excel VBA

In Excel 2013, I have two classes: LoadCase and LoadCombination, which implement interface ILoadCase.
The declaration for ILoadCase is:
Option Explicit
'' Public properties
Public Property Get Name() As String
End Property
Public Property Let Name(ByVal value As String)
End Property
Public Property Get ID() As Long
End Property
Public Property Let ID(ByVal valus As Long)
End Property
And the (partial) implementations for both LoadCase and LoadCombination are:
Option Explicit
Implements ILoadCase
'' Public properties
Public Property Get ILoadCase_Name() As String
ILoadCase_Name = pName
End Property
Private Property Let ILoadCase_Name(ByVal value As String)
pName = value
End Property
Public Property Get ILoadCase_ID() As Long
ILoadCase_ID = pID
End Property
Private Property Let ILoadCase_ID(ByVal value As Long)
pID = value
End Property
I've omitted code which is irrelevant to the implementation of the interface.
I then have a class BeamForces, which contains results for a particular ILoadCase object:
Option Explicit
Public Fx As Double
Public Fy As Double
Public Fz As Double
Public Mx As Double
Public My As Double
Public Mz As Double
Public ParentLoadCase As ILoadCase
I thought that with this I'd be able to do something like this:
Set currentBeamForces = New BeamForces
With currentBeamForces
.Fx = forces(0)
.Fy = forces(1)
.Fz = forces(2)
.Mx = forces(3)
.My = forces(4)
.Mz = forces(5)
Set .ParentLoadCase = TargetLoadCase
End With
Where TargetLoadCase is either a LoadCase or a LoadCombination, but this gives me an error every time.
I've coded this like I would in .NET and just expected that it would work, but does casting to an interface not work in VBA? Or am I going wrong here somewhere?
EDIT
More details. I first call the following method:
Public Function LoadBeamForcesAtNode(ByVal TargetBeam As Beam, ByVal TargetNode As Node, Optional ByVal TargetLoadCases As Collection = Nothing) As Boolean
Dim i As Integer
Dim currentLoadCase As Variant
Dim targetBeamForces As BeamForces
If TargetLoadCases Is Nothing Then
For Each currentLoadCase In Me.LoadCases.Items
Call TargetLoadCases.Add(currentLoadCase)
Next
For Each currentLoadCase In Me.LoadCombinations.Items
Call TargetLoadCases.Add(currentLoadCase)
Next
End If
'On Error GoTo ExitPoint
For Each currentLoadCase In TargetLoadCases
Set targetBeamForces = InstantiateBeamForces(TargetBeam, TargetNode, currentLoadCase)
If TargetNode Is TargetBeam.Node1 Then
Set TargetBeam.Forces1 = targetBeamForces
Else
Set TargetBeam.Forces2 = targetBeamForces
End If
Next
LoadBeamForcesAtNode = True
ExitPoint:
End Function
Where TargetLoadCases is a collection which can contain both LoadCase and LoadCombination objects.
The problem occurs in InstantiateBeamForces, the code for which is
Private Function InstantiateBeamForces(ByVal TargetBeam As Beam, ByVal TargetNode As Node, ByVal TargetLoadCase As Variant) As BeamForces
Dim forces(5) As Double
Dim currentBeamForces As BeamForces
Call Me.output.GetMemberEndForces(TargetBeam.ID, IIf(TargetNode Is TargetBeam.Node1, 0, 1), TargetLoadCase.ILoadCase_ID, forces, 0)
Set currentBeamForces = New BeamForces
With currentBeamForces
.Fx = forces(0)
.Fy = forces(1)
.Fz = forces(2)
.Mx = forces(3)
.My = forces(4)
.Mz = forces(5)
Set .ParentLoadCase = TargetLoadCase
End With
Set InstantiateBeamForces = currentBeamForces
End Function
Which creates a new BeamForces object and populates it with the values returned by the ...GetMemberEndForces(...) API COM call.
The problem is that the .ParentLoadCase property is nothing after the assignment, so I'm assuming an invalid cast...
** EDIT 2 **
Here is a screenshot of TargetLoadCase when I put a breakpoint in InstantiateBeamForces.
The ILoadCase member is Nothing, but I don't get why. Could this be the cause of the problem?

Using Class Properties to return ODBC calls to another class

I have this class - CompLoc it returns two fields from a ODBC call.
I want to call the class like this.
dim myCompLoc as New CompLoc(company,location)
newCompany = mycompLoc.Company
newLocation = mycompLoc.Location
my Class.
Private _company As String
Public Property Company() As String
Get
Return _company
End Get
Set(ByVal value As String)
_company = value
End Set
End Property
Private _location As String
Public Property Location() As String
Get
Return _location
End Get
Set(ByVal value As String)
_location = value
End Set
End Property
Public Sub New(ByVal loc As String, ByVal comp As String)
'select company,location from mysqlTable Where location = loc and company = comp'
_company = 'field from ODBC'
_location = 'field from ODBC'
End Sub
I need to pass to the ODBC company and location, to return company and location (I know...not always in our hands). I have this class that I want to populate from the ODBC call.
corp.EmpId = currentRecord(0).ToString
corp.FirstName = currentRecord(1).ToString
corp.LastName = currentRecord(2).ToString
corp.BasePay = Decimal.Parse(currentRecord(3).ToString)
corp.corpCompany = currentRecord(4).ToString
corp.corpLocation = currentRecord(5).ToString
I need to add to the end of this class ---
corp.newCompany = myCompLoc.Company
corp.newLocation = myCompLoc.Location
Can I have some quidance to build my myCompLoc class properly so that I can call it from corp and receive both properties with values. I can pass this around as an array or other method, but i want to do it a better way.
Here is how I solved this. corpSQL inherits company and location from a properties class
corp inherits from corpMySql
Dim corporation as New Corporation
Dim corp As New corpMySql
Dim newcorp = corp.Getcorp(corp.Company, corp.Location)
corporation.Company = newcorp.NewCompany
corporation.Plant = newcorp.NewLocation
I hope this makes sense. I was able to do what I set out to acheive, but I think another way is more fluid. Maybe using inheritence?

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

Error says x is not a member of y, but it is

I have a sp that I added to my linq designer, which generated the result class:
Partial Public Class web_GetTweetsByUserIDResult
Private _userid As Integer
Private _tweetid As Integer
Private _TweeterFeed As String
Public Sub New()
MyBase.New
End Sub
<Global.System.Data.Linq.Mapping.ColumnAttribute(Storage:="_userid", DbType:="Int NOT NULL")> _
Public Property userid() As Integer
Get
Return Me._userid
End Get
Set
If ((Me._userid = value) _
= false) Then
Me._userid = value
End If
End Set
End Property
<Global.System.Data.Linq.Mapping.ColumnAttribute(Storage:="_tweetid", DbType:="Int NOT NULL")> _
Public Property tweetid() As Integer
Get
Return Me._tweetid
End Get
Set
If ((Me._tweetid = value) _
= false) Then
Me._tweetid = value
End If
End Set
End Property
<Global.System.Data.Linq.Mapping.ColumnAttribute(Storage:="_TweeterFeed", DbType:="NVarChar(100)")> _
Public Property TweeterFeed() As String
Get
Return Me._TweeterFeed
End Get
Set
If (String.Equals(Me._TweeterFeed, value) = false) Then
Me._TweeterFeed = value
End If
End Set
End Property
End Class
However, in this one section of code where I am trying to use the "TweeterFeed" member of the result class I am getting the error, "Error 4 'TweeterFeed' is not a member of 'System.Data.Linq.ISingleResult(Of web_GetTweetsByUserIDResult)'."
My code in this section is, :
<WebMethod()> _
Public Function GetTweetsByUserID(ByVal userID As Integer) As List(Of SimpleTweet)
Dim result As New List(Of SimpleTweet)
Dim urlTwitter As String = "https://api.twitter.com/1/statuses/user_timeline.xml?include_entities=true&include_rts=true&screen_name={0}&count=20"
Dim lq As New lqDFDataContext
Dim var = lq.web_GetTweetsByUserID(userID).ToList()
If Not var Is Nothing Then
For Each twitterfeed In var
Dim listURL As String = String.Format(urlTwitter, var.TweeterFeed)
Dim tweetXML As XmlDocument = utils.GetXMLForURL(listURL)
Dim tweetnodelist As XmlNodeList = tweetXML.ChildNodes(1).ChildNodes
For Each node As XmlNode In tweetnodelist
Dim tweet As New SimpleTweet
tweet.CreatedAt = node.SelectSingleNode("created_at").InnerText
tweet.HTMLText = utils.ReturnTextWithHRefLink(node.SelectSingleNode("text").InnerText)
tweet.ID = node.SelectSingleNode("id").InnerText
tweet.Name = node.SelectSingleNode("user/name").InnerText
tweet.ScreenName = node.SelectSingleNode("user/screen_name").InnerText
tweet.Text = node.SelectSingleNode("text").InnerText
tweet.UserID = node.SelectSingleNode("user/id").InnerText
tweet.ProfileImageURL = node.SelectSingleNode("user/profile_image_url_https").InnerText
result.Add(tweet)
Next
Next
End If
Return result
End Function
Does anyone have any idea what is going on? As far as I see "TweeterFeed" is clearly a member of the class, I can't figure out why I would be getting this error.
You're using var.TweeterFeed when you should be using twitterFeed.TweeterFeed. twitterFeed is a result extracted from var which is a sequence of results.
Using a more descriptive variable name than var would probably have made this clearer to you :)
I have this class
Public Class Tamano
Private pWidth As Integer
Private pHeight As Integer
Public Property Width As Integer
Public Property Height As Integer
End Class
I got the compilation error message 'Height' is not a member of 'Tamaño' in IIS
To fix it, add Set and Get to the properties and it compiles.
Public Class Tamano
Private pWidth As Integer
Private pHeight As Integer
Public Property Width As Integer
Get
Return pWidth
End Get
Set(value As Integer)
pWidth = value
End Set
End Property
Public Property Height As Integer
Get
Return pHeight
End Get
Set(value As Integer)
pHeight = value
End Set
End Property
End Class
This might not be directly related to your question but It might help someone else.