Using Lotus from VBA, how to import rich text items to a new session? - vba

I have a VBA function that initializes a lotus notes session, creates a document and mails it. It also accepts as an optional parameter a NotesRichTextItem which I append to the body of the email.
However, I am getting the error message "All objects must be from the same session". How do I 'import' this NotesRichTextItem into my session?
Edit-Code added
Sub SendLotusMail(SubjTxt As String, _
BodyTxt As String, _
EmailTo As String, _
EmailCC As String, _
AutoSend As Boolean, _
Attach As String, _
ReportTitle As String, _
Optional AppendToBody As NotesRichTextItem = Null)
On Error GoTo EH
NtSession.Initialize
OpenMailDb ReportTitle
Set NtDoc = Ntdb.CreateDocument
NtDoc.AppendItemValue "Form", "Memo"
NtDoc.AppendItemValue "SendTo", EmailTo
NtDoc.AppendItemValue "CopyTo", EmailCC
NtDoc.AppendItemValue "Subject", SubjTxt
Set NtBodyRT = NtDoc.CreateRichTextItem("Body")
NtDoc.AppendItemValue "Body", NtBodyRT
If Attach <> "" Then NtBodyRT.EmbedObject EMBED_ATTACHMENT, "", Attach, "Attachment"
NtBodyRT.AppendText BodyTxt
'This next line throws the error "All objects must be from the same session"
NtBodyRT.AppendRTItem AppendToBody
Edit-Solution found
I don't like it very much, but I got around all these issues by passing the RichTextItem object, it's parent NotesDocument, and it's parent's parent NotesSession to this function. So, now I'm calling this procedure with 3 optional objects instead of 1. Hooray.
Edit-New Solution found
Well, the previous solution was causing me problems, so until I find (or someone suggests) a workaround, I'll just use some custom email procedures for the reports that require it. It does duplicate some code, but not significantly.

The issue may be the fact that the NtSession object is being re-initialized in your sub. If the calling routine sends in a rich text item, I am assuming it must have created and initialized a NotesSession as well. If that's the case, you would want your code to re-use that same session. It looks like NtSession is a global - if that's the case, you could:
Enforce that the calling routing always have initialized that global session;
Optionally pass in a NtSession object as an argument (and your code can check if that object is null before creating and initializing its own session); or
Before calling Initialize, check if NtSession already is initialized - to do that, you may be able to check an attribute and see if the object throws on error (non-tested code):
function isNotesSessionInitialized (ns)
on error goto err
dim sUser
sUser = ""
sUser = ns.commonUserName
err:
if (sUser = "") then
return false
else
return true
end if
end function

It would help to see some code here. I'll make a guess at what is happening, though.
In your VBA function, you'll need to create a new NotesRichTextItem object within your email. For instance:
Dim docMail as New NotesDocument(db)
Dim rtBody as New NotesRichTextItem(docMail, "Body")
Call rtBody.AppendRTItem(myRTparameter)
I imagine that should work without an error.

(I'm writing this to close out my question)
I've gotten around this issue by just having separate email procs for the reports that require custom setups. Yes, there is some duplication of code, but it's far better than the behemoth I was about to make.

Related

How to get data in Access VBA from a C# method that returns an object List

I'm calling a C# WebService method called getInterventions() on a VBA Access 2003 application through a custom DLL. The signature of this method is as follows:
List<Item> getInterventions(string, string, string, string, string, string)
Item is a custom defined class.
When I try to retrieve the result of getInterventions() on VBA Access code, it pops a VBA Runtime error 242 : object required
The following is my code:
Dim WsObject As Object
Dim result As Object
Set WsObject = CreateObject("Namespace1.Path.To.Class") 'This isn't the actual DLL path as I cannot share that
'Set result = WsObject .getSingleIntervention("123, "0", "123456789", "") ' this works
Set result = WsObject .getInterventions("", "", "123456789", "", "", "") 'this doesn't work
If result Is Nothing Then
'do some stuff
Else
'do some other stuff
End If
getSingleIntervention() is a similar method which returns a single object rather than a list of objects. Returning the result of this method works without issues (see commented line). This proves that both the WS & DLL calls work. This method is defined as follows:
Item getSingleIntervention(string, string, string, string)
I have tested calling getInterventions() directly from the C# code via Visual Studio 2015 with the same parameters I'm passing in my VBA code and it worked. This proves that it's not an issue with parameters or the method content.
My conclusion:
I am guessing it's something to do with the fact that I can't simply store a C# List of objects into a VBA Object.
Any help would be appreciated, thank you.
Please, automatically add mscorlib.tlb reference, running the next code:
Sub addMscorlibRef() 'necessary to use ArrayList
'Add a reference to 'Mscorlib.dll':
'In case of error ('Programmatic access to Visual Basic Project not trusted'):
'Options->Trust Center->Trust Center Settings->Macro Settings->Developer Macro Settings->
' check "Trust access to the VBA project object model"
If dir("C:\Windows\Microsoft.NET\Framework64\v4.0.30319", vbDirectory) = "" Then _
MsgBox "You need to install ""Framework version 3.5"".": Exit Sub
On Error Resume Next
Application.VBE.ActiveVBProject.References.AddFromFile "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.tlb"
If err.Number = 32813 Then
err.Clear: On Error GoTo 0
MsgBox "The reference already exists...": Exit Sub
Else
On Error GoTo 0
MsgBox """Mscorlib"" reference added successfully..."
End If
End Sub
Then try declaring Dim result As ArrayList.
The ArrayList is the same one that is used in C#. Then, adapt the dll to use such an object. As you deduced, no any object able to be used in VBA can receive the C# list object content.

Reference to Window Object in VBA

I have a problem referencing a windows object in VBA. it throws the following error: "Error 5 (Invalid procedure call or argument). I cannot find the cause, because I see no programming error.
Public Sub TestWindowhandle()
Dim lResult As Long
Dim objShell, wins, winn
Dim IE_Count As Long, i As Long, This_PID As Long
On Error GoTo TestWindowhandle_Error
Set objShell = CreateObject("Shell.Application")
Set wins = objShell.Windows
IE_Count = wins.Count
For i = 0 To (IE_Count - 1)
Set winn = wins.Item(i)
Next i
On Error GoTo 0
Exit Sub
TestWindowhandle_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in line " & Erl & " in procedure TestWindowhandle of Module Module1"
Stop
End Sub
Something odd with that interface, it seems to only work with a copy of the control variable so:
Set winn = wins.Item(i + 0)
or
Set winn = wins.Item((i))
I believe here is what is happening.
The Item method accepts a Variant parameter.
When calling an external method that accepts a Variant parameter, VB likes to create and pass Variants that provide the value by reference - that is, with the VT_BYREF flag set.
However VB does not set this flag when sending out intermediate results (temporaries, not stored in a variable), which makes sense because even if the called method updates the value, no one will be able to see it.
So when you call .Item(i), VB sends out a Variant of type VT_I4 | VT_BYREF, and when you call .Item(i + 0), a Variant of type VT_I4 is dispatched, without the VT_BYREF.
In most situations the difference is not significant because VARIANT-aware methods should be able to cope with either. However this particular method does different things depending on exactly which VT_ it receives, and because of this it is explicitly willing to reject any VT_s other than the three accepted ones. So in order to call it you need to trick VB into removing the byref flag.
Interestingly, when you declare the variable as Variant, VB still dispatches a VT_VARIANT | VT_BYREF, but the method seems to support this situation and correctly resolves to the pointed-to inner Variant that has the non-reference type of VT_I4.
Note this has nothing to do with VB's ByVal/ByRef - this is about the internal structure of the VARIANT data type.

How do you correctly set document properties using VBA?

The problem
I'm having some trouble setting document properties using VBA in Word 2010.
I have a document containing several Heading 1 sections and I use a macro to extract a selected section (along with it's contents) and paste it to a new document.
This part works fine, but at the end I need to set several document properties, but none of them are being set.
I'm trying to set both built-in and custom properties, but for the purpose of this question I'd like to set title, subject and, category.
I've created a function to set the properties I desire (as below), and VBA is throwing no error (even when I remove error handling in the function).
Does anybody know what I am doing wrong?
How the function should work
Here is a brief summary of what the function should do, but the full function is below should you find it easier to check that -
Check to see if the property already exists
It does and it is a default property
Set the default property
Set the PropertyTypeUsed variable to default
it does and it is a custom property
Set the custom property
Set the PropertyTypeUsed variable to custom
It does not exist at all
Create a new custom property
Set the custom property
Set the PropertyTypeUsed variable to custom
Check whether or not a value has successfully been set
A default property should have been set
Was the property set successfully?
A custom property should have been set
Was the property set successfully?
Return the result
The function I believe is causing the issue
Function UpdateDocumentProperty(ByRef doc As Document, _
ByVal propertyName As String, _
ByVal propertyValue As Variant, _
Optional ByVal propertyType As Office.MsoDocProperties = 4)
'** Set the result to 'False' by default '*
Dim result As Boolean
result = False
'** A property to hold whether or not the property used is default or custom *'
Dim propertyTypeUsed As String
'** Check to see if the document property already exists *'
If PropertyExists(doc, propertyName) Then ' A default property exists, so use that
doc.BuiltInDocumentProperties(propertyName).value = propertyValue
propertyTypeUsed = "default"
ElseIf PropertyExists(doc, propertyName, "custom") Then ' A custom property exists, so use that
doc.CustomDocumentProperties(propertyName).value = propertyValue
propertyTypeUsed = "custom"
Else ' No property exists, so create a custom property
doc.CustomDocumentProperties.Add _
name:=propertyName, _
LinkToContent:=False, _
Type:=propertyType, _
value:=propertyValue
propertyTypeUsed = "custom"
End If
'** Check whether or not the value has actually been set *'
On Error Resume Next
If propertyTypeUsed = "default" Then
result = (doc.BuiltInDocumentProperties(propertyName).value = propertyValue)
ElseIf propertyTypeUsed = "custom" Then
result = (doc.CustomDocumentProperties(propertyName).value = propertyValue)
End If
On Error GoTo 0
UpdateDocumentProperty = result
End Function
Full project code
The full code for this project can be found in two Paste Bins -
The functions
The form
I'm not sure if it's possible to get the code for actually creating the form (short of exporting it, but I have no where to put it), but in any case it's very simple -
The form - frmChooseDocument
The label - lblChooseDocument (Which New Starter document would you like to export?)
The combobox - comChooseDocument
The cancel button - btnCancel
The OK button - btnOK (Initially disabled)
In reality I'm using the document that houses this code as a 'master' document for new startes, containing detailed instructions on how to use variouse applications.
The code itself looks for Heading 1 formatted text within the document and adds them to the combobox in the form, allowing the user to select a section to export. A new document is then created and saved as a PDF.
Update
As suggested in the comments, I have checked that the type of value being set matches that of the value being passed to the function and it does.
In the case of all 3 properties described above, both the value that I am passing and the property as stored against the document are of type string.
I've added a couple of lines to output the type and value where I am setting the result and all looks well, but obviously it is not!
Debug.Print "My value: (" & TypeName(propertyValue) & ")" & propertyValue
Debug.Print "Stored property: (" & TypeName(doc.BuiltInDocumentProperties(propertyName).value) & ")" & doc.BuiltInDocumentProperties(propertyName).value
Here is the output -
My value: (String)New Starter Guide - Novell
Stored property: (String)New Starter Guide - Novell
My value: (String)New starter guide
Stored property: (String)New starter guide
My value: (String)new starters, guide, help
Stored property: (String)new starters, guide, help
I managed to set my word document title by saving the document after changing the property. I set the "Saved" property to false first to make sure that Word registers the change in state.
Function ChangeDocumentProperty(doc As Document, sProperty As String, sNewValue As String)
Debug.Print "Initial Property, Value: " & sProperty & ", " & doc.BuiltInDocumentProperties(sProperty)
doc.BuiltInDocumentProperties(sProperty) = sNewValue
doc.Saved = False
doc.Save
ChangeDocumentProperty = (doc.Saved = True And doc.BuiltInDocumentProperties(sProperty) = sNewValue)
Debug.Print "Final Property, Value: " & sProperty & ", " & doc.BuiltInDocumentProperties(sProperty)
End Function
Immediate Window:
? ThisDocument.ChangeDocumentProperty(ThisDocument, "Title", "Report Definitions")
Initial Property, Value: Title, Report Glossary
Final Property, Value: Title, Report Definitions
True
Permanent object properties cannot be set by functions. In other words, VBA does not allow functions to have side effects that persist after the function is finished running.
Re-write the function as a Sub and it should work.

Alternatives to using a Collection class

I have been looking through old code to get familiar with the system I use and found a piece of code that I feel can be used better.
What goes on here is some data gets added to the collection(around 150 string variables, some with two variables(variableName/VariableValue), most with only one(VariableName)). It will try to set a module level string variable to the item of the collection passing it the index(variableName) then if there's a value setting the VariableVAlue to the module level variable.
What I feel needs work is that if the collection is passed a variable and the variable doesn't have a value it will return a "" which would cause a runtime error hence there's a On Error GoTo Handler code to manually add a "" to the collection. I feel there's a better way to do this rather than knowing there will be a runtime issue then solving it after catching it. Would there be a way to have a return "" not throw an exception or would the use of an Array also work here since it's a "collection" as well?
Here's an example to try to help visualize:
Public Function GetCollectionVariable(ByVal varName as string) as String
If collection1 Is Nothing Then
m_collection1 = New Collection
End If
On Error GoTo Handler
GetCollectionVariable = collection1.Item(VarName)
exit function
Handler:
collection1.add("", VarName)
GetCollectionVariable = ""
End FUnction
Thanks for your time!!
If Collection1 is a dictionary, you can use TryGetValue.

Restrict type in a Collection inside a class module

I have a collection inside a class module. I'd like to restrict the object type that is "addable" to this collection, i.e. collection should only ever accept objects of one given type and nothing else.
Is there any way to enforce the type of objects added to a collection?
From what I can tell, there is no built-in way to do this. Is the solution then to make this collection private, and build wrapper functions for the methods usually accessible for Collections, i.e. Add, Remove, Item, and Count?
I kinda hate having to write 3 wrapper functions that add no functionality, just to be able to add some type enforcement to the Add method. But if that's the only way, then that's the only way.
There is no way to avoid wrapper functions. That's just inherent in the "specialization through containment/delegation" model that VBA uses.
You can build a "custom collection class", though. You can even make it iterable with For...Each, but that requires leaving the VBA IDE and editing source files directly.
First, see the "Creating Your Own Collection Classes" section of the old Visual Basic 6.0 Programmer's Guide:
http://msdn.microsoft.com/en-us/library/aa262340(v=VS.60).aspx
There is also an answer here on stackoverflow that describes the same thing:
vb6 equivalent to list<someclass>
However, those are written for VB6, not VBA. In VBA you can't do the "procedure attributes" part in the IDE. You have to export the class module as text and add it in with a text editor. Dick Kusleika's website Daily Dose of Excel (Dick is a regular stackoverflow contributer as you probably know) has a post from Rob van Gelder showing how to do this:
http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/
In your case, going to all that trouble - each "custom collection" class needs its own module - might not be worth it. (If you only have one use for this and it is buried in another class, you might find that you don't want to expose all of the functionality of Collection anyway.)
This is what I did. I liked Rob van Gelder's example, as pointed to by #jtolle, but why should I be content with making a "custom collection class" that will only accept one specific object type (e.g. People), forever? As #jtolle points out, this is super annoying.
Instead, I generalized the idea and made a new class called UniformCollection that can contain any data type -- as long as all items are of the same type in any given instance of UniformCollection.
I added a private Variant that is a placeholder for the data type that a given instance of UniformCollection can contain.
Private mvarPrototype As Variant
After making an instance of UniformCollection and before using it, it must be initialized by specifying which data type it will contain.
Public Sub Initialize(Prototype As Variant)
If VarType(Prototype) = vbEmpty Or VarType(Prototype) = vbNull Then
Err.Raise Number:=ERR__CANT_INITIALIZE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__CANT_INITIALIZE) & _
TypeName(Prototype)
End If
' Clear anything already in collection.
Set mUniformCollection = New Collection
If VarType(Prototype) = vbObject Or VarType(Prototype) = vbDataObject Then
' It's an object. Need Set.
Set mvarPrototype = Prototype
Else
' It's not an object.
mvarPrototype = Prototype
End If
' Collection will now accept only items of same type as Prototype.
End Sub
The Add method will then only accept new items that are of the same type as Prototype (be it an object or a primitive variable... haven't tested with UDTs yet).
Public Sub Add(NewItem As Variant)
If VarType(mvarPrototype) = vbEmpty Then
Err.Raise Number:=ERR__NOT_INITIALIZED, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__NOT_INITIALIZED)
ElseIf Not TypeName(NewItem) = TypeName(mvarPrototype) Then
Err.Raise Number:=ERR__INVALID_TYPE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__INVALID_TYPE) & _
TypeName(mvarPrototype) & "."
Else
' Object is of correct type. Accept it.
' Do nothing.
End If
mUniformCollection.Add NewItem
End Sub
The rest is pretty much the same as in the example (plus some error handling). Too bad RvG didn't go the whole way! Even more too bad that Microsoft didn't include this kind of thing as a built-in feature...
I did almost the same code of Jean-François Corbett, but I adapted because for some reason wasn't working.
Option Explicit
Public pParametro As String
Private pColecao As New Collection
Public Sub Inicializar(ByVal parametro As String)
pParametro = parametro
End Sub
Public Sub Add(NewItem As Object)
If TypeName(NewItem) <> pParametro Then
MsgBox "Classe do objeto não é compatível à coleção"
Else
pColecao.Add NewItem
End If
End Sub
Public Property Get Count() As Long
Count = pColecao.Count
End Property
Public Property Get Item(NameOrNumber As Variant) As Variant
Set Item = pColecao(NameOrNumber)
End Property
Sub Remove(NameOrNumber As Variant)
pColecao.Remove NameOrNumber
End Sub
Then, when i want to create an instance from CCollection I make like the code bellow:
Set pFornecedores = New CCollection
pFornecedores.Inicializar ("CEmpresa")
Where CEmpresa is the class type from the object I want
Yes. The solution is to make your collection private and then make public wrapper functions to add, remove, getitem and count etc.
It may seem like hassle to write the additional code but it is a more robust solution to encapsulate the collection like this.