Problem saving value to document, via custom class, in querysave - lotus-notes

I have written a small custom class to run an audit trail in Lotus Notes 8.5.2. I set the value of a NotesRichTextItem in my custom class and everything looks fine. When I drop out of my custom class, back into the Querysave and I check the Source.Document I can see the value fine. Once the querysave finishes (the line after my custom class call is End Sub) I check the document properties and the field is empty. I will include all code below, although the function called from my querysave is querySaveCheckValues (I pass in Source).
Custom class
Option Public
Option Declare
Public Class AuditTrail
REM boolean to audit all document items or use item list
Private includeAllItems As Boolean
Private currentDoc As NotesDocument
Private session As NotesSession
Private AUDIT_FIELD_LIST As String
Private AUDIT_FIELD As string
Private auditFieldList As NotesItem
Private postOpenValues List As String
Private auditField As NotesRichTextItem
Private MULTI_VALUE_SEPARATOR As String
'default message value insert strings
Private INSERT_FIELD_NAME As String
Private INSERT_OLD_VALUE As String
Private INSERT_NEW_VALUE As string
'message string defaults
Private DEFAULT_MESSAGE_CHANGE As String
'********** Sub new **********
Sub New(Source As NotesUIDocument)
dim currentDoc As NotesDocument
'put received uiDoc into NotesDocument
Set currentDoc = source.Document
REM set some class variables
setDefaultStrings
includeAllItems = True 'Details to all items on document
Set session = New NotesSession()
REM Check if the pre-defined audit field exists. If it doesn't we will audit all fields
If currentDoc.hasItem(AUDIT_FIELD_LIST) Then
'check if audit field list has at least one value
If UBound(currentDoc.GetItemValue(AUDIT_FIELD_LIST)) > 0 Then
includeAllItems = False
'assign field to NotesItem
Set auditFieldList = currentDoc.GetFirstItem(AUDIT_FIELD_LIST)
End If
End If
'get handle to audit field
If Source.Isnewdoc Then
Set auditField = New NotesRichTextItem(currentDoc, AUDIT_FIELD)
End If
Set auditField = currentDoc.GetFirstItem(AUDIT_FIELD)
End Sub
'********** collect values from current document **********
Function postOpenCollectValues(Source As NotesUIDocument)
Dim currentDoc As NotesDocument
Dim docItem As NotesItem
Dim fieldName As String
Dim fieldValue As String
Set currentDoc = Source.Document
If includeAllItems = False then
If Not auditFieldList Is Nothing Then
'list through values, find field and add to list
Dim i%
For i = 0 To UBound(auditFieldList.Values)
fieldName = auditFieldList.Values(i)
'look for item on document
If currentDoc.Hasitem(fieldName) Then
Set docItem = currentDoc.GetFirstItem(fieldName)
'check if item is multivalue
If UBound(docItem.Values) > 0 Then
fieldValue = Join(docItem.Values,MULTI_VALUE_SEPARATOR)
Else
fieldValue = docItem.Values(0)
End If
'convert value to string and put into list
postOpenValues(fieldName) = fieldValue
End If
Next
End If
End if
End Function
'********** Query save check to see if any values have changed **********
Function querySaveCheckValues(Source As NotesUIDocument)
Dim docItem As NotesItem
Dim fieldName As String
Dim oldValue, newValue As String
Set currentDoc = Source.Document
'Use list of fields generated during post open to save from etting errors when new fields
'are added to forms
ForAll x In postOpenValues
'eliminate mess if field has been removed from form
If currentDoc.hasItem(ListTag(x)) Then
Set docItem = currentDoc.GetFirstItem(ListTag(x))
fieldName = ListTag(x)
'compare old and new value
oldValue = x
If UBound(docItem.Values) > 0 Then
newValue = Join(docItem.Values,MULTI_VALUE_SEPARATOR)
Else
newValue = docItem.Values(0)
End If
Call me.compareValues(fieldName, CStr(oldValue), Newvalue)
End If
End ForAll
'make sure any changes added to audit field in backend and not overwriten
' Call Source.Refresh(true)
End Function
'********** Simple function to write lines to audit **********
Private Function writeAudit(message As String)
Dim tmpItem As NotesRichTextItem
Dim dateTime As New NotesDateTime(Now)
Dim nameItem As New NotesName(session.Username)
'take a copy of the current audit field content and blank audit
Set tmpItem = New NotesRichTextItem(currentDoc, "tmpAudit")
Call tmpItem.AppendRTItem(AuditField)
Call auditField.Remove()
'create a new audit field item and add new message
Set AuditField = New NotesRichTextItem(currentDoc, AUDIT_FIELD)
Call AuditField.AppendText(CStr(dateTime.LSLocalTime))
Call AuditField.AddTab(1)
Call AuditField.AppendText(nameItem.Abbreviated)
Call AuditField.AddTab(1)
Call AuditField.AppendText(message)
'append previous audit field content
Call AuditField.AppendRtItem(tmpItem)
Call tmpItem.remove()
End Function
'********** Function to compare single and multi values **********
Private Function compareValues(fieldName As String, oldValue As String, newValue As String)
Dim Message As String
'check for multi value
If InStr(oldValue,MULTI_VALUE_SEPARATOR) = 0 Then
'single value
If newValue <> oldValue Then
'prepare message
Message = prepareMessage(fieldName, oldValue, newValue, "CHANGE")
Call writeAudit(Message)
End If
End If
End Function
'********** Replace values in default message with old and new values **********
Private Function prepareMessage(fieldName As String, oldValue As String, newValue As String, messageType As String) As string
Dim tmpMessage As String
'case statement for type
Select Case messageType
Case "CHANGE"
tmpMessage = DEFAULT_MESSAGE_CHANGE
'replace default insert text with real field name
tmpMessage = Replace(tmpMessage,INSERT_FIELD_NAME,fieldName)
'old value
tmpMessage = Replace(tmpMessage,INSERT_OLD_VALUE,oldValue)
'new value
tmpMessage = Replace(tmpMessage,INSERT_NEW_VALUE,newValue)
End Select
prepareMessage = tmpMessage
Exit function
End Function
'********** Little function to setup our equivelant of constants **********
Private Function setDefaultStrings
AUDIT_FIELD_LIST = "auditFieldList" 'default audit field list name
AUDIT_FIELD = "AuditField" 'field used to store audit
MULTI_VALUE_SEPARATOR = "~" 'Used to combine and split values in a multi value item
'Default message insert strings
INSERT_FIELD_NAME = "%FIELDNAME%"
INSERT_OLD_VALUE = "%OLDVALUE%"
INSERT_NEW_VALUE = "%NEWVALUE%"
'Messages Strings
DEFAULT_MESSAGE_CHANGE = "Value of field '" & INSERT_FIELD_NAME & _
"' amended from '" & INSERT_OLD_VALUE & "' to '" & INSERT_NEW_VALUE & "'"
End Function
'********** handle error messages generated by this code **********
Private Function handleErrors
const DEFAULT_ERROR_MESSAGE = "Unable to write audit information - an error occured"
'if we have a handle on the audit field write an entry
If Not auditField Is Nothing Then
writeAudit(DEFAULT_ERROR_MESSAGE)
End If
End Function
End Class

I think your code would work if you move the call to your class to the PostSave event instead of QuerySave.
I'm basing that on the fact that you're altering the back-end document within the QuerySave event, and after that event runs it should overwrite the back-end document with the new values from the front-end. Just a hunch, though, as I haven't confirmed this is the case.

Related

Creating Class properties with sub levels

I've been reading this topic on how to use class modules.
My goal is to improve my code performance and readability so I think I'm in the right path.
But I have some questions about the limitations.
In my head i want to do this:
Is it possible to achieve such a structure?
The topic I've read has very few examples and this is not handled. I'm assuming this would be possible with collections of collections, but I not sure how to look for this.
My data comes from 2 tables, one has all the items but the department and the other one has the ID's alongisde the departments. Both tables have the dates of the current month as headers and their Schedule/Department depending on the table.
I'd know how to achieve this for one day, but not for a whole month.
This is how I wrote the basics for my class:
Option Explicit
Private DirNeg As String
Private Agrup As String
Private DNI As String
Private Centro As String
Private Servicio As String
Private Nombre As String
Property Get Business() As String
Business = DirNeg
End Property
Property Let Business(ByVal sBusiness As String)
DirNeg = sBusiness
End Property
Property Get Group() As String
Group = Agrup
End Property
Property Let Group(ByVal sGroup As String)
Agrup = sGroup
End Property
Property Get ID() As String
ID = DNI
End Property
Property Let ID(ByVal sID As String)
DNI = sID
End Property
Property Get Location() As String
Location = Centro
End Property
Property Let Location(ByVal sLocation As String)
Centro = sLocation
End Property
Property Get Service() As String
Service = Servicio
End Property
Property Let Service(ByVal sService As String)
Servicio = sService
End Property
Property Get Name() As String
Name = Nombre
End Property
Property Let Name(ByVal sName As String)
Nombre = sName
End Property
On the other hand, is it correct to fill the whole class on the Class_Initializeevent? My data will always be the same so I don't need to loop in a normal module to fill the class, it could be done everytime the class is created.
EDIT/UPDATE:
This is how my data looks like:
Schedules alongside Agent's info
Departments alongside Agent's ID
clAgent Class Module:
Option Explicit
Private DirNeg As String
Private Agrup As String
Private DNI As String
Private Centro As String
Private Servicio As String
Private Nombre As String
Private Fechas As Object
Property Get Business() As String
Business = DirNeg
End Property
Property Let Business(ByVal sBusiness As String)
DirNeg = sBusiness
End Property
Property Get Group() As String
Group = Agrup
End Property
Property Let Group(ByVal sGroup As String)
Agrup = sGroup
End Property
Property Get ID() As String
ID = DNI
End Property
Property Let ID(ByVal sID As String)
DNI = sID
End Property
Property Get Location() As String
Location = Centro
End Property
Property Let Location(ByVal sLocation As String)
Centro = sLocation
End Property
Property Get Service() As String
Service = Servicio
End Property
Property Let Service(ByVal sService As String)
Servicio = sService
End Property
Property Get Name() As String
Name = Nombre
End Property
Property Let Name(ByVal sName As String)
Nombre = sName
End Property
Property Get clFechas(ByVal StringKey As String) As clFechas
With Fechas
If Not .Exists(StringKey) Then
Dim objFechas As New clFechas
.Add StringKey, objFechas
End If
End With
End Property
Private Sub Class_Initialize()
Set Fechas = CreateObject("Scripting.Dictionary")
End Sub
clFechas Class Module:
Option Explicit
Private Modos As Object
Private Horarios As Object
'Aqiço creamos la propiedad Modo para la clase Fecha
Public Property Get Modo(ByVal StringKey As String) As String
Modo = Modos(StringKey)
End Property
Public Property Let Modo(ByVal StringKey As String, ByVal StringValue As String)
Modos(StringKey) = StringValue
End Property
Public Property Get Keys() As Variant
Keys = Modos.Keys
End Property
'Aquí creamos la propiedad Horario para la clase Fecha
Public Property Get Horario(ByVal StringKey As String) As String
Modo = Horarios(StringKey)
End Property
Public Property Let Horario(ByVal StringKey As String, ByVal StringValue As String)
Horarios(StringKey) = StringValue
End Property
Public Property Get Keys() As Variant
Keys = Horarios.Keys
End Property
'Iniciamos la clase
Private Sub Class_Initialize()
Set Modos = CreateObject("Scripting.Dictionary")
Set Horarios = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set Modos = Nothing
Set Horarios = Nothing
End Sub
You don’t seem to have any issues with regular properties so let’s focus on the complex ones; Schedule and Department. Both are the same, so same rules apply to both.
The property is basically list, the date is the index and the item is an object. I personally prefer to work with dictionaries as I can look if a key exist etc.
So, your Agent class could look something like this:
Option Explicit
Private m_schedules As Object
Public Property Get Schedule(ByVal Key As Date) As Schedules
With m_schedules
If Not .Exists(Key) Then .Add Key, New Schedules
End With
Set Schedule = m_schedules(Key)
End Property
'For testing purposes - can be ommited.
Public Property Get Keys() As Variant
Keys = m_schedules.Keys
End Property
'For testing purposes - can be ommited.
Public Property Get Count() As Long
Count = m_schedules.Count
End Property
Private Sub Class_Initialize()
Set m_schedules = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set m_schedules = Nothing
End Sub
The Schedules class:
Option Explicit
Private m_schedule As String
Public Property Get Schedule() As String
Schedule = m_schedule
End Property
Public Property Let Schedule(ByVal param As String)
m_schedule = param
End Property
Now, let's test it:
Sub Test()
Dim obj As Agent
Set obj = New Agent
obj.Schedule(#1/9/2019#).Schedule = "Schedule 1"
obj.Schedule(#2/9/2019#).Schedule = "Schedule 2"
obj.Schedule(#3/9/2019#).Schedule = "Schedule 3"
PrintToDebug obj
'Lets make a change
obj.Schedule(#2/9/2019#).Schedule = "Schedule 2222"
PrintToDebug obj
End Sub
Private Sub PrintToDebug(ByVal obj As Agent)
Debug.Print ""
Dim m As Variant
With obj
For Each m In .Keys
Debug.Print "Key: " & m & String(3, " ") & "Value: " & .Schedule(m).Schedule
Next m
End With
Debug.Print "Total Items: " & obj.Count
End Sub
Output:
'Key: 09/01/2019 Value: Schedule 1
'Key: 09/02/2019 Value: Schedule 2
'Key: 09/03/2019 Value: Schedule 3
'Total Items: 3
'Key: 09/01/2019 Value: Schedule 1
'Key: 09/02/2019 Value: Schedule 2222
'Key: 09/03/2019 Value: Schedule 3
'Total Items: 3
Additional information regarding the Dictionary object can be found here: Dictionary object
Also keep this in mind. It's quite important:
If key is not found when changing an item, a new key is created with
the specified newitem. If key is not found when attempting to return
an existing item, a new key is created and its corresponding item is
left empty.
If the dictionary item is not a simple string, let me know to update the answer. Sorry, I couldnt read the data in the screenshots. :)

Object Properties at Runtime

I want to write to custom class properties dynamically. In my use case, I have a table with column headers. The headers are properties of an Issue class. There are over 120 columns per issue. The end user chooses which columns they want included in the report. How do I set the properties of an object when the columns are not known until runtime? I couldn't find anything on Google that helped.
EDITED for clarity
Here is a snippet of my CIssue class:
Option Explicit
Private pIncidentNumber As String
Private pIncidentType As String
Private pContent As String
Private pStartDate As Date
Private pEndDate As Date
Public Property Let IncidentNumber(Value As String)
pIncidentNumber = Value
End Property
Public Property Get IncidentNumber() As String
IncidentNumber = pIncidentNumber
End Property
Public Property Let IncidentType(Value As String)
pIncidentType = Value
End Property
Public Property Get IncidentType() As String
IncidentType = pIncidentType
End Property
Public Property Let Content(Value As String)
pContent = Value
End Property
Public Property Get Content() As String
Content = pContent
End Property
Public Property Let StartDate(Value As Date)
pStartDate = Value
End Property
Public Property Get StartDate() As Date
StartDate = pStartDate
End Property
Public Property Let EndDate(Value As Date)
pEndDate = Value
End Property
Public Property Get EndDate() As Date
EndDate = pEndDate
End Property
It does nothing but help organize my code. I will build a collection class for this, also. If the end user chooses Incident Number and Content columns I want to set the appropriate properties. There could be up to 1,000 rows of data. So I need to set the properties for the rows that fit the criteria.
Example
I might have 72 rows that fit the criteria. Therefore, I need to add to my collection 72 objects of type CIssue with the correct properties set according to the columns the end user chose.
Thanks!
The core problem:
Create only properties in CIssue objects that are selected according to a listview.
For this first issue, I created a Sheet ("Sheet1") to which I added an ActiveX ListView (MicroSoft ListView Control, version 6.0) that I populated with the Column headers (or property names) as follows in a regular module:
Option Explicit
Sub PopulateListView()
Dim i As Integer
i = 1
With Worksheets("Sheet1")
.TestListView.ListItems.Clear
Do While Not IsEmpty(.Cells(1, i))
.TestListView.ListItems.Add i, , .Cells(1, i).Value
i = i + 1
Loop
End With
End Sub
I set the following properties:
Checkboxes to True
MultiSelect to True
This will allow us to loop over selected items and create properties in our CIssue class accordingly.
Next, I added a reference to MicroSoft Scripting Runtime, so the Dictionary class is available. This is needed, because with the Collection class there's no easy way to retrieve the "property" by "key" (or property name, as below).
I created the CIssue class as follows:
Option Explicit
Private p_Properties As Dictionary
Private Sub Class_Initialize()
Set p_Properties = New Dictionary
End Sub
Public Sub AddProperty(propertyname As String, value As Variant)
p_Properties.Add propertyname, value
End Sub
Public Function GetProperty(propertyname As Variant) As Variant
On Error Resume Next
GetProperty = p_Properties.Item(propertyname)
On Error GoTo 0
If IsEmpty(GetProperty) Then
GetProperty = False
End If
End Function
Public Property Get Properties() As Dictionary
Set Properties = p_Properties 'Return the entire collection of properties
End Property
This way, you can do the following in a regular module:
Option Explicit
Public Issue As CIssue
Public Issues As Collection
Public lv As ListView
Sub TestCreateIssues()
Dim i As Integer
Dim Item As ListItem
Set lv = Worksheets("Sheet1").TestListView
Set Issues = New Collection
For i = 2 To 10 'Or however many rows you filtered, for example those 72.
Set Issue = New CIssue
For Each Item In lv.ListItems 'Loop over ListItems
If Item.Checked = True Then ' If the property is selected
Issue.AddProperty Item.Text, Worksheets("Sheet1").Cells(i, Item.Index).value 'Get the property name and value, and add it.
End If
Next Item
Issues.Add Issue
Next i
End Sub
Thereby ending up with a Collection of CIssue objects, that only have the required properties populated. You can retrieve each property by using CIssue.GetProperty( propertyname ). It will return "False" if the property doesn't exist, otherwise the value of the property. Since it returns Variant it will cater for Dates, Strings, etc.
Note that if you want to loop over filtered rows, you can amend the loop above accordingly. Note that the propertyname parameter for the GetProperty method is also a Variant - This allows you to pass in strings as well as the actual Key objects.
To populate another sheet, with whatever you captured this way, you can do something like the following (in either the same or a different module; note that the Sub above needs to be run first, otherwise your Collection of CIssues will not exist.
Sub TestWriteIssues()
Dim i As Integer
Dim j As Integer
Dim Item As ListItem
Dim p As Variant
Dim k As Variant
i = 1
j = 0
'To write all the properties from all issues:
For Each Issue In Issues
i = i + 1
For Each p In Issue.Properties.Items
j = j + 1
Worksheets("Sheet2").Cells(i, j).value = p
Next p
j = 0
Next Issue
'And add the column headers:
i = 0
For Each k In Issues.Item(1).Properties.Keys
i = i + 1
Worksheets("Sheet2").Cells(1, i).value = k
'And to access the single property in one of the Issue objects:
MsgBox Issues.Item(1).GetProperty(k)
Next k
End Sub
Hope this is more or less what you were after.
N.b. more background on why the choice for Dictionary instead of Collection in this question

How do put Clipboard in its own thread to avoid STA threading issue

I have a class DLL library that I built from a sample I found on a website. The class converts RTF to HTML. I call it from my SQL Server Reporting Services report. The problem is that the code is using the clipboard which needs its own thread.
The error in SSRS states:
Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.
I tried implementing a thread by samples I found from the internet to no avail. Could some help show me how to take the clipboard piece of the function and put it in its own thread, and then when that is done, resume the function that builds the HTML string that sends it back to my SSRS report?
Here is the function:
Public Function sRTF_To_HTML(ByVal sRTF As String) As String
'Declare a Word Application Object and a Word WdSaveOptions object
Dim MyWord As Microsoft.Office.Interop.Word.Application
Dim oDoNotSaveChanges As Object = _
Microsoft.Office.Interop.Word.WdSaveOptions.wdDoNotSaveChanges
'Declare two strings to handle the data
Dim sReturnString As String = ""
Dim sConvertedString As String = ""
Try
'Instantiate the Word application,
'set visible to false and create a document
MyWord = CreateObject("Word.application")
MyWord.Visible = False
MyWord.Documents.Add()
'Create a DataObject to hold the Rich Text
'and copy it to the clipboard
Dim doRTF As New System.Windows.Forms.DataObject
doRTF.SetData("Rich Text Format", sRTF)
'HERE IS WHERE THE CLIPBOARD STATEMENTS BEGIN
Clipboard.SetDataObject(doRTF)
'Paste the contents of the clipboard to the empty,
'hidden Word Document
MyWord.Windows(1).Selection.Paste()
'…then, select the entire contents of the document
'and copy back to the clipboard
MyWord.Windows(1).Selection.WholeStory()
MyWord.Windows(1).Selection.Copy()
'Now retrieve the HTML property of the DataObject
'stored on the clipboard
sConvertedString = _
Clipboard.GetData(System.Windows.Forms.DataFormats.Html)
'HERE IS WHERE THE CLIPBOARD STATEMENTS END
'Remove some leading text that shows up in some instances
'(like when you insert it into an email in Outlook
sConvertedString = _
sConvertedString.Substring(sConvertedString.IndexOf("<html"))
'Also remove multiple  characters that somehow end up in there
sConvertedString = sConvertedString.Replace("Â", "")
'…and you're done.
sReturnString = sConvertedString
If Not MyWord Is Nothing Then
MyWord.Quit(oDoNotSaveChanges)
MyWord = Nothing
End If
Catch ex As Exception
If Not MyWord Is Nothing Then
MyWord.Quit(oDoNotSaveChanges)
MyWord = Nothing
End If
MsgBox("Error converting Rich Text to HTML")
End Try
Return sReturnString
End Function
My attempt to put the function in a thread did not work. Though I get no threading errors when I test the DLL in my solution using a form project to call it, I still get the threading error when calling it from a SQL Server Reporting Services report.
I tried the following thread and did not work.
Imports System.IO
Imports System.Threading
Imports System.Windows.Forms
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
<AttributeUsage(AttributeTargets.Method)> _
Public NotInheritable Class clsGetHTMLfromRTF
Inherits Attribute
Private Sub New()
End Sub
<STAThread()> _
Public Shared Function TranlslateRTFtoHTML(ByVal rtfText As String) As String
Dim thread = New Thread(AddressOf ConvertRtfInSTAThread)
Dim threadData = New ConvertRtfThreadData() With {.RtfText = rtfText}
thread.SetApartmentState(ApartmentState.STA)
thread.isBackground = True
thread.Start(threadData)
thread.Join()
Return threadData.HtmlText
End Function
Public Shared Sub ConvertRtfInSTAThread(ByVal rtf As Object)
Dim threadData = TryCast(rtf, ConvertRtfThreadData)
threadData.HtmlText = sRTF_To_HTML(threadData.RtfText)
End Sub
Public Class ConvertRtfThreadData
Public Property RtfText() As String
Get
Return m_RtfText
End Get
Set(ByVal value As String)
m_RtfText = value
End Set
End Property
Private m_RtfText As String
Public Property HtmlText() As String
Get
Return m_HtmlText
End Get
Set(ByVal value As String)
m_HtmlText = value
End Set
End Property
Private m_HtmlText As String
End Class
Public Shared Function sRTF_To_HTML(ByVal sRTF As String) As String
'Declare a Word Application Object and a Word WdSaveOptions object
Dim MyWord As Microsoft.Office.Interop.Word.Application
Dim oDoNotSaveChanges As Object = _
Microsoft.Office.Interop.Word.WdSaveOptions.wdDoNotSaveChanges
'Declare two strings to handle the data
Dim sReturnString As String = ""
Dim sConvertedString As String = ""
Try
'Instantiate the Word application,
'set visible to false and create a document
MyWord = CreateObject("Word.application")
MyWord.Visible = False
MyWord.Documents.Add()
'Create a DataObject to hold the Rich Text
'and copy it to the clipboard
Dim doRTF As New System.Windows.Forms.DataObject
doRTF.SetData("Rich Text Format", sRTF)
Clipboard.SetDataObject(doRTF)
'Paste the contents of the clipboard to the empty,
'hidden Word Document
MyWord.Windows(1).Selection.Paste()
'…then, select the entire contents of the document
'and copy back to the clipboard
MyWord.Windows(1).Selection.WholeStory()
MyWord.Windows(1).Selection.Copy()
'Now retrieve the HTML property of the DataObject
'stored on the clipboard
sConvertedString = _
Clipboard.GetData(System.Windows.Forms.DataFormats.Html)
'Remove some leading text that shows up in some instances
'(like when you insert it into an email in Outlook
sConvertedString = _
sConvertedString.Substring(sConvertedString.IndexOf("<html"))
'Also remove multiple  characters that somehow end up in there
sConvertedString = sConvertedString.Replace("Â", "")
'…and you're done.
sReturnString = sConvertedString
If Not MyWord Is Nothing Then
MyWord.Quit(oDoNotSaveChanges)
MyWord = Nothing
End If
Catch ex As Exception
Return ex.Message
If Not MyWord Is Nothing Then
MyWord.Quit(oDoNotSaveChanges)
MyWord = Nothing
End If
'MsgBox("Error converting Rich Text to HTML" & vbCrLf & ex.Message)
End Try
Return sReturnString
End Function
End Class
You can create a new Thread object and before you start the thread, call
SetApartmentState(ApartmentState.STA)
on it. In the thread procedure do your clipboard access.

VBA (Excel) Dictionary on Mac?

I have an Excel VBA project that makes heavy use of Windows Scripting Dictionary objects. I recently had a user attempt to use it on a Mac and received the following error:
Compile Error: Can't find project or library
Which is the result of using the Tools > References > Microsoft Scripting Runtime library.
My question is, is there a way to make this work on a Mac?
The following are the 3 cases I can think of as being possible solutions:
Use a Mac plugin that enables use of Dictionaries on Macs (my favorite option if one exists)
Do some kind of variable switch like the following:
isMac = CheckIfMac
If isMac Then
' Change dictionary variable to some other data type that is Mac friendly and provides the same functionality
End If
Write 2 completely separate routines to do the same thing (please let this not be what needs to happen):
isMac = CheckIfMac
If isMac Then
DoTheMacRoutine
Else
DoTheWindowsRoutine
End If
Pulling the Answer from the comments to prevent link rot.
Patrick O'Beirne # sysmod wrote a class set that addresses this issue.
Be sure to stop by Patirk's Blog to say thanks! Also there is a chance he has a newer version.
save this as a plain text file named KeyValuePair.cls and import into Excel
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "KeyValuePair"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'Unrestricted class just to hold pairs of values together and permit Dictionary object updating
Public Key As String
Public value As Variant
save this as a plain text file named Dictionary.cls and import into excel
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Dictionary"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
'Collection methods: Add, Count, Item, Remove
'Dictionary : .Add(Key as string, Item as variant), .CompareMode, .Count, .Exists(Key); _
.Item(Key) - writeable, .Items, .Keys, .Remove(Key), .RemoveAll
'plus KeyValuePairs collection, KeyValuePair(Index as long), Tag as variant
' 25-11-2011 KeyValuePair helper object
Public KeyValuePairs As Collection ' open access but allows iteration
Public Tag As Variant ' read/write unrestricted
Private Sub Class_Initialize()
Set KeyValuePairs = New Collection
End Sub
Private Sub Class_Terminate()
Set KeyValuePairs = Nothing
End Sub
' in Scripting.Dictionary this is writeable, here we have only vbtextCompare because we are using a Collection
Public Property Get CompareMode() As VbCompareMethod
CompareMode = vbTextCompare '=1; vbBinaryCompare=0
End Property
Public Property Let Item(Key As String, Item As Variant) ' dic.Item(Key) = value ' update a scalar value for an existing key
Let KeyValuePairs.Item(Key).value = Item
End Property
Public Property Set Item(Key As String, Item As Variant) ' Set dic.Item(Key) = value ' update an object value for an existing key
Set KeyValuePairs.Item(Key).value = Item
End Property
Public Property Get Item(Key As String) As Variant
AssignVariable Item, KeyValuePairs.Item(Key).value
End Property
' Collection parameter order is Add(Item,Key); Dictionary is Add(Key,Item) so always used named arguments
Public Sub Add(Key As String, Item As Variant)
Dim oKVP As KeyValuePair
Set oKVP = New KeyValuePair
oKVP.Key = Key
If IsObject(Item) Then
Set oKVP.value = Item
Else
Let oKVP.value = Item
End If
KeyValuePairs.Add Item:=oKVP, Key:=Key
End Sub
Public Property Get Exists(Key As String) As Boolean
On Error Resume Next
Exists = TypeName(KeyValuePairs.Item(Key)) > "" ' we can have blank key, empty item
End Property
Public Sub Remove(Key As String)
'show error if not there rather than On Error Resume Next
KeyValuePairs.Remove Key
End Sub
Public Sub RemoveAll()
Set KeyValuePairs = Nothing
Set KeyValuePairs = New Collection
End Sub
Public Property Get Count() As Long
Count = KeyValuePairs.Count
End Property
Public Property Get Items() As Variant ' for compatibility with Scripting.Dictionary
Dim vlist As Variant, i As Long
If Me.Count > 0 Then
ReDim vlist(0 To Me.Count - 1) ' to get a 0-based array same as scripting.dictionary
For i = LBound(vlist) To UBound(vlist)
AssignVariable vlist(i), KeyValuePairs.Item(1 + i).value ' could be scalar or array or object
Next i
Items = vlist
End If
End Property
Public Property Get Keys() As String()
Dim vlist() As String, i As Long
If Me.Count > 0 Then
ReDim vlist(0 To Me.Count - 1)
For i = LBound(vlist) To UBound(vlist)
vlist(i) = KeyValuePairs.Item(1 + i).Key '
Next i
Keys = vlist
End If
End Property
Public Property Get KeyValuePair(Index As Long) As Variant ' returns KeyValuePair object
Set KeyValuePair = KeyValuePairs.Item(1 + Index) ' collections are 1-based
End Property
Private Sub AssignVariable(variable As Variant, value As Variant)
If IsObject(value) Then
Set variable = value
Else
Let variable = value
End If
End Sub
Public Sub DebugPrint()
Dim lItem As Long, lIndex As Long, vItem As Variant, oKVP As KeyValuePair
lItem = 0
For Each oKVP In KeyValuePairs
lItem = lItem + 1
Debug.Print lItem; oKVP.Key; " "; TypeName(oKVP.value);
If InStr(1, TypeName(oKVP.value), "()") > 0 Then
vItem = oKVP.value
Debug.Print "("; CStr(LBound(vItem)); " to "; CStr(UBound(vItem)); ")";
For lIndex = LBound(vItem) To UBound(vItem)
Debug.Print " (" & CStr(lIndex) & ")"; TypeName(vItem(lIndex)); "="; vItem(lIndex);
Next
Debug.Print
Else
Debug.Print "="; oKVP.value
End If
Next
End Sub
'NB VBA Collection object index is 1-based, scripting.dictionary items array is 0-based
'cf Scripting.Dictionary Methods s.Add(Key, Item), s.CompareMode, s.Count, s.Exists(Key); _
s.Item(Key) - updateable, s.Items, s.Key(Key), s.Keys, s.Remove(Key), s.RemoveAll
'Scripting.Dictionary has no index number; you can index the 0-based variant array of Items returned
' unlike Collections which can be indexed starting at 1
'Efficient iteration is For Each varPair in thisdic.KeyValuePairs
'Another difference I introduce is that in a scripting.dictionary, the doc says
' If key is not found when changing an item, a new key is created with the specified newitem.
' If key is not found when attempting to return an existing item, a new key is created and its corresponding item is left empty.
'but I want to raise an error when addressing a key that does not exist
'similarly, the scripting.dictionary will create separate integer and string keys for eg 2
Patirk's implementation doesn't work for MS Office 2016 on Mac. I made use of the implementation by Tim Hall.
Here is the link: https://github.com/VBA-tools/VBA-Dictionary
Also import of cls files into Excel doesn't work in MS Office 2016 on Mac as of September 2017. So I had to create a class module and to copy and paste the contents of Dictionary.cls manually in that module while removing meta info from Dictionary.cls such as VERSION 1.0 CLASS, BEGIN, END, Attribute.
I have at last updated the files for Excel 2016 for Mac.
http://www.sysmod.com/Dictionary.zip
(capital D in Dictionary)
Unzip this and import the class files (tested in Excel 2016 for Mac 16.13 Build 424, 27-Apr-2018)
My bug report to MS is at answers.microsoft.com
Excel 16.13 for Mac User Defined Class passed as parameter all properties are Null
Let me know if I've missed anything else!
Good luck,
Patrick O'Beirne

Trying to set up a custom object model using example, not working

I am trying to set up a custom object model using an example I found in an answered question here on stackoverflow.
VBA Classes - How to have a class hold additional classes
Here is the code I have created based on the answer.
Standard Module
Sub test()
Dim i As Long
Dim j As Long
'code to populate some objects
Dim AssemList As Collection
Dim Assem As cAssem
Dim SubAssemList As Collection
Dim SubAssem As cSubAssem
Set AssemList = New Collection
For i = 1 To 3
Set SubAssemList = New Collection
Set Assem = New cAssem
Assem.Description = "Assem " & i
For j = 1 To 3
Set SubAssem = New cSubAssem
SubAssem.Name = "SubAssem" & j
SubAssemList.Add SubAssem
Next j
Set Assem.SubAssemAdd = SubAssemList '<------ Object variable or With Block not Set
AssemList.Add Assem
Next i
Set SubAssemList = Nothing
'write the data backout again
For Each clock In AssemList
Debug.Print Assem.Description
Set SubAssemList = Assem.SubAssems
For Each SubAssem In SubAssemList
Debug.Print SubAssem.Name
Next
Next
End Sub
cAssem Class
Private pDescription As String
Private pSubAssemList As Collection
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
Public Property Get Description() As String
Description = pDescription
End Property
Public Property Let Description(ByVal sDescription As String)
pDescription = sDescription
End Property
Public Property Get SubAssems() As Collection
Set SubAssems = pSubAssemList
End Property
Public Property Set SubAssemAdd(AssemCollection As Collection)
For Each AssemName In AssemCollection
pSubAssemList.Add AssemName ' <------- This is the line that is triggering the error
Next
End Property
cSubAssem Class
Private pSubAssemName As String
Public Property Get Name() As String
Name = pSubAssemName
End Property
Public Property Let Name(ByVal sName As String)
pSubAssemName = sName
End Property
I have not changed anything in the code except class names and variable names and from my limited point of view I cannot understand the cause of the error.
I am just starting to really dig into objects and Class Modules in VBA so I appreciate any knowledge this community could pass my way.
Many Thanks
You have a typo in your sub class initializer:
Private Sub Class_Initialize()
Set pSubAssems = New Collection
End Sub
should read:
Private Sub Class_Initialize()
Set pSubAssemList = New Collection
End Sub

Resources