Wednesday, August 9, 2006

Web Document Locking Version 0.1 (SnTT)

I've just had to implement document locking for a web application. What follows are the notes I have made on what it will do and all of the steps to reproduce it in other applications. I say it's "Version 0.1" because I know there are some options I can do to streamline the code. I can probably set a field value to pass parameters back into a single WebQuerySave agent or at least merge the webLock and webUnlock agents. Thanks to devWorks and Jake Howlett for information on the onbeforeunload event. And thanks to Marc for Agent and Query parameter code. Please, only positive criticism of the code... :)

See limitations at link below for the extra pop up window from the onbeforeunload event. I need to trap for when the user clicks OK. If I can do so, then it shouldn't throw up the onbeforeunload event window a second time:
onBeforeUnload (devWorks Posting)

Locking Options:
If Locked – no edit button if locked by another user and red text displays who has it locked
Going into Edit Mode – Locks the document
Closing form by Any Action Bar Button – Unlocks properly with no unload prompts
Navigating away from the form without using action bar – Currently two pop ups will warn you if you click OK to proceed from the form. I hope to get it back down to just popping up once, but this is a big start. Clicking cancel will take you back to the form so that you can exit the document gracefully. Clicking OK will close the document, unlock it, and not save any other modifications. This pop up would display if the browser is closed, another view is selected, another address is typed in the address bar, etc...

Form Modifications:
Text somewhere on the form:
Document is being modified by [Computed Value]
[computed value] = @Name([CN];$Writers)
Hide-when for text: @IsNewDoc | ( !@If(@IsAvailable($writers)))

Hide-when sample for Edit Button:
@IsNewDoc | ( @If(@IsAvailable($writers)) & !@If($writers=@UserName) )

HTML Head Content: (In the below code, substitute the "[[" and "]]" with "<" and ">", respectively.)
@Implode(
("[[SCRIPT LANGUAGE=\"JavaScript\"]]") :
("isEditMode = " + @If(@IsDocBeingEdited; "true;"; "false;")) :
("isLocked = " + @If(@IsAvailable($writers); "true;"; "false;")) :
("DBURL = location.protocol + '//' + location.host + '/" + @WebDbName + "/';") :
("DocUNID = '" + @Text(@DocumentUniqueID) + "';") :
"[[/SCRIPT]]";
@NewLine)

HTML Body Attributes:
"onbeforeunload=\"beforeunload();\""

WebQueryOpen:
@If(@IsDocBeingEdited;@Command([ToolsRunMacro]; "webLock");"")

WebQuerySave:
@Command([ToolsRunMacro]; "wqsAgent")

JS Header:
function beforeunload() {
msg = "----------------------------------------------------------\n";
msg += "Your form has not been saved.\n";
msg += "All changes you have made will be lost\n";
msg += "----------------------------------------------------------";
if (isEditMode && isLocked){
if (document.forms[0].closewarning.value="Yes") event.returnValue = msg;
}
}

onLoad:
if (isEditMode && isLocked)
{document.forms[0].closewarning.value="Yes";
}

onUnload:
window.onbeforeunload = null;
document.forms[0].closewarning.value="No";
if (isEditMode && isLocked) {
location.href = DBURL + "webunlock?openagent&unid=" + DocUNID;
}

Action Buttons: Second line is optional and really unnecessary
window.onbeforeunload = null;
document.forms[0].closewarning.value="No";
other code & submit() to call WebQuerySave

Agents:

wqsAgent: (just have this code somewhere in either WQS or whatever code you have that needs to update the backend document after form submission)

Call doc.Unlock

webLock:

Sub Initialize
'Called on web QuerySave
Dim s As New NotesSession
Dim doc As NotesDocument
Dim db As NotesDatabase
Set db = s.CurrentDatabase
Set doc = s.DocumentContext


Call doc.Lock(s.EffectiveUserName)
Call doc.Save(True,False)


End Sub

webUnlock:

Sub Initialize
'Called on unload and some other events
Dim s As New NotesSession
Dim db As NotesDatabase
Dim doc As NotesDocument
Dim AgentParams As Variant ' will be returned as a String List
Dim UNID As String

On Error Goto ErrorHandler
Set db = s.CurrentDatabase
Msgbox "Unlocking Document"
AgentParams = getAgentParameters(s.DocumentContext, True)
If Not Iselement(AgentParams("unid")) Then Exit Sub
UNID = AgentParams("unid")
Set doc = db.GetDocumentByUNID(UNID)
If Not doc Is Nothing Then Call doc.UnLock()
Print "[http://server/" & db.Filepath & "/TicketsOpen?OpenView]"
Exit Sub

ErrorHandler :
DisplayErr("Initialize")
End
End Sub

Sub DisplayErr(CodeName As String)
%REM
Purpose: Display error message
Details: Displays an error message for the current error
on the status bar and in a dialog box
includes CodeName in message (generally name of calling function)
%END REM

Dim tmpString As String
tmpString = "Error on line " & Cstr(Erl()) & " in " & CodeName & " - Error " & Cstr(Err()) & " : " & Error$(Err())
Print tmpString
Messagebox tmpString
End Sub

Function getAgentParameters (AgentDoc As NotesDocument, lowercaseTags As Integer)
%REM
Purpose: Returns a list of all agent parameters
Details: Returns a list of all agent parameters
if lowercaseTags is true, List Tags are all in lowercase
if lowercaseTags is false, List Tags are in original case
%END REM

GetAgentParameters = getQueryParameters(AgentDoc, lowercaseTags)
End Function

Function getQueryParameters (Doc As NotesDocument, lowercaseTags As Integer)
%REM
Purpose: Returns a list of parameters
Details: Using Query_String_Decoded, returns a list of all parameters
if lowercaseTags is true, List Tags are all in lowercase
if lowercaseTags is false, List Tags are in original case
%END REM

Dim QS As String, QSArray As Variant, QSList List As String
Dim tmpVar As String, tmpVal As String

QS = Strright(Doc.Query_String_Decoded(0), "&")
If QS <> "" Then
QSArray = Split(QS, "&")
Forall tmpQItem In QSArray
tmpVar = Strleft(tmpQItem, "=")
If lowercaseTags Then tmpVar = Lcase(tmpVar)
tmpVal = Strright(tmpQItem, "=")
QSList(tmpVar) = tmpVal
End Forall
End If
getQueryParameters = QSList
End Function


This LotusScript was converted to HTML using the ls2html routine,
provided by Julian Robichaux at nsftools.com.


Technorati:

6 comments:

ABC said...

Provide examples of the database?

Chris Whisonant said...

I believe that the code I've posted in this blog post should more than suffice in getting this ported into your own database.

Anonymous said...

But what happens when user clicks on Exit Button (X) of browser.
Since the agent on unload event is always returning some URL

Anonymous said...

But what happens when user clicks on Exit Button (X) of browser.
Since the agent on unload event is always returning some URL

Nilay Patil
Email: Patil.nilay@gmail.com

Chris Whisonant said...

Nilay, the beforeunload js function is what handles this from the html body attributes. The code is in the post and I also described a limitation I have found in a previous post. When you click the X, the beforeunload kicks in and asks if you're sure (if I recall my testing with this...). Are you having trouble with that part of it?

Anonymous said...

But what happens when user clicks on Exit Button (X) of browser.

I believe he has that eventuality covered... closing the browser calls the onUnload code.