Prevent Save Conflicts On The Web
I'm working on a system where there's a danger of save conflicts. Probably only as much as there is in any other Domino web application, but it's a requirement that I cater for it. You know the scenario: two users open a document at the same time. They then both go on to press the submit button. This triggers a fairly complicated LotusScript WebQuerySave agent and, more often than not, results in a conflict.
Today I've been trying to tackle this using some additional LotusScript code in the agent and I'm very, very confused. My understanding has always been that code in the WebQuerySave event executes before any changes are actually committed to the back end. To quote from the help file (emphasis my own):
A WebQuerySave event runs the agent after field input validation formulas are run and before the document is actually saved to disk...
...the document is automatically saved after the agent runs.
What I've been finding is that the document replaces the one held "on disk" before the code has done running. Consider this code:
Set session = New NotesSession
Set database = session.CurrentDatabase
Set webDocument = session.DocumentContext
Set realDocument = database.GetDocumentByUNID( webDocument.UniversalID )
Print Cstr( webDocument.GetItemValue("Status")(0) ) + "<br />"
Print Cstr( realDocument.GetItemValue("Status")(0) )
Here there are two NotesDocument objects. One is the document being submitted and one is the document that already exists in the database. In theory that is. What I've found is that they seem to be one and the same. What would make sense to my simple way of thinking is that - until the WQS code has run - the context document hasn't been saved/committed and so the values held are different. With the above code the output is the same time value, twice.
Maybe the objects pointed to the same thing and the code was confused. Grasping at straws I even tried using the following approach:
Evaluate({@GetDocField("}+ webDocument.UniversalID+{" ; "Status" )},
webDocument)
Still, it returned the value of the field that is being submitted, making it appear as if the back-end document is over-written with the new values, irrespective of whether it is indeed saved or not. Am I being stupid here?
What I'm looking for is a way to check that the document being submitted hasn't been saved since it was opened? If it has then the agent is to print a warning back to the browsers and cancel the save. How can we check the values in the actual document with those of the one being submitted?
Of course, cancelling the save using LotusScript and a SaveOptions field is just as open to problem.
Notes likes to cache things - and reuse when possible. You're actually ending up with the same object twice in your example code. Here is some code that will give you the submitted object and the object on disk:
Dim session As New NotesSession
Dim database As NotesDatabase
Dim webDocument As NotesDocument
Dim unid As String
Dim realDocument As NotesDocument
Set session = New NotesSession
Set database = session.CurrentDatabase
Set webDocument = session.DocumentContext
unid = webDocument.UniversalID
Set webDocument = Nothing
Set realDocument = database.GetDocumentByUNID( unid )
Set webDocument = session.DocumentContext
Print webDocument.GetItemValue("Status")(0) & "<br />"
Print realDocument.GetItemValue("Status")(0)
Clearing out the variable before getting the object on disk assures that Notes won't take any shortcuts. After you get the object on disk, then you can get the submitted document again.
Brilliant Matt - tried and tested! Thanks for that. Such a counter-intuitive solution I think it would have been a while since I arrived there.
I hope it doesn't look lazy of me asking for answers this way. My hope's that it helps people out as a reference in the future, as well as helping me out ;-)
I think this might turn in to an article. I've added a few more little bits of trickery to a simple form that warns users if the document has changed. That is if you don't beat me to it Matt. Well done on BreakingPar. Always liked that site!
Hi Jake...
Matt... Nice solution to the problem. I am currently working on an Intranet Application where i am using this sort of technique.
Instead of a Form i use a Page with 2 hidden IFrames. One to open the document by calling an agent (unid passed in url) and getting it to print out the field values.
The second is used to Save changes by creating a form with only the field values that have changed. I pass up the unid and the last known modified date/time. With this i can detect if any changes have been made whilst i was editting!
It's a simple way to stop Save Conflicts as you never physically edit the document and can manage the saving of information.
Regards
Patrick
Smart solution!! I had a similar problem and used the solution from here: {Link}
But ofcourse Matt's solution doesn't even need the sorted view. Cool!!!
Hi Jake,
head over to www.taoconsulting.com.sg/downloads and get the zip "Tracing document changes in Notes". I've written the db for client/browser to compare submitted and stored document. Solves your question too.
;-) stw
I had not heard about setting the document to nothing, interesting tip. Another way to do the same thing without losing the handle to the doc in memory is to get the doc_SavedOnDisk = view.GetDocumentByKey(key,true)...
Hi Jake and friends
Yes another solution is possible, though it only works in IE. Using XML and the "readviewentries" we test if the lastmodifieddate from a view equals the lastmodifieddate in the "browserdocument". That way we can alert the user without even starting the webquerysaveagent. There more to it, but here a snippet of javascript to explain:
onDiskMod = nodes.item(0).childNodes.item(2).getElementsByTagName("text");
thisMod = document.forms[0].varCurLastSave.value;
if( onDiskMod(0).text != thisMod ){
rc = false
Cheers
Steenn AA
Steen. I'd thought about using that approach, but I was worried about the view having an index that might cache. Is there a way round that, which will gaurantee fresh results every time? Even if a user saved the document elsewhere, literally a second before.
Hi Jake,
I've found the most user friendly method was to have a system of stopping users open the document in the first place. This R5-friendly doc locking solution means they don't waste valuable editing time, especially if its a big document.
It does mean creating a locking database holding lock tokens (documents containing the owner+doc id)as well as triggering both (web)QueryOpen and web(QuerySave) events on the editable document.
So, for a single server solution this would be the sequence of events:
1. User tries to edit document
2. system checks list of tokens for that particular document.
3. If one exists, and user is the owner of that token the document opens in edit mode.
4. If one exists, but user is NOT owner of token, then document fails to open and tells the user who has locked it.
5. If one does not exist, then token is created and document opens.
6. On exiting the document, token is located and deleted by system.
Its sounds clunky, but it works well in practice and the clients seem to appreciate it. It really depends on the requirements.
There is an obvious issue with this solution, permanently locked documents due to PC crashes etc but a regular scheduled "cleaner" agent soon sorts this out.
...now for the multiple replica solution.
cheers
Andrew T.
I should add, of course, the above solution is for authenticated access only. Anonymous web access is going to be useless here.
Document locking was another option on the plate. I just really don't want to go there. Document locking on the web? Surely it's a nightmare and not to be trusted?!
I don't think it solves the problem here either. It's like a service desk scenario - the tickets need dealing with ASAP. Why wait for the first person to open it to get round to pressing save. Instead you can just let numerous people open it and give it to the *first* person to press save.
Weel frankly I didn't think much or hard about the cache issue. Testing showed though, that unless two users hit the save-button simultaniously (yes we counted 1-2-3-go), the "last" user gets the java script error alert. Somehow we must have done something right
:0) Steen AA
Thanks Steen. I'll give it a go. Hopefully I'll get it right too ;-)
The problem with locking on the web is making sure that locks are released. I've used a "lease" system, in which a frequently-running scheduled agent deletes lock documents that are older than, say, an hour (depending on the application) in combination with a time-since-opened calculation in the browser (and a setTimeout to warn users of leases coming near to expiration). Any save-and-continue renews the lease, save-and-close removes the lock, and abandoned documents are only unavailable for a limited time. (There's also an administrative release for docs known to be abandoned.) It's a bit of trouble, yes, but it gets around a lot of potential problems.
Jake,
As I mentioned in another (probably less relevant) thread ({Link} I've written something to handle save conflicts in web apps a couple of years ago.
I tried a number of different ways to trap save conflicts without any success. In the end, the solution was simple: test the backend document for the presence of a $Conflict field. If it's there, you have a save conflict. You can even navigate to the conflicts parent and tell the user who updated the document before they did.
The date stamping technique referred to in the above thread was used to trap 'stale' requests. This is where someone issues a ?EditDocument, makes some changes and leaves the form sitting there for a while before submitting it. The WebQuerySave agent detects that the request is stale (by comparing the date stamps) and informs the user.
Arka
Hi Jake and others
Since I read this post, we've been met with the Mozilla challenge. Because of that the code has been modified a bit.
It's still the XMLHttpRequest at stake, but initiated either for IE og Mozilla (new ActiveXObject("Microsoft.XMLHTTP") or new XMLHttpRequest() )
Jake, you asked about caching, and I think (yes, tested) this is all we need to ensure "fresh lookups": xmlhttp.setRequestHeader('If-Modified-Since','Wed, 31 Dec 1980 00:00:00 GMT');
must go before the xmlhttp.send(null);
Unfortunately the xml returned differs slightly. It seems like spaces are removed in IE; or not removed by Mozilla. Interpretation is free. But the result is that notes view columnvalues must be pointed to in different ways. (probably you could make a function to remove the spaces, but anyhow)
1.column value (IE)
xmlDoc.documentElement.childNodes.item(0).childNodes.item(0).childNodes.item(0).childNodes.item(0).nodeValue;
1.column value (Mozilla)
xmlDoc.documentElement.childNodes.item(1).childNodes.item(1).childNodes.item(1).childNodes.item(0).nodeValue;
BUT
3.column value(IE)
xmlDoc.documentElement.childNodes.item(0).childNodes.item(2).childNodes.item(0).childNodes.item(0).nodeValue;
3.column value(Mozilla)
xmlDoc.documentElement.childNodes.item(1).childNodes.item(5).childNodes.item(1).childNodes.item(0).nodeValue;
Notice the .nodeValue notation, since it works in both browsers - .text is IE only.
BTW: I've use IE 6.5 and Mozilla 1.7. I think at least Mozilla 1.5 is needed.
Well, I hope my experiences can save someone some time.
Cheers
Steen AA
Used the tip in reply #1 today - thanks Jake!