Nun das ist viel klarer:
HINWEIS: UNTEN IST DAS WEITERGEMACHT.
<html>
<head>
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript">
function processTransferin() {
var valTransferin = $("transferin").childNodes[0].nodeValue;
arrFieldNameValues = valTransferin.split("~~~");
var data = new Object();
var iterator = function(value, index) {
//alert("element " + index + " is " + value);
arrNameValue = value.split("°=°");
if (arrNameValue[0] && arrNameValue.length > 1) {
if (arrNameValue[1].indexOf("µ")) {
arrNameValue[1] = arrNameValue[1].split("µ");
}
data[arrNameValue[0]] = arrNameValue[1];
}
}
arrFieldNameValues.each(iterator);
for (name in data) {
var value = data[name];
if ($(name)) {
var txtNode = document.createTextNode(value);
$(name).appendChild(txtNode);
//$(name).innerHTML = value;
}
}
}
</script>
</head>
<body onload="processTransferin();">
<!-- <body> -->
<div id="transferin">Category°=°Muppets Show~~~RequestAuthorsMailRecipients°=°CN=Kermit der Frosch/O=ozzµCN=/OU=Miss Piggy/O=ozz~~~RequestAuthors°=°CN=Waldorf/O=ozz~~~RequestReaders°=°CN=Statler/O=OzzµCN=Fozzy Bär/O=ozzµCN=Gonzo/O=ozz</div>
<hr />
<div>category:<span id="Category"/></div>
<div>requestauthormailrecipients:<span id="RequestAuthorsMailRecipients"/></div>
<div>requestauthors:<span id="RequestAuthors"></span></div>
<div>requestreaders:<span id="RequestReaders"></span></div>
</body>
</html>
Nur noch transferin wird als berechneter Text vom Server geholt. Die restlichen "Felder" werden auf dem Client und nicht mehr von Domino errechnet. Der Wert der Felder steht in den im Html leeren Span-Tags. Sie werden im onload event der Seite errechnet.
In der realen Anwendung steht der Inhalt von transferin in einem Feld von Category-Dokumenten. Will ich nun über AJAX die Kategorie ändern, muß ich nicht mehr die ganze Seite berechnen. Ich muß mir über ajax nur noch den neuen Wert von transferin besorgen und die JavaScript-Funktion processTransferin() aufrufen. Die Felder werden dann automatisch gesetzt.
Zur Zeit ist das weniger Prototype/Scriptaculous und mehr POJS (Plain Old JavaScript). Die Funktion $ (in $("transferin") ist Prototype. Ausserdem noch dieses var iterator Konstrukt. Man kann den Code
- so kopieren
- in eine html Datei pasten
- prototype.js aus dem Internet runterladen (try google).
- prototype.js in das selbe Verzeichnis wie die html Datei packen.
- html Datei mit dem browser öffnen.
Und jetzt mit Ajax. Zugegeben ist das erstmal JSP/Servlet, weil das mit der Debug Unterstützung in Netbeans einfacher ist als in Domino.
Muß also die Ajax Komponente noch in LotusScript transkribieren und die Businesslogik da reinbringen. Kommt morgen ;)
Hier also die JSP und das Servlet. Das Servlet wird per Ajax aus dem von der JSP generierten html aufgerufen. Und da beginnt Prototype zu glänzen. Wobei ichs noch nicht richtig beherrsche. Z.B. trau ich mich noch nicht richtig an die closures ran.
Die im Ajax ausgetauschten Daten sind in JSON. JSON ist gut.
JSP:
<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<%--
The taglib directive below imports the JSTL library. If you uncomment it,
you must also add the JSTL library to the project. The Add Library... action
on Libraries node in Projects view can be used to add the JSTL 1.1 library.
--%>
<%--
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
--%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JSP Page</title>
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript">
var ui = {}; // context stuff.
function processTransferin() {
ui.transferin = $("transferin");
//var valTransferin = $("transferin").childNodes[0].nodeValue;
var valTransferin = $("transferin").innerHTML;
arrFieldNameValues = valTransferin.split("~~~");
var data = new Object();
var iterator = function(value, index) {
//alert("element " + index + " is " + value);
arrNameValue = value.split("°=°");
if (arrNameValue[0] && arrNameValue.length > 1) {
if (arrNameValue[1].indexOf("µ")) {
arrNameValue[1] = arrNameValue[1].split("µ");
}
data[arrNameValue[0]] = arrNameValue[1];
}
}
arrFieldNameValues.each(iterator);
for (name in data) {
var value = data[name];
if ($(name)) {
//var txtNode = document.createTextNode(value);
//$(name).appendChild(txtNode);
$(name).innerHTML = value;
}
}
}
function changeCat(newCat) {
// DER AJAX Code.
var request = new Ajax.Request(
"AjaxServlet",
{
method: 'get',
parameters: "cat=" + newCat,
onComplete: parseTransferin,
onFailure: showAjaxError
}
);
}
// CALLBACK METHODE von ajax call
function parseTransferin(transport) {
var response = transport.responseText;
var jsonObj = eval("(" + response + ")");
//$(transferin).innerHTML = jsonObj.transferout;
//var children = $A(ui.transferin.childNodes);
//children.each (
// function(child){
// alert (child.text);
//}
//);
ui.transferin.innerHTML = jsonObj.transferout;
processTransferin();
//alert ("transferout=" + jsonObj.transferout + "\nunid=" + jsonObj.unid);
}
// NOCH NE CALLBACK METHODE VON AJAX.
function showAjaxError(request) {
alert("error");
}
</script>
</head>
<body onload="processTransferin();">
<div id="transferin">Category°=°Muppets Show~~~RequestAuthorsMailRecipients°=°CN=Kermit der Frosch/O=ozzµCN=/OU=Miss Piggy/O=ozz~~~RequestAuthors°=°CN=Waldorf/O=ozz~~~RequestReaders°=°CN=Statler/O=OzzµCN=Fozzy Bär/O=ozzµCN=Gonzo/O=ozz</div>
<hr />
<div>category:<span id="Category"/></div>
<div>requestauthormailrecipients:<span id="RequestAuthorsMailRecipients"/></div>
<div>requestauthors:<span id="RequestAuthors"></span></div>
<div>requestreaders:<span id="RequestReaders"></span></div>
<form>
<input name="buttonChangeCat" type="button" value="buttonChangeCat" onclick="changeCat()"></button>
<!-- <input name="buttonChangeCat" type="button" value="buttonChangeCat" onclick="processTransferin()"/></button>-->
</form>
</body>
</html>
SERVLET:
package ajax;
import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;
/**
*
* @author ajanssen
* @version
*/
public class AjaxServlet extends HttpServlet {
/** Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* @param request servlet request
* @param response servlet response
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/javascript;charset=UTF-8");
PrintWriter out = response.getWriter();
System.out.println("servlet run");
out.println("{");
out.println("transferout:\"Category°=°Presidentes~~~RequestAuthorsMailRecipients°=°CN=Michelle Bachelet/O=chileµCN=Ricardo Lagos/O=chile~~~RequestAuthors°=°CN=Pedro I/O=Brasil~~~RequestReaders°=°CN=Alan Garcia/O=PeruµCN=Irrigoyen/O=ArgentinienµCN=de Lozada/O=Bolivien\",");
out.println("unid:111222333");
out.println("}");
out.close();
}
/** Returns a short description of the servlet.
*/
public String getServletInfo() {
return "Short description";
}
// </editor-fold>
}
Der Effekt ist jedenfalls, dass man auch bei Abhängigkeiten mehrerer Felder von einem "Transferfeld" (typisches Domino Idiom, ihr kennt es alle) Ajax angewendet werden kann.
Nun denn.
Ajax ist zwar eigentlich tendentiell eine Clienttechnologie, aber es redet eben auch mit dem Server.
Zwar wird der Server NICHT veranlaßt eine ganz neue Seite zu liefern, aber Informationen vom Server können mit Ajax abgefragt werden. Diese Informationen vom Server werden dann in die Webseite "reingemischt".
Oben ist diese Serverseitige Komponente als Servlet implementiert.
Wie kann ich das als Notes Agenten programmieren?
Erste konkrete Frage: Wie soll der Agent überhaupt angesprochen werden?
Antwort: so:
var request = new Ajax.Request(
"AjaxServlet",
{
method: 'get',
parameters: "cat=" + newCat,
onComplete: parseTransferin,
onFailure: showAjaxError
}
);
1. "AjaxServlet" ist die URL zu der Resource.
In Domino sollte das so aussehen:
"/<dbPath>/<agentName>?openAgent"
Wie geht der Path?
Das ist einfach eine JavaScript Funktion im HtmlHead Feld der Maske:
"<script>" + @NewLine +
"function getBaseURL() {" + @NewLine +
"return \"/" + @WebDbName + "\";" + @NewLine +
"}" + @NewLine +
"</script>" + @NewLine +
@NewLine +
2. method: ist die http-Methode. Ich glaub bei Notes-Agenten müsste es 'post' heissen.
3. parameters: sind die url parameters. Das kann so bleiben. Wie das im Agenten ausgelesen wird, kommt später.
4. onComplete: ist die Methode, die im javaScript aufgerufen wird, wenn der Server etwas mit HTTP-State 2xx zurückliefert (wenn der Agent also erfolgreich etwas zurückliefert). Das kann so bleiben.
5. Auch onFailure ist rein JavaScript intern, kann so bleiben.
Der Ajax Aufruf hat sich also gegen das höchst wissenschaftliche Servlet kaum geändert:
var baseURL = getBaseURL();
var request = new Ajax.Request(
baseURL + "/AjaxController?openAgent,
{
method: 'post',
parameters: "cat=" + newCat,
onComplete: parseTransferin,
onFailure: showAjaxError
}
);
Der Agent heisst AjaxController. Den mach ich als nächstes.
Hab bei der Entwicklung des serverseitigen Teils gemerkt, dass ich 2 Parameter beim Client vergessen habe. Einer ist sehr wichtig für Model View Controller (MVC).
"AjaxServlet",
{
method: 'get',
parameters: "action=" + action + " unid=" + unid + " cat=" + newCat,
onComplete: parseTransferin,
onFailure: showAjaxError
}
Vor allem das action= ist wichtig. Das MVC soll bewirken, dass ALLE Ajax Requests gegen eine serverseitige Anwendung von ein und demselben Agenten verarbeitet werden. Sonst müllt man die Anwendung mit Tonnen von kleinen Agenten voll.
Gut. Was passiert jetzt auf der Serverseite.
1. Die Serverseite nimmt eine Anfrage aus dem JavaScript entgegen.
2. verarbeitet diese
3. Schickt Daten im JSON Format zurück. XML ginge auch, aber JSON ist von JavaScript einfacher zu verarbeiten.
Wie verarbeitet der Notes Agent die Anfrage?
Das folgende hat übrigens noch kein fertiges Fehlerhandling.
Die Parameter werden als CGI Variablen an die URL des Agenten gehängt. Darum kümmert sich Prototype automatisch, wenn man parameters: wie oben schreibt.
Ein Agent sollte das so auslesen können:
Set docContext = s.DocumentContext
queryStr = docContext.getItemValue("QUERY_STRING_DECODED")
ist halt ne CGI Variable.
In queryStr sollte dann sowas stehen wie:
"?openAgent&action=changeCategory&cat=blueChips&unid_doc_cur=9093458034853045"
Die einzelnen name=wert Paare zwischen den & lassen sich durch diese einfache Funktion in eine leicht zu handhabende List-Struktur bringen:
Function webParamsAsList (inVal As String) As Variant
Dim firstAmper As Integer
Dim ret List As String
Dim arrNameValuePairs As Variant
Dim elemNameValuePair As String
Dim arrNameValuePair As Variant
Dim i As Integer
firstAmper = Instr(inVal, "&")
inVal = Right$(inVal, Len(inVal) - firstAmper)
arrNameValuePairs = Split(inVal, "&")
For i = 0 To Ubound(arrNameValuePairs)
elemNameValuePair = arrNameValuePairs(i)
arrNameValuePair = Split(arrNameValuePairs(i), "=")
ret(Lcase(arrNameValuePair(0))) = Lcase(arrNameValuePair(1))
Next
webParamsAsList = ret
End Function
Nun kommt der Kontroller. Da wird einfach nur der action-Parameter ausgelesen und in die entsprechende Funktion zur Weiterverarbeitung aufgerufen.
webParams = webParamsAsList(inVal)
resp = ""
If Lcase(webParams("action")) = "changecategory" Then
resp = processChangeCategory(s, webParams)
End If
Am Ende soll dann nur noch etwas an den Browser zurückgeliefert werden. Kommt später.
Und das JSON Zeugs geht auf der Serverseite (also im lotusScript Agenten) so:
Man erzeugt aus den Variablen ein bischen JSON formatiertes Zeugs.
processChangeCategory = "{" & strNL &_
|"transferout":| & |"| & strTransferout & |",| & strNL &_
|"unid":| & |"| & strUnid & |"| & strNL &_
"}"
Und das gibt man dann über print aus. Wichtig ist, dass vorher noch das Encoding des über http zurückgesendeten Streams auf text/javascript stellt und wohl auch das encoding auf dem in der Webseite (wo das ajax clients Zeugs drin ist) übereinstimmt.
Für Encoding gibts für LotusScript im Gegensatz zu jeder mir bekannten Webprorammierumgebung keine Methode. Man muß das als erstes Print Statement mit 1 Leerzeile dadrunter ausgeben.
Print |Content-Type:text/javascript;charset=iso-8859-1| & Chr$(13) & Chr$(10) & Chr$(13) & Chr$(10)
Print respBody
(respBody entspricht der obigen Variable processChangeCategory)
Ich hatte ein bischen zu kämpfen mit dem Encoding. Er schien Probleme zu haben, als ich das erst auf UTF-8 stehen hatte. Domino gibt zwar standardmässig UTF-8 aus, in der Webseite stand aber ISO-8859-1. Kann das aber jetzt nicht mehr reproduzieren. Leicht mysteriös.
JSON läßt sich in JavaScript viel einfacher verarbeiten als xml. Das ist einfach nur ein eval statement und ich hab das Objekt.
function parseTransferin(transport) {
var response = transport.responseText;
var jsonObj = eval("(" + response + ")");
alert ("jsonObj.uind=" + jsonObj.unid)
alert("jsonObj.transferout=" + jsonObj.transferout)
//ui.transferin.innerHTML = jsonObj.transferout;
//processTransferin();
}
Fazit soweit:
Die Prototype Bibliothek bringt Vorteile, die ich noch gar nicht alle ausereizt habe.
JSON ist voll in Ordnung und sollte XML vorgezogen werden.
Ajax sind eigentlich so eine Art Webservice zwischen einer JavaScript Komponente im Browser und einer serverseitigen Komponente.
Als nächstes kommt Scriptaculous.
Auf Notes Seite gibts ja zur Zeit diese Erweiterung von ext zu ext.nd mit speziellen Domino Erweiterungen. Will ich mir auch noch anschauen, aber zu Prototype gibts einfach die beste Literatur (Prototype and Scriptaculous in Practice von Manning) und ich traue sowieso keinem Bibliothekenschreiber, der mir erzählt, dass mit seiner library alles transparent geregelt ist. Mehr als oft wichtig auch das unter der Haube zu verstehen.
Und jetzt hab ich noch was anderes gefixt:
Ein großes Problem von Domino Webanwendungen ist, dass der Code für ein feature sich leicht über verschiedene Stellen verstreut. Gerade die gerne verwendeten openSource DHTML widgets brauchen von sich schon 1 Stylesheet, 1 oder mehrere js Dateien und Initialisierungscode, der dann in Domino auch dadurch zur Zerstreuung neigt, da Leute das als passThru html in die Form posten, weil man da ja computed Text verwenden kann. Schwer zu maintainen.
Nun bestand mein bisheriger JavaScript Code auch aus mehreren Funktionen. Prototype unterstützt aber die Erstellung eigener Klassen, die in JavaScript-Pur recht hacklig ist. Jetzt der neue Code. Die Funktion processTransferin() hätte ich auch lieber in der Klasse. Ist mir aber zur Zeit noch nicht gelungen (ist nicht einfach).
var CatManager = Class.create();
CatManager.prototype= {
initialize:function() {
},
changeCategory: function() {
ui.transferin = $("transferin");
var indexSelected = document.forms[0].category.selectedIndex;
var newCat = document.forms[0].category[indexSelected].text;
var url = getBaseURL() + "/" + "AjaxController?openAgent"
var params = "action=changeCategory&cat=" + newCat + "&unid_doc_cur=" + $("unid").innerHTML;
var request = new Ajax.Request(
url,
{
method: 'get',
parameters: params,
onComplete: this.parseTransferin,
onFailure: this.showAjaxError
}
);
},
parseTransferin: function(transport) {
var response = transport.responseText;
var jsonObj = eval("(" + response + ")");
//ui.transferin.innerHTML = jsonObj.transferout;
$(transferin).innerHTML = jsonObj.transferout;
processTransferin(jsonObj.transferout);
}.bind(this),
showAjaxError: function(request) {
alert("error");
}
}
var instCatManager = new CatManager()
function processTransferin(valTransferin) {
//var valTransferin = $("transferin").innerHTML;
arrFieldNameValues = valTransferin.split("~~~");
var data = new Object();
var iterator = function(value, index) {
//alert("element " + index + " is " + value);
arrNameValue = value.split("°=°");
if (arrNameValue[0] && arrNameValue.length > 1) {
if (arrNameValue[1].indexOf("µ")) {
arrNameValue[1] = arrNameValue[1].split("µ");
}
data[arrNameValue[0]] = arrNameValue[1];
}
}
arrFieldNameValues.each(iterator);
for (name in data) {
var value = data[name];
if ($(name)) {
$(name).innerHTML = value;
}
}
};
Zumindest ist es nur 1 Klasse und 1 Funktion und beide können in den JSHeader.