Odd Behaviour of LotusScript Function Not Returning NotesDocument
I know I promised the flyout download today but that needs more work and I don't have the time today. Sorry.
Instead I need to ask for assistance. Something simple is driving me crazy and you're my only hope.
I have written a simple function in my global script library that I can use in all my agents to get the current user's Person document from the NAB. It's called GetUserDocument and looks like this:
Function GetUserDocument(username as String) As NotesDocument Dim NAB As New NotesDatabase(web.database.Server, "names.nsf") If NAB.IsOpen Then Dim UserView as NotesView Set UserView = NAB.GetView("($VIMPeople)") If UserView Is Nothing Then Set GetUserDocument = Nothing Exit Function End If Else Set GetUserDocument = Nothing Exit Function End If 'TESTING Dim tmp As NotesDocument Set tmp = UserView.GetDocumentByKey(username, True) print tmp.FullName(0) 'This prints ok and finds right user! 'END TESTING Set GetUserDocument = UserView.GetDocumentByKey(username, True) End Function
To test the function I've written an agent which contains the code below:
Dim UserDoc As NotesDocument Set UserDoc = GetUserDocument(web.user.Canonical) If UserDoc Is Nothing Then Print "Can't find it!" Else Print "Found it!" End If
The trouble is that it doesn't seem to work. What I get printed back is "Jake Howlett Can't find it!" which suggests that the function is getting to the end and the "tmp" document is found ok, but when the document is returned to the calling agent is becomes "nothing". What the...
I have other functions in the library that return NotesDocuments and they work fine. The only difference I can see is that they are in the same database, whereas, in this case, they are not. Is that some weird limitation I don't know about?
That's because you're declaring the NAB db object inside the called function.. when you return the document to the calling script it's parent database object is gone, therefore the document is lost too.
The Notes Forum post on this: {Link}
Aha. You won't believe this, but that did occur to me. Honestly. I just didn't think to test it.
Looks like my global "web" object needs a new AddressBook property...
Thanks Sandra!
I've had my share of head scratching because of this one. I'm glad I could help. ;)
Does the result change if you replace
Set GetUserDocument = UserView.GetDocumentByKey(username, True)
with
Set GetUserDocument = tmp
?
Maybe not as simple as i'd thought. I tried to add the NAB variable to my "web" global WebSession object, like so:
Class WebSession
Public NAB As NotesDatabase
Sub new
Set Me.NAB = New NotesDatabase(Me.database.Server,"names.nsf")
End Sub
End Class
set web= new WebSession()
But using web.NAB in the GetUserDocument still doesn't work. Does the DB have to be an actual global variable in its own right?
Jake
Scratch that last comment. It does work from the global web object.
In my code I always use a global list for caching NotesDatabases, NotesViews ...
This has at least 2 advantages: It is faster when the database or view is used later again AND you have no problems with the Lotusscript memory management which in your case deletes the instance of the NotesDatabase because it is not more used and thereby deletes all "depending" objects too.
In your case I would have (code not tested!)
Dim gCacheObjects List As Variant
Function NABDb As NotesDatabase
Dim id
id = "NABDb"
If Not Iselement(gCacheObjects(id)) Then
Set gCacheObjects(id) = New NotesDatabase(web.database.Server, "names.nsf")
End If
If Not gCacheObjects(id).IsOpen Then Error 32000, "NAB not available"
Set NABDb = gCacheObjects(id)
End Function
Function NABView(viewname) As NotesView
Dim id
id = "NABView#" & Ucase(viewname)
If Not Iselement(gCacheObjects(id)) Then
Set gCacheObjects(id) = NABDb.GetView(viewname)
End If
If gCacheObjects(id) Is Nothing Then Error 32000, "NAB-View <" & viewname & "> not available"
Call gCacheObjects(id).Refresh
Set NABView = gCacheObjects(id)
End Function
Function GetUserDocument(username As String) As NotesDocument
Dim id
id = "UserDocument#" & Ucase(username)
If Not Iselement(gCacheObjects(id)) Then
On Error Resume Next
Err = 0
Set gCacheObjects(id) = NABView("($VIMPeople)").GetDocumentByKey(username, True)
If Err <> 0 Then
Set gCacheObjects(id) = Nothing
Err = 0
End If
End If
Set GetUserDocument = gCacheObjects(id)
End Function
I run into that one all the time. I can never seem to remember what the problem is from one time to the next either.
Oh, and you might want to use NotesSession.AddressBooks to get the NAB instead. Just in case the database isn't names.nsf (happens sometimes).
Since Jake has his WebSession class already, making NAB a property of that session sounds like a much better idea than dealing with ugly global variables.
Preferably use lazy initialization on that one.
Additionally, I fully agree to Julian. To both parts of his posting ... ;-)
I think it works if you pass the db with it in the function as another object.
Function GetUserDocument(NAB as notesdatabase, username as String) As NotesDocument
This is off-topic, but why:
Set GetUserDocument = Nothing ?
The result of the Function would still be Nothing if you just did:
If UserView Is Nothing Then Exit Function
Dunno Tommy. Inexperience I guess. I assumed you had to have a line that set the result to the name of the function at some point in the proceedings. I'll take you word for it that you don't and maybe change it...
Simple way to verify for yourself:
Function test As NotesDocument
End Function
--
If test() Is Nothing Then
Msgbox "Test is Nothing"
Else
Msgbox "Test is something"
End If
--
In my limited experience, a Function always returns an uninitialized variable of it's return type.
Function test As String
End Function
test() -> ""
Dim testStr As String
testStr -> ""
I have to admit that I am lazy and often let functions return the default (Nothing, 0, False, "" or Empty, depending on datatype). However, setting it explicitly does make for better readability. Not everyone has these memorized.
There are three very simple lines of code that will make this work for you:
1 and 2: In the Declarations of the script library:
Public sess as NotesSession
Public dbThis as NotesDataBase
# 3 - Declare your function as public (you should specify "Option Public" in script lib options if it is not already there)
Public Function GetUserDocument(username as String) As NotesDocument..
As the session and current database are very commonly used in many global functions and subs, I take this a step further by declaring both session and database globally and then intialiizing them in the Intialize Sub of my global script lib. This negates the need to intialize them any number of different places or having to test them for Nothing over and over (testing for isOpen and isValid is recommended in your Initialze sub following the set statements), they are there ready to use by any function or sub within the library itself as well as any form, agent or action that 'use's that script lib
defaultLib
Options
Option Public
Declarations
Public sess as NotesSession
Public dbThis as NotesDatabase
Initialize
Set sess = New NotesSession
Set dbThis = sess.CurrentDatabase
If not ( dbThis.IsOpen ) Then
....error handling...
ElseIf not ( dbThis.IsValid ) Then
....error handling...
End If
ANY code that uses defaultLib can reference sess or dbThis objects without worry as to whether or not they are set
By declaring Function GetUserDocument as Public, the object it returns is available globally as well.
Explcit declaration and initialization or destruction of any variable definitely falls under best practice guidelines.
So, I talked about the pros of this approach, here's the cons:
1) Any code that uses defaultLib, takes the performance hit of declaring and initializing those two class objects (sess and dbThis).
2) If this code breaks for any reason, any code that uses is broken as well (we all know and respect the corruption factor of reusable code I'm sure).
Peace
Hi Jake,
I just tried your code, as passing around notes document objects without the database being in scope is something I've always done - and not had a problem with. I was thinking all day - have I been keeping the db's in scope by luck...nah, surely not. So with nothing better to do on a Saturday night, than settle my curiosity - I've imported your code into ND8 and it works as expected. I did make a couple of changes like adding the Option Public in the Library - if you don't you should get an error in the Agent during compile time. The other change is that I switched out the username to be a value. Then finally, I checked that the documents UNID was the same for both getdocumentbykey calls. I'd normally would have done what pedro suggested, rather than calling the getdocumentbykey multiple times.
You've probably done this already, but try setting the username to a literal value to see if you get any different results, then try a local names.nsf.
I've exported the code below, but I can send you a zipped copy of the db - if you want.
Not sure if this helps you or not - but I can sleep easy tonight :-)
'jsJake:
Option Public
Dim UserView As NotesView
Dim tmp As NotesDocument
Function GetUserDocument(username As String) As NotesDocument
Dim NAB As New NotesDatabase("", "names.nsf")
If NAB.IsOpen Then
Set UserView = NAB.GetView("($Users)")
If UserView Is Nothing Then
Set GetUserDocument = Nothing
Exit Function
End If
Else
Set GetUserDocument = Nothing
Exit Function
End If
'TESTING
Set tmp = UserView.GetDocumentByKey(username, True)
Print tmp.FullName(0) 'This prints ok and finds right user!
Print "Docid :"+tmp.UniversalID
'END TESTING
Set GetUserDocument = UserView.GetDocumentByKey(username, True)
End Function
'Development \ Testing Jake:
Use "jsJake"
Sub Initialize
Dim UserDoc As NotesDocument
Set UserDoc = GetUserDocument("jake")
If UserDoc Is Nothing Then
Print "Can't find it!"
Else
Print "Found it!" + Userdoc.Fullname(0)
Print "Doc id:"+Userdoc.UniversalID
End If
End Sub
Sub Terminate
End Sub
@Tony -- UserView lives outside of the function in your example, so the reference chain is alive outside of the function.
How come the chain can survive if the View is global and the database isn't? If that can happen why can't the document survive too? Odd.
@Stan
My mistake, copied the wrong lss. However,
I believe that once you have a handle to the object then you can pass that object to other functions, regardless of it's relationship in the class hierarchy (I'm not sure about this reference chain).
To check my sanity I tried the following - which also worked.
'jsJake:
Option Public
Function GetUserDocument(username As String) As NotesDocument
Dim NAB As New NotesDatabase("", "names.nsf")
Dim UserView As NotesView
Dim tmp As NotesDocument
If NAB.IsOpen Then
Set UserView = NAB.GetView("($Users)")
If UserView Is Nothing Then
Set GetUserDocument = Nothing
Exit Function
End If
Else
Set GetUserDocument = Nothing
Exit Function
End If
'TESTING
Set tmp = UserView.GetDocumentByKey(username, True)
Print tmp.FullName(0) 'This prints ok and finds right user!
Print "Docid :"+tmp.UniversalID
'END TESTING
' REMOVAL OF THE REFERENCE CHAIN
Print ("removing database from scope")
Set NAB = Nothing
' Delete NAB - this crashes the client
Print ("removing view from scope")
Delete UserView
Print tmp.FullName(0) 'This prints ok and finds right user!
Print "Docid :"+tmp.UniversalID
Set GetUserDocument = tmp
End Function
So even when I explicitly set the view and database to nothing it to nothing seems to work and keep the document object. Interestingly, I tried both delete and setting to nothing and deleting the NAB object crashes the 8.0.0 client.
In the Designer Help - LotusScript/COM/OLE classes in the 'Using an Object' the following is at the bottom.
"When you delete a Domino object, any contained objects are lost. For example, if you delete or close a NotesDatabase object, any NotesDocument objects in that database to which you refer are deleted."
So I went back to the code. In the list of variables after I set the db to nothing and delete the view they are both empty. However, the document still has a reference to its parent database, that you can then browse the views and isOpen = true. Strange.
Try this:
public Class NAB
Private vw As NotesView
Private db As NotesDatabase
Private isValid As Boolean
Sub new (sSV As String, sDB As String)
Set db = New NotesDatabase(sSV, sDB)
If db.IsOpen Then
Set vw = db.GetView("($VIMPeople)")
isValid = True
End If
End Sub
Function getUserDoc(sUsername As String) As NotesDocument
If Me.isValid Then
Set getUserDoc = vw.GetDocumentByKey( sUsername, True )
Else
Set getUserDoc = Nothing
End If
End Function
End Class
Tony. I think the big difference is that you're running the code locally in the client and I'm talking solely about WQ agents on the web. That's probably why we're seeing different results.
This is an old blog I know, but I'm sooo glad I found this via google! The same problem was driving me crazy too!
You need to declare in global your database,session in which your return document is.
For example i've a function that create a lock in an external DB, then another function that will check if a lock exist in this external DB (here it's sdoc variable in database DB). If my database DB is not declared in global, it doesn't work.
Yes, Ferry. It's still happening in my part of the world too ;p in 2010!
This used up 2 full days of head-scratching before I finally found this page through Google.
Clearly I don't often work in multi-db set-ups this way...
Thanks everyone!
By the way, I tried this trick and some-how it worked like magic:
set db = session.getdatabase(dbname)
set doc = db.getdocumentbyid(doc_id) 'or use lookup or anything..
url = doc.notesurl
set db = nothing
set doc = session.resolve(url)
with a function containing the above code, I could return the document object and use it in another procedure.
of course, this is not practical if, for example, you're working with a set of documents to return them in a documentcollection or an array.
I tend to declare the database in the function as a Static.
This seems to resolve the issue quite nicely.
So your initial code would be something like
Static NAB as NotesDatabase
If NAB Is Nothing Then
Set NAB = New NotesDatabase(web.database.Server, "names.nsf")
End If
Not really sure about the memory impact, but it keeps the code nice and clean (no global variables)