Array as a class property - arrays

I need to pass an array as a property of a custom class, but cannot find a way to do it, and didn't find a question already answered that worked for me. So here I am.
My 'movie' class has 4 properties as you can see below: titre, annee, duree and genres. The first 3 are working well, but I'am blocked on the last one. From a string, separated by commas, I need to create an array of the different genres of the movie (ie: adventure, action, etc). I can easily create this array, but cannot find a way to assign it as a movie.property.
The code below returns an incompatibility error. I probably have to implement a let property but don't understand how to do it... Does anyone has a clue of what I am doing wrong ? Tell me if you need a bigger sample of the code.
Public Sub initialiser(ByVal strLigne As String)
Dim arrData
arrData = Split(strLigne, vbTab)
strtitre = arrData(0)
strAnnee = arrData(1)
intDuree = ConvertToInt(arrData(2))
genres = CreerTabGenre(arrData(3))
End Sub
Property Get genre() As eGenre ' eGenre is an enum, and I have no choice about that (homework...)
genre = genres() 'Here I tried all the combinations between parenthesis on both, on none, etc
End Property
edit: Maybe the problem is how I access the property after ? tabFilms is an array of all the different movies. As a test, I try to access the first genre of the array, but I'm met with 'incorrect affectation of property' error...
Function genrePopulaire(tabFilms() As film) As String
Dim i As Integer, j As Integer
For i = 0 To UBound(tabFilms)
MsgBox tabFilms(i).genre(0)
Next i
End Function

First of all, your split array uses vbTab to split the string. And you said that they are separeted by commas. If so, you should add a comma at the end of the string. It would look like:
Lord of the Rings; 2002; 200; SciFi;
and use the split array like this:
arrData = split(strLigne, ";")
Also, I've tried this code working with class module.
Try this, on the module procedure:
Sub Test()
Dim Movie As Object
Set Movie = New clsMovie
Dim arrData
'strligne example
Dim strligne As String
strligne = "TLOTR" & vbTab & "2002" & vbTab & 200 & vbTab & "SciFi"
arrData = Split(strligne, vbTab)
With Movie
.TestMovie arrData
'OUTPUT EXAMPLE:
Debug.Print "Movie's name is " & .Titre & ". La duree est " & .Duree & " minutes and it was realised on " & .Annee & " and its genre is " & .Genre
End With
'you can change value at any time
movie.titre = "Harry Potter"
debug.print movie.titre & " " & movie.duree
End Sub
On the class Module (IMPORTANT: called clsMovie):
Option Explicit
'Declare all variables as private. If you want, instead of "Private" you can do it "Public" but it can be quite dangerous on a huge project. As you are declaring them Private, it's necessary to work with Let / Get statements below.
Private pTitre As String, pAnnee As String, pDuree As String, pGenres As String
Function TestMovie(arr)
Titre = arr(0)
Annee = arr(1)
Duree = arr(2)
Genres = arr(3)
End Function
'From here to the end are the "Let / Get" properties as we work with private variables in the class
Property Get Titre() As String
Titre = pTitre
End Property
Private Property Let Titre(value As String)
pTitre = value
End Property
Property Get Annee() As String
Annee = pAnnee
End Property
Property Let Annee(value As String)
pAnnee = value
End Property
Property Get Duree() As String
Duree = pDuree
End Property
Property Let Duree(value As String)
pDuree = value
End Property
Property Get Genres() As String
Genres = pGenres
End Property
Property Let Genres(value As String)
pGenres = value
End Property

Related

Replace text in HTMLBody of an email template

I have been using the below code for years to generate communications. I am adapting it to new requirements.
The code grabs data from Sharepoint/MS Teams to filter then copy into a new tab. The code calls an email template that contains placeholders in multiple tables which includes a banner. By using strings it replaces the value of cells with the placeholder.
The data has bullet points and is in paragraphs. However when the email is generated, it has the data in one block as one continuous line.
I tried inserting line breaks but without success.
My latest iteration is to use a replace function, after the loop through the string arrays.
.HTMLBody = replace(.htmlBody, ";", "<BR>")
I put ";" at the end of a line when I want to go the next line.
However, whilst the <BR> does add the line break, it changes the font to Times New Roman and puts in a lot HTML garbage when the email is generated. I suspect is it is from the "<BR>".
The odd thing is when I add a debug.print onto .htmlbody it shows the font and line breaks are correct, with or without the second replace function.
I also tried to change "<BR>" with CHR(10) and vbnewline and other permutations.
I have not included the code that declares the outlook objects and the location of the email template as that works.
Sub ImportSPData() 'Source the Sharepoint data
Dim objMyList As ListObject
Dim objWksheet As Worksheet
Const strSPServer As String = "https://xxxx.xxxxx.xxx.com/teams/xxxx/_vti_bin" 'Sharepoint Url
Const LISTNAME As String = "{1574AC55-E21A-41D2-9EEC-891CFEC69BF6}" 'Sharepoint list code - where the data is inputted
Const VIEWNAME As String = "{34D4B58A-D4C6-4190-9248-896D062543C6}" 'Sharepoint View code - The specific view of the list
Set objWksheet = Worksheets("ImportData") 'Where the data is exported to
objWksheet.Select
If ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
objWksheet.Cells.Select
Selection.ClearContents
Range("A1").Select
Set objMyList = objWksheet.ListObjects.Add(xlSrcExternal, Array(strSPServer, LISTNAME, VIEWNAME), False, , Range("A1")) 'where the above export lands
Range("A1").Select
ActiveSheet.ListObjects(1).Unlist
If Not ActiveSheet.AutoFilterMode Then
ActiveSheet.Range("A1").AutoFilter
End If
Call applyAutoFilter 'sets up the stage. However when the "Import Data" tab is viewed, the line breaks and bullet points are missing.
End Sub
Sub Replace()
'Populate replacement strings from sharepoint. .Range("xx") corresponds to the column containing new text.
Dim repNumberText As String: repNumberText = dataSheet.Range("f2").Value
Dim repTitleText As String: repTitleText = dataSheet.Range("I2").Value
Dim repSummaryText As String: repSummaryText = dataSheet.Range("B2").Value
Dim repImpactText As String: repImpactText = dataSheet.Range("C2").Value
Dim repUnderwayText As String: repUnderwayText = dataSheet.Range("D2").Value
Dim repCompletedText As String: repCompletedText = dataSheet.Range("E2").Value
Dim repUpdateText As String: repUpdateText = dataSheet.Range("G2").Value
repSummaryText = "<p>" & repSummaryText & "</p>"
repCompletedText = "<p>" & repCompletedText & "</p>"
Dim replaceStrings() As Variant
Dim replaceWithStrings() As Variant
'Replacement Array, replaceStrings are the text placeholders in the email templates, replacewithstrings are the variables assigned above.
replaceStrings = Array("NumberText", "TitleText", "SummaryText", "ImpactText", "UnderwayText", "CompletedText", "UpdateText")
replaceWithStrings = Array(repNumberText, repTitleText, repSummaryText, repImpactText, repUnderwayText, repCompletedText, repUpdateText)
Dim currentItem As String
Dim currentReplaceItem As String
Dim i As Integer
i = UBound(replaceStrings)
Dim j As Integer
j = 0
With msgFile
Today = Format(Now(), "DDDD DD MMM yyyy")
'Dim HtmlBody As String
'Loop through arrays and replace text
Do Until j = i + 1
.HtmlBody = Replace(.HtmlBody, replaceStrings(j), replaceWithStrings(j))
j = j + 1
Loop
'Replace subject texts.\
' .Subject = "Communications"
' .Subject = Today
.Subject = Replace(.Subject, "NumberText", repNumberText)
.Subject = Replace(.Subject, "TitleText", repTitleText) & " " & "-" & " " & Today
.Display
It is not clear when and where all these customizations are made. Also it is not clear where the original item comes form. Anyway, keep in mind that you need to prepare a well-formed HTML document to be able to set up the HTMLBody property correctly. Even if Outlook handles cases where only some tags are provided, it is better to deal with a fully-formed HTML document.
The Outlook object model supports three main ways of customizing the message body:
The Body property returns or sets a string representing the clear-text body of the Outlook item.
The HTMLBody property of the MailItem class returns or sets a string representing the HTML body of the specified item. Setting the HTMLBody property will always update the Body property immediately. For example:
Sub CreateHTMLMail()
'Creates a new e-mail item and modifies its properties.
Dim objMail As Outlook.MailItem
'Create e-mail item
Set objMail = Application.CreateItem(olMailItem)
With objMail
'Set body format to HTML
.BodyFormat = olFormatHTML
.HTMLBody = "<HTML><BODY>Enter the message text here. </BODY></HTML>"
.Display
End With
End Sub
The Word object model can be used for dealing with message bodies. See Chapter 17: Working with Item Bodies for more information.
Note, the MailItem.BodyFormat property allows you to programmatically change the editor that is used for the body of an item.

Copying array into workbook which is part of a collection gives run time error 451 in VBA [duplicate]

I have a Client class. Inside that class there is an array losses. First I create and populate with clients a clientsColl array. Then for each client in that array I populate its losses array.
Then I try to print into debug a first element of losses for each client. However, it doesnt work and Property let procedure not defined and property get procedure did not return an object error appears.
And the same time if I just try to display a first element of losses for the first client, without any cycle, it works fine:
Dim clientsColl() As Client
clientsColl = getClients(dataWorkbook)
Dim clientCopy As Variant
Debug.Print "first: " & clientsColl(1).getLosses(1) 'works fine
For Each clientCopy In clientsColl
Debug.Print "in for each: " & clientCopy.getLosses(1) 'error here
Next
In Client class:
Public Property Get getLosses()
getLosses = losses
End Property
Private losses() As Double
How the losses array is populated:
Public Sub calculateFinancialResult()
ReDim losses(1 To simulationCount)
ReDim profits(1 To simulationCount)
Dim i As Long
For i = 1 To simulationCount
If outcomes(i) = 1 Then
losses(i) = totalLoss
...
Else
...
End If
Next
End Sub
Why does this happen and how to fix it?
EDIT: more of the main sub:
For Each clientCopy In clientsColl
clientCopy.setSimulationCount = globals("SIMULATION_COUNT")
...
clientCopy.calculateFinancialResult
...
Next
EDIT:
At the same time a simple for cycle works fine:
Debug.Print "first: " & clientsColl(1).getLosses(1)
For tempCount = LBound(clientsColl) To UBound(clientsColl)
Debug.Print "in for each: " & _
clientsColl(tempCount).getLosses(1)
Next
To conclude what was said in comments:
Your problem (error 451) often occures when you trying to compound properties.
To represent this case we can use any structure of any object with properties.
Let's emulate it with array of collections:
Option Explicit
Sub Test()
Dim Arr As Variant
Dim Col As Collection
Dim i As Long
Dim j As Long
ReDim Arr(1 To 10)
For i = 1 To 10
Set Col = New Collection
For j = 1 To 10
Call Col.Add(j)
Next
Set Arr(i) = Col
Next
On Error Resume Next
Debug.Print Arr(1).Item(1)
Debug.Print Arr(1).Item()(1)
On Error GoTo 0
End Sub
Your problem stems from the fact that you're treating your properties as attributes. On not-so-compounded (or when your array is declared explicitly as array of class instances) level it works due to early binding. But when things start to get more complex - it's fail, since your property just another function.
Hence, to achieve what you want, you should call it explicitly with another pair of parentheses.
Your getLosses property doesn't take an argument so your syntax is actually wrong, even though VBA can cope with it when early bound. You should be using:
Debug.Print "first: " & clientsColl(1).getLosses()(1) 'works fine
For Each clientCopy In clientsColl
Debug.Print "in for each: " & clientCopy.getLosses()(1) 'error here
Next
I also meet this problem when I create my customize array class using compound properties.
I solved it by adding class statment for return value in Property Get code. Just as what #Rory said.
You could try Public Property Get getLosses() As Double in the Client class.

Adding Item to Array (VB 2008)

The objective of the program is to interpret hockey statistics from a file using StreamReader and then display an added column of points. The following code kinda does so, however it’s ineffective in the sense that it doesn’t add the points value to the array - it separately outputs it. Looking for assistance as to how it would be possible to incorporate the points value into aryTextFile();
Dim hockeyFile, LineOfText, aryTextFile() As String
Dim i As Integer
Dim nameText(), NumberText(), goalsText(), assistsText(), GamesWonText() As String
Dim IntAssists(), IntGoals(), PointsText() As Single
hockeyFile = "C:\Users\Bob\Downloads\hockey.txt" 'state location of file
Dim objReader As New System.IO.StreamReader(hockeyFile) 'objReader can read hockeyFile
For i = 0 To objReader.Peek() <> -1 'reads each line seperately, ends when there is no more data to read
LineOfText = objReader.ReadLine 'stores seperate lines of data in HockeyFile into LineofText
aryTextFile = LineOfText.Split(",") 'takes lines and converts data into array
Name = aryTextFile(0) 'first piece of data in lines of text is the name
nameText(i) = aryTextFile(0)
If nameText(0) = "Name" Then
TextBox1.Text = LineOfText & ", Points." & vbCrLf 'displays first line fo text and adds "Points" label
End If
If Name <> "Name" Then 'when second line starts, then begin to intepret data
NumberText(i) = aryTextFile(1)
assistsText(i) = aryTextFile(2) 'assists are in third value of array
goalsText(i) = aryTextFile(3) 'goals are in fourth value of array
GamesWonText(i) = aryTextFile(4)
IntAssists(i) = Val(assistsText(i)) 'since our assists value is a string by default, it must be converted to a integer
IntGoals(i) = Val(goalsText(i)) 'since our goals value is a string by default, it must be converted to a integer
PointsText(i) = (IntGoals(i) * 2) + (IntAssists(i)) 'goals are two points, assists are one point
TextBox1.Text = TextBox1.Text & NumberText(i) & assistsText(i) & goalsText(i) & GamesWonText(i) & PointsText(i) & vbCrLf 'Displays points as last value in each line
End If
Next i
This should get you pretty close:
It'll need extra validation. It doesn't take into account whatever value you have between the name and the goals.
Private Sub ProcessHockeyStats()
Try
Dim inputFile As String = "c:\temp\hockey.txt"
Dim outputFile As String = "c:\temp\output.txt"
If Not File.Exists(inputFile) Then
MessageBox.Show("Missing input file")
Return
End If
If File.Exists(outputFile) Then
File.Delete(outputFile)
End If
Dim lines() As String = File.ReadAllLines(inputFile)
Dim output As List(Of String) = New List(Of String)
Dim firstLine As Boolean = True
For Each line As String In lines
Dim values() As String = line.Split(","c)
Dim points As Integer
If firstLine Then
output.Add("Name, Assists, Goals, Points")
firstLine = False
Else
'needs validation for values
points = CInt(values(1) * 2) + CInt(values(2))
output.Add(String.Concat(line, ",", points))
End If
Next
File.WriteAllLines("c:\temp\outfile.txt", output)
Catch ex As Exception
MessageBox.Show(String.Concat("Error occurred: ", ex.Message))
End Try
End Sub
VS2008 is ancient, especially when later versions of Visual Studio are free. I felt like showing an implementation using more-recent code. Like others, I strongly support building a class for this. The difference is my class is a little smarter, using the Factory pattern for creating instances and a Property to compute Points as needed:
Public Class HockeyPlayer
Public Property Name As String
Public Property Number As String
Public Property Assists As Integer
Public Property Goals As Integer
Public Property Wins As Integer
Public ReadOnly Property Points As Integer
Get
Return (Goals * 2) + Assists
End Get
End Property
Public Shared Function FromCSVLine(line As String) As HockeyPlayer
Dim parts() As String = line.Split(",")
Return New HockeyPlayer With {
.Name = parts(0),
.Number = parts(1),
.Assists = CInt(parts(2)),
.Goals = CInt(parts(3)),
.Wins = CInt(parts(4))
}
End Function
End Class
Dim hockeyFile As String = "C:\Users\Bob\Downloads\hockey.txt"
Dim players = File.ReadLines(hockeyFile).Skip(1).
Select(Function(line) HockeyPlayer.FromCSVLine(line)).
ToList() 'ToList() is optional, but I included it since you asked about an array
Dim result As New StringBuilder("Name, Number, Assists, Goals, Wins, Points")
For Each player In players
result.AppendLine($"{player.Name}, {player.Number}, {player.Assists}, {player.Goals}, {player.Wins}, {player.Points}")
Next player
TextBox1.Text = result.ToString()
I was gonna give you VS 2008 version afterward, but looking at this, the only thing here you couldn't do already even by VS 2010 was string interpolation... you really should upgrade.
Parallel arrays are really not the way to handle this. Create a class or structure to organize the data. Then create a list of the class. The list can be set as the DataSource of a DataGridView which will display your data in nice columns with headings matching the names of your properties in the Hockey class. You can easily order your data in the HockeyList by any of the properties of Hockey.
Public Class Hockey
Public Property Name As String
Public Property Number As String
Public Property Goals As Integer
Public Property Assists As Integer
Public Property Points As Integer
Public Property GamesWon As Integer
End Class
Private HockeyList As New List(Of Hockey)
Private Sub FillListAndDisplay()
Dim path = "C:\Users\Bob\Downloads\hockey.txt"
Dim Lines() = File.ReadAllLines(path)
For Each line As String In Lines
Dim arr() = line.Split(","c)
Dim h As New Hockey()
h.Name = arr(0)
h.Number = arr(1)
h.Assists = CInt(arr(2).Trim)
h.Goals = CInt(arr(3).Trim)
h.GamesWon = CInt(arr(4).Trim)
h.Points = h.Goals * 2 + h.Assists
HockeyList.Add(h)
Next
Dim orderedList = (From scorer In HockeyList Order By scorer.Points Ascending Select scorer).ToList
DataGridView1.DataSource = orderedList
End Sub

Excel vba: Property let procedure not defined and property get procedure did not return an object

I have a Client class. Inside that class there is an array losses. First I create and populate with clients a clientsColl array. Then for each client in that array I populate its losses array.
Then I try to print into debug a first element of losses for each client. However, it doesnt work and Property let procedure not defined and property get procedure did not return an object error appears.
And the same time if I just try to display a first element of losses for the first client, without any cycle, it works fine:
Dim clientsColl() As Client
clientsColl = getClients(dataWorkbook)
Dim clientCopy As Variant
Debug.Print "first: " & clientsColl(1).getLosses(1) 'works fine
For Each clientCopy In clientsColl
Debug.Print "in for each: " & clientCopy.getLosses(1) 'error here
Next
In Client class:
Public Property Get getLosses()
getLosses = losses
End Property
Private losses() As Double
How the losses array is populated:
Public Sub calculateFinancialResult()
ReDim losses(1 To simulationCount)
ReDim profits(1 To simulationCount)
Dim i As Long
For i = 1 To simulationCount
If outcomes(i) = 1 Then
losses(i) = totalLoss
...
Else
...
End If
Next
End Sub
Why does this happen and how to fix it?
EDIT: more of the main sub:
For Each clientCopy In clientsColl
clientCopy.setSimulationCount = globals("SIMULATION_COUNT")
...
clientCopy.calculateFinancialResult
...
Next
EDIT:
At the same time a simple for cycle works fine:
Debug.Print "first: " & clientsColl(1).getLosses(1)
For tempCount = LBound(clientsColl) To UBound(clientsColl)
Debug.Print "in for each: " & _
clientsColl(tempCount).getLosses(1)
Next
To conclude what was said in comments:
Your problem (error 451) often occures when you trying to compound properties.
To represent this case we can use any structure of any object with properties.
Let's emulate it with array of collections:
Option Explicit
Sub Test()
Dim Arr As Variant
Dim Col As Collection
Dim i As Long
Dim j As Long
ReDim Arr(1 To 10)
For i = 1 To 10
Set Col = New Collection
For j = 1 To 10
Call Col.Add(j)
Next
Set Arr(i) = Col
Next
On Error Resume Next
Debug.Print Arr(1).Item(1)
Debug.Print Arr(1).Item()(1)
On Error GoTo 0
End Sub
Your problem stems from the fact that you're treating your properties as attributes. On not-so-compounded (or when your array is declared explicitly as array of class instances) level it works due to early binding. But when things start to get more complex - it's fail, since your property just another function.
Hence, to achieve what you want, you should call it explicitly with another pair of parentheses.
Your getLosses property doesn't take an argument so your syntax is actually wrong, even though VBA can cope with it when early bound. You should be using:
Debug.Print "first: " & clientsColl(1).getLosses()(1) 'works fine
For Each clientCopy In clientsColl
Debug.Print "in for each: " & clientCopy.getLosses()(1) 'error here
Next
I also meet this problem when I create my customize array class using compound properties.
I solved it by adding class statment for return value in Property Get code. Just as what #Rory said.
You could try Public Property Get getLosses() As Double in the Client class.

Can I ReDim a module level array using a property?

I think I have a pretty good handle on how to handle module level arrays in VBA though Property Get and Let. Is there a way to ReDim a module level array through a property?
The following code errors out at the ReDim statement in the last procedure (DoTest).
Private mstrTestArray() As String
Private Sub Class_Initialize()
ReDim mstrTestArray(0) As String
End Sub
Private Property Get TestArray() As String()
TestArray = mstrTestArray
End Property
Private Property Let TestArray(ByRef strTestArray() As String)
mstrTestArray = strTestArray
End Property
Private Property Get TestArrayValue(d1 As Long) As String
TestArrayValue = mstrTestArray(d1)
End Property
Private Property Let TestArrayValue(d1 As Long, strValue As String)
mstrTestArray(d1) = strValue
End Property
Sub DoTest()
Dim intCharCode As Integer
For intCharCode = 97 To 122
If Not Len(TestArrayValue(UBound(TestArray))) > 0 Then
TestArrayValue(UBound(TestArray)) = Chr(intCharCode)
Else
ReDim Preserve TestArray(UBound(TestArray) + 1) As String
TestArrayValue(UBound(TestArray)) = Chr(intCharCode)
End If
Next intCharCode
Debug.Print TestArrayValue(LBound(TestArray)) _
& " through " _
& TestArrayValue(UBound(TestArray))
End Sub
Thanks!
This is a good question. I'll answer your question directly at the bottom, but let's start with a brief background of Object-Oriented Programming in VBA. In most object-oriented languages, a property will often look like a field, but act like a method. What does this mean?
When you instantiate a class and set a value to a property, it looks like this:
Sub TestMyClass()
Dim mc As MyClass
Set mc = new MyClass
mc.MyProperty = 1
End Sub
In the above code, MyProperty looks like a field, right? But let's look at how it's defined in the class:
Private pMyProperty As Integer
Public Property Get MyProperty() As Integer
MyProperty = pMyProperty
End Property
Public Property Let MyProperty(lMyProperty As Integer)
pMyProperty = lMyProperty
End Property
As you can see in the above code, while pMyProperty is an Integer field, the public Get and Set methods for MyProperty actually look more like methods. A property "wraps" around a field and is especially helpful in setting access to the underlying field.
In your example, you were trying to ReDim a property that returns a reference to the array. I'm not 100% sure, but I don't think you can use ReDim on a reference of an array.
I changed your code to modify the actual Private field mstrTestArray and it seemed to work fine. Is that something you can try?
Sub DoTest()
Dim intCharCode As Integer
For intCharCode = 97 To 122
If Not Len(TestArrayValue(UBound(TestArray))) > 0 Then
TestArrayValue(UBound(TestArray)) = Chr(intCharCode)
Else
ReDim Preserve mstrTestArray(UBound(mstrTestArray) + 1) As String
TestArrayValue(UBound(TestArray)) = Chr(intCharCode)
End If
Next intCharCode
Debug.Print TestArrayValue(LBound(TestArray)) _
& " through " _
& TestArrayValue(UBound(TestArray))
End Sub

Resources