InViewEdit For The Web, Without AJAX -- AKA View Processor 2
If I know you as well as I should I think you ought to like this one!
Remember way back in 2003 I wrote about Processing multiple documents from a view?
It's a useful way in which to perform a certain action on a chosen set of documents in a view. It was all fairly simple stuff. In the case of the demo I put online back then all the backend code did was change one field value, based on which button was pressed.
The methodology used is still useful — although, in practice, you might want to perform more involved actions on each selected document. Now though, I've now come up with a much better way of doing it. That is, it's better if all you want to do is edit each document's fields directly, with no other logic involved, which is often the case.
What I found myself needing (and subsequently coding) this week was something like an InViewEdit solution, but for the web. Without the AJAX or JavaScript that is. Just good old HTML and a funky backend agent.
Although the actual requirement that made me arrive at the solution was quite simple, here's a more complex example of it in use. Click the screen shot below to go to the actual view and have a play.
Do us a favour while you're there — add some content and mess about with it. I'm keen to find any bugs there might be.
Bugs aside there is one rather major limitation. The processing agent refers to the Request_Content field, which has a 64kb limit. In the demo form this equates to about 300 rows. Although this should be plenty it's a same the limit exists. IBM have "removed" the limit in 7.0.1 but basing a workaround on that would be nigh on impossible with the approach I've taken.
As you'd expect, once it's ready for the prime time I'll put all the code and write-up in an article with the download. For the curious you can probably work out what's happening by examining the HTML and looking at the LotusScript of the Agent that the form gets POSTed to.
This solution came about due to the specific needs of a client's application. They like their privacy so I won't say what the need was. Let's say, for the sake of example, that they arrange meetings and need to know who attended. Each person invited is recorded as a child document of the meeting document. After each meeting they go to a view (restricted to category of parent document ID) called "Needs attendance recording". The view shows attendee documents where the Attendee field is blank. The view shows the attendees name along with a Yes/No radio button to quickly mark the attendance of all the people who were invited in one go. You can see this concept in the unmarked view.
To save space, for earch row, you could add a hidden "row_unid" field which contains the docunid/noteid plus the row number. Then the other field names in the same row can have the row number instead of the docunid/noteid. However, your agent will need to do a slightly bit more work in figuring out the data ...
I'd thought of something along those lines but favoured simplicity over saving space. If you're reaching the limits (64kb) then it's time to question the approach being used, so space isn't key IMO.
Another reason not to take that approach is that it will then only work if all fields are passed to the agent in the right order. At the moment it does that because each document is in a row. This doesn't have to be the case though. The beauty of the approach used is that you can pass FieldName[UNID] in any old order and the field will update. I can't think of a scenario where this might happen but it's better to be flexible.
..try typing the following into the first text field...
"></table>
I know.. but you said you were interested..
I found a small problem in your agent code in lines 26 and 28.
If the GetDocumentByUNID method fails to find a document, it raises an error lsERR_NOTES_BAD_UNID (4091). So checking 'If Not doc Is Nothing Then' is actually never reached. You should use "On Error 4091 Resume Next".
Deletion stubs can also cause problems. Fabrice Proudhon wrote more about it at {Link} .
Good point Roman. I hadn't thought of that. In this case I guess it's a matter of weighing up the chance of that happening. The only way it could is if either somebody deleted a document while you were in the process of opening/editing the view and submitting it or that the HTML form is mucked up or something. Both unlikely in most cases, but worth accounting for I guess. Will try and add the fix at some point.
Nicely hacked Andy. Fixed now though. Title is "escaped" in the view by replacing things with " and > etc
I did something just like this in 2001; man, was it a pain to code! It was on 4.6, and I don't remember if Treat View Contents as HTML didn't exist, or if I just didn't know about it, but I couldn't figure out how to code a column that would display a select box, so I ended up creating the "view" via an agent URL that looped through a view and spit out HTML to create the tables. The code I used for the submit button was actually fairly similar to what you wrote, but I'm betting it took me a whole lot longer to come up with...
Then, of course, the users weren't happy with a single button that would save all their changes; they wanted to have the option of clicking that one button, but also wanted to be able to selectively save changes, so I had to code a submit button on each row as well.
Not tried it but surely this approach is easily hackable as your not validating the fields you are adding to the documents. A simple bit of JavaScript or just a properly formatted URL to add additional fields to the forms could allow you to store anything. For internal use this approach is ok, for external use it would need a bit more work IMHO.
Good point John. Didn't think of that. Most (90%) of what I code is internal and I don't always give a lot thought to how safe it might be if a wannabe hacker went at it.
Not even sure I would (or even need to ever) use this approach for an external site. If I did then it would be easy to cater for a set of allowed fields. I'd probably make the agent run as the web user too.
As ever though it's just a "how to". Use at your own risk and all that. etc
Hey Jake,
I notice you list a known issue of 64k limit on Request_Content. You might find the limit is in the use of Evaluate. I've run into problems using Evaluate with @UrlEncode where certain XML message sizes would truncate unexpectedly. In practice, Request_Content should hold unlimited text (as you upload files through this same header). You might replace your Evaluate @Explode with Split and see if that resolves the problem.
Script replacements for @URLEncode / Decode:
{Link}
@ReplaceSubstring can be replaced with Join(Split(.....
The speed on the script might be a bit (teeny weeny bit) slower than Evaluate, but your limit should disappear.
Nice work as usual!
Hi Jerry. I wish you were right. However, this is a limit on request_content. You can read a bit about it in the link in the yellow panel above.
Notice in the lotusscript the first if is:
if doc.HasItem("Request_Content") then
If you send more than 64KB this if "fails" and it goes to the else clause which prints an error.
Instead of the Request_Content field when posting large amounts of data there's Request_Content_000 etc. Useful. Not.
You probably are right about the limit on evaluate but it won't come in to play in this case as the code won't get that far ;o)
Jake
Wow - certainly news to me. I would expect that using a create document url command, but Posting data is usually posting data... a limit on that is news indeed... though I must confess, upon recollection, most of my POST experience has been FROM Domino TO something else... Useful indeed. :-|
I suspect Domino treats request_content then as a rich text item on the context document. That's a silly way to handle the incoming data. I wonder if this is different for a Java agent.
This bugged me for some reason. I recall we ran into some of this with the afore mentioned URLEncode issue. We also had to adjust server settings. Blah. That's never a good answer in my mind. But, if you have a mind to do so, on the server document, the Internet Protocals tab, lower right corner, you can increase post content size by adjusting those values.
All of that is quite pointless though as you indicated much more and you're doing something crazy (often the case here, I assure you) and with pagination this is all very moot. Right. Now I can sleep.
btw, Jake, I couldn't reproduce your Request_Header_000 with Domino Server 6. Just no Request_Content peirod when blowing past the server document controlled limits.
The Request_Content_000 is how they "fixed" it in release 7 Jerry.
Jerry, the limit existed (and there was no way around it) in every version prior to 7.0.1. So, unless your server is on 7.0.1 or higher, there will never be a Request_Content_XXX item.
Some fix.
Aren't we forgetting that you can POST a rich-text field on a form and get >64KB all the way back in R6 (and likely earlier - I'm no Domino historian).
Then you can just have a WQS process the contents of that body for your view actions.
It's indirect, but works for me.
Also, it lends itself well to request logging.
Out of curiosity, does anyone know why Notes/Domino has those dreadful 64k limits all over the place?
I suspect it has something to do with the C-layer/datatypes used, but haven't found a good explanation.
We're digressing. The 64kb thing isn't really an issue here.
The idea and the intended solution is for simple scenarios like the one came up with above -- mark attendees of meetings as there or not there.
I'd question any scenario that required it to post >64k of data.
Jake
Nice stuff. Very useful...Innovative.
I noticed: When you place the focus on the "Status" field and press enter, nothing happens to update the selection.
Not too important, but it could be confusing for a user, wondering how to update the Status field.
Hi Jake - in this scenario I would agree, >64k is worrisome of the quality of the design, or user experience.
Don't forget how big a WSDL or SOAP message can become though. If I expect Domino to be able to receive message structure and content, it ought be able to handle >64k Post without the class I sent you the other day.
Ok not too sure about this, and yes the code needs refactoring, but if you post namevalue pairs to an agent for processing, loop through the request_content_xxx fields and append them to a stream... Streams don't seem to have the 64k limit, only fields, then process the stream as you would the field...
If Contextdoc.HasItem("REQUEST_CONTENT") Then
fieldname = "REQUEST_CONTENT"
NameValuePairs = Split(Contextdoc.GetItemValue( fieldname)(0),"&")
Forall nv In NameValuePairs
fieldname = Strleft(nv,"=")
fieldvalue = Strright(nv,"=")
Call stream.WriteText( "&" + fieldname +"="+ Trim(fieldvalue) , 5)
End Forall
Call contextdoc.RemoveItem(fieldname)
Else
For x = 0 To 9
If Contextdoc.HasItem("REQUEST_CONTENT_00"& x) Then
fieldname = "REQUEST_CONTENT_00"& x
NameValuePairs = Split(Contextdoc.GetItemValue( fieldname)(0),"&")
Forall nv In NameValuePairs
fieldname = Strleft(nv,"=")
fieldvalue = Strright(nv,"=")
Call stream.WriteText( "&" + fieldname +"="+ fieldvalue , 5)
End Forall
Call contextdoc.RemoveItem(fieldname)
End If
Next
End If
' Print "Bytes = " & stream.Bytes
stream.Position = 0
Do
buffer = Stream.ReadText(1)
' Print (buffer)
NameValuePairs = Split(buffer,"&")
Forall nv In NameValuePairs
fieldname = Strleft(nv,"=")
fieldvalue = Strright(nv,"=")
If fieldname <> "" Then
Savedoc.ReplaceItemValue fieldname, fieldvalue
Set item2 = savedoc.GetFirstItem( fieldname )
If item2.ValueLength > 900 Then item2.IsSummary = False
End If
End Forall
Loop Until Stream.IsEOS
...just a thought..
Interesting idea Andy.
Does it assume though that the point at which Domino splits request_content in to request_content_000 and request_content_001 coincides with the end of the value part of the last pairing in the text? Does Domino do that? I don't know.
As far as I can see, it matter not as when we rebuild it in the stream we append each request_content_xxx so the continuation starts where it left off.
I have used the above code and it seems to do the job, I guess you could equally use a richtext field, or structure the variables as XML and do a DXL import on the stream ( have tried this in the past too )...
The main point was that it's only the fields that have the limits, not other data structures.
..food for thought..
cheers
Hi Jake, have you any idea when this will become ready for "prime time". It appears to be exactly the solution I am looking for!
Many thanks,
Stewart.
Hi Stewart. See the "dext" download in the Sandbox tab!
I modified this and it works awesome! I created a view (that has embedded HTML table attributes like <tr> <td>...) and then embedded that into a form. At the top of that form, just put this:
</form>
<form action="UpdateDocuments?OpenAgent" method="post">
<p><input type="submit" value="Update Documents"></p>
Then embed the view. Then make an agent that runs on "Schedule" but set it to "Never". Then this thing will work!
Thanks so much for the framework to get this started!
Here's the finished agent code:
Sub Initialize
On Error Goto ErroRoutine
'Dim doc As NotesDocument
Dim pairings As Variant
Dim split_pairings As Variant, split_name As Variant
Dim last_processed As String
Dim session As New NotesSession
Dim doc As NotesDocument
Dim editdoc As NotesDocument
Set doc = session.DocumentContext
Set db = session.CurrentDatabase
If doc.HasItem("REQUEST_CONTENT") Then
pairings = Evaluate(|@Explode(REQUEST_CONTENT; "&")|, doc)
'loop all the name/value pairings submitted by the form
Forall v In pairings
'Need to replace + for " " at this point! before it's decoded!
split_pairings = Evaluate(|@URLDecode("Domino"; @ReplaceSubstring(@Explode("|+v+|"; "="); "+"; " "))|)
split_name = Evaluate(|@Explode(@ReplaceSubstring("|+split_pairings(0)+|";"]";"");"[")|)
If Ubound(split_name)>0 Then 'we have something to process (that is, a special bracketed [] field)
'save the last processed document if not same as this one!
If split_name(1) <> last_processed And last_processed<>"" Then
' Call editdoc.Save( True, True)
End If
'get the next document to process
Set editdoc = db.GetDocumentByUNID( split_name(1) )
If Not editdoc Is Nothing Then
If Ubound(split_pairings)>0 Then ' we have a value
Call editdoc.ReplaceItemValue(split_name(0), split_pairings(1))
Else 'blank
Call editdoc.ReplaceItemValue(split_name(0), "")
End If
' last_processed = editdoc.UniversalID
last_processed = split_name(1)
End If
End If
'Now save the last document processed before we left the loop
Call editdoc.Save( True, True)
' Print "Document saved"
End Forall
'Now save the last document processed before we left the loop
' Call doc.Save( True, True)
Else
Print "Web doc is not found!"
End If
'$$RETURN to where they came from
'Print "["+web.document.HTTP_Referer(0)+"]"
Print "["+doc.HTTP_Referer(0)+"]"
GracefulExit:
Exit Sub
ErroRoutine:
Print "Error" + Error + " on line " + Cstr(Erl)
Print split_name(0)
Print split_pairings(0)
Print split_name(1)
Print split_pairings(1)
Resume GracefulExit
End Sub