Creare e modificare più documenti sulla stessa pagina web

Una soluzione per gestire su un'unica Xpage le modifiche contemporanee a più documenti, evitando l'uso di molteplici Data Sources. Meglio di una vista con colonne editabili, estremamente flessibile ed efficace

Ottenere una pagina web totalmente dinamica, che permettesse di modificare i dati contenuti in più documenti Domino, con la stessa facilità d'uso offerta da un foglio elettronico. Con qualcosa in più. Questo era l'obiettivo che mi sono posto affrontando lo sviluppo di una nuova applicazione per un nostro cliente.

Voglio condividere la soluzione trovata perché, affrontando il problema, ho come sempre fatto una ricerca in rete, ma non ho trovato alcuna soluzione soddisfacente.
Fra i requisiti che mi sono imposto, c'erano quelli di non utilizzare né viste ne Data Sources; ciò perché le une e le altre avrebbero introdotto vincoli e rigidità che avrebbero limitato la qualità dell'applicazione. L'intera applicazione gira su un server Lotus Domino 8.5.2 ed è sviluppata, naturalmente, con tecnologia Xpages.

Soluzione:
La pagina in questione serve a preparare un modulo per l'esecuzione di una Survey relativa ad un sottoinsieme di prodotti inseriti in un listino.
La parte significativa della soluzione è racchiusa nel riquadro che costruisce dinamicamente il contenuto del questionario inviato agli utenti deputati a questa attività.

L'operatore, nella colonna sinistra può scorrere l'intero listino ed effettuare rapide ricerche. Quando seleziona un prodotto, il sistema crea un documento risposta, inizial-
mente composto da solo due campi: il codice e la descrizione del prodotto prescelto. Un refresh parziale della pagina provoca l'aggiornamento del riquadro in figura, costituito sostanzialmente da un repeat control che visualizza i documenti selezionandoli da un'apposita vista, categorizzata secondo il parentDocumentUnid.

La label codice nell'elenco prodotti, contiene nell'evento onClick il codice ServerSide per creare un nuovo documento:

var currDb:NotesDatabase session.getCurrentDatabase();
var docProfilo:NotesDocument currDb.getProfileDocument("(ProfiloDatabase)","");
var userName:NotesName session.createName(@UserName());
var searchView:NotesView currDb.getView("(embeddedSurveyItems)");

//se il documento principale non è ancora stato salvato, lo salvo tramite la funzione 'saveDraft()' memorizzata in una script library. Questa funzione non fa altro che creare un documento nel Db, impostando i campi di "testata" necessari 
if (!viewScope.selectedSurvey) {
  saveDraft();
}

var surveyDoc:NotesDocument currDb.getDocumentByUNID(viewScope.selectedSurvey);
//verifico che il prodotto selezionato non sia già stato inserito nella survey
var aKey new java.util.Vector ();
aKey.add(viewScope.selectedSurvey);
aKey.add(prodList.getDocument().getItemValueString("Codice"));
var doc:NotesDocument searchView.getDocumentByKey(aKey, true);
if (!doc){
  doc currDb.createDocument();
  doc.replaceItemValue("Form","SurveyItem");
  doc.replaceItemValue("c1",prodList.getDocument()
.getItemValueString("Codice"));
  doc.replaceItemValue("c2",prodList.getDocument()
.getItemValueString("Descrizione"));
  doc.replaceItemValue("prodUnid", prodList.getDocument().getUniversalID());

  //controllo accessi
  doc.replaceItemValue("Author", userName.getCanonical())
.setReaders(true);
  compilaAccessi (session,docProfilo,doc);

  //salvo il nuovo documento come risposta alla survey
  doc.makeResponse(surveyDoc);
  doc.save();
}else{
  return requestScope.errorMessage "Prodotto già inserito";
}

Il codice qui sopra controlla se il prodotto non è già stato inserito nella Survey, poi crea un documento SurveyItem e lo salva come risposta alla Survey
Esegue poi un refresh parziale della pagina (del riquadro destro della pagina, per aggiornare la lista dei prodotti visualizzati nel repeat control repeatProductList)

Il repeat control mostra i documenti ottenendo una ViewEntryCollection da una vista Notes, come facciamo di solito per mostrare le risposte al documento attivo.
if (viewScope.selectedSurvey){
try{
var searchView:NotesView database.getView("(embeddedSurveyItems)");
var entryColl:NotesViewEntryCollection searchView.getAllEntriesByKey(viewScope.selectedSurvey,true);
return entryColl;
}catch(e){
return null
}
}else{
return null
}
L'icona cestino, facendo riferimento al Collection Name, elimina dal database il documento corrispondente alla riga, utilizzando uno script server side sull'evento onClick (refresh parziale sul Panel che contiene il Repeat Control
var doc:NotesDocument itemSurvey.getDocument();
doc.removePermanently(true);

Per salvare le modifiche ai documenti, invece, ho utilizzato l'evento onChange di ciascun campo (con refresh parziale sul Panel contenente la riga corrente. Avrei anche potuto probabilmente non eseguire alcun refresh, devo provare). Poiché i campi si trovano all'interno di un repeat control, non ho modo di conoscere a priori il nome di ognuno; quindi, la funzione getComponent() è inutile. Utilizzando this.parent all'interno dell'evento onChange, riesco però ad ottenere un handle al campo cui l'evento stesso è associato.
Lo script (sempre server side) varia leggermente in funzione del tipo di campo:
Radio button:
var unid itemSurvey.getDocument().getUniversalID();
var doc:NotesDocument database.getDocumentByUNID(unid);
var newValue this.parent.value;
doc.replaceItemValue("c3",newValue);
doc.save(true,false,true);
Data:
var unid itemSurvey.getDocument().getUniversalID();
var doc:NotesDocument database.getDocumentByUNID(unid);

var newValue this.parent.value;
if (newValue ="" || newValue =null){
doc.replaceItemValue("c4","");
}else{
d1 session.createDateTime(newValue);
doc.replaceItemValue("c4",d1);
}

doc.save(true,false,true);
CheckBox Group:
var newValue this.parent.value;
var unid itemSurvey.getDocument().getUniversalID();
var doc:NotesDocument database.getDocumentByUNID(unid);
var s newValue.toString();
var myList s.split(",");
doc.replaceItemValue("c5",myList);
doc.save(true,false,true);

Fin qui tutto piuttosto normale.
Cosa succede però quando l'utente ritorna alla pagina aprendo una survey precedentemente salvata in bozza per completarne la compilazione? I campi nel repeat control dovrebbero ovviamente presentarsi compilati con i valor salvati in precedenza.
Questa operazione all'apparenza ovvia, si è rivelata tutt'altro che banale.
Inizialmente, non sapevo come fare. Né il default value, né il data binding funzionano in questo caso. Né altri posti dove inserire uno script potevano funzionare, perché non avevo modo di agganciare il nome di ciascun elemento da popolare (trattandosi, come detto in precedenza, di elementi contenuti all'interno di un repeat control).
Poi, ho scoperto l'esistenza dell'evento onComplete e mi sono accorto che questo evento esiste anche per tutti i tipi di campo!
Questo evento si attiva dopo che il campo è stato caricato nella pagina, ed agisce sul campo stesso. In questo modo, si ha la possibilità di avere accesso a ciascun campo indipendentemente dal numero di volte che lo stesso si ripete all'interno del repeat control.
Non è facilissimo trovare l'evento onComplete. Innanzitutto, bisogna selezionare nella Outline la riga "Event Handler" del campo interessato. Poi, scorrere verso il basso il pannello "All Properties" fino a trovare l'evento.



Inoltre, l'Event Handler appare solo se sul campo è stato utilizzato almeno uno degli eventi standard (onclick, onchange, onmouseover...). Osservate l'immagine qui sopra. Mentre per l'item Radio Button Group è visibile la riga Event Handler (ricordate lo script inserito più sopra sull'evento onchange?), per la Combo Box in alto non esiste Event Handler.
Lo script inserito nell'evento onComplete è di tipo Server Side. Anche in questo caso, il codice varia leggermente in funzione del tipo di campo
Radio button:
getComponent(this.parent.id).setValue(itemSurvey.getDocument()
.getItemValueString("c3"));
Data:
try{
 getComponent(this.parent.id).setValue(itemSurvey.getDocument().getItemValue("c4")
.elementAt(0).toJavaDate());
}catch(e){
}
CheckBox Group:
getComponent(this.parent.id).setValue(itemSurvey.getDocument()
.getItemValue("c5"));

In tutti e tre i casi lo script ottiene l'oggetto documento corrispondente alla riga del repeat control tramite il valore assegnato alla proprietà Collection name del repeat control (nel nostro caso "itemSurvey"). L'handle al campo da compilare, come in precedenza, lo si ottiene passando this.parent.id come parametro alla funzione getComponent().

Successivamente, ho usato la stessa tecnica per popolare il questionario utilizzato dagli utenti incaricati di svolgere la survey. In quel caso il codice è leggermente più complicato perché ha richiesto l'uso di un repeat control nidificato. Ma il risultato è stato altrettando soddisfacente.

Il funzionamento è stato verificato con Domino 8.5.2 e 8.5.3 e su diversi browser (Chrome, Firefox, Opera. Sia su Windows che su MAC).

Non ho ancora avuto il tempo di preparare un esempio da scaricare. Se vi interessa, scrivetemi (bmezzanotte@naes.it), vi manderò volentieri un nsf con i componenti necessari.

Bernardino

NAeS Consulting srl

via S.Senatore, 10
20122 Milano
p.IVA 04052980960

Informazioni commerciali

tel.: +39 02 45 48 72 63
fax: +39 02 70 04 66 85
e-mail: info@naes.it

Supporto tecnico

networking: support.networking@naes.it
software: suppsw@naes.it
help desk: https://helpdesk.naes.it