In questo paragrafo ripercorriamo i concetti illustrati nei paragrafi precedenti applicandoli ad un caso molto comune: la modifica di un documento tramite una videata di dettaglio. Nell’immagine seguente possiamo vedere una struttura di esempio.
La videata di dettaglio contiene i seguenti oggetti:
- Una proprietà di videata che referenzia il documento da modificare (in questo caso è la proprietà product).
- L’evento onLoad che permette di leggere il prodotto da modificare e di inizializzare la datamap.
- Gli elementi visuali che servono per modificare i dati (in questo caso tre IonInput e un IonCheckbox). Tali elementi sono collegati alle relative proprietà del documento Product tramite la datamap.
- La datamap che permette di visualizzare e modificare il documento. In questo caso si chiama dmProdotto; si noti che nelle proprietà viene impostato il Tipo contenuto ma non viene indicato alcun Template di riga.
- I pulsanti nell’intestazione per salvare le modifiche o cancellare il documento.
Per attivare il dettaglio, la videata ListaProdotti intercetta il clic su una riga della lista e apre la videata di dettaglio. Si noti che per navigare fra le videate viene usato App.Pages, un oggetto NavigationController che verrà illustrato nel capitolo relativo al framework IonicUI.
Il codice dell’evento onClick sull’item della lista è quindi il seguente:
$item.onClick = function (event)
{
App.Pages.push(app, App.DettaglioProdotto, {doc : this.row.document});
};
Possiamo notare che fra le opzioni per la nuova videata viene passata anche la referenza al documento Product collegato alla riga della lista che viene cliccata (this.row.document). L’effetto è il seguente:
Il codice da scrivere per ottenere questo risultato è veramente limitato. Nell’evento onLoad della videata, viene semplicemente collegato il documento passato dalla lista alla datamap dmProdotto.
App.DettaglioProdotto.prototype.onLoad = function (options)
{
view.product = options.doc;
$dmProdotto.document = view.product;
$title.innerText = view.product.ProductName;
};
È importante ricordare che per ottenere la visualizzazione dei dati del prodotto è necessario collegare a design time le proprietà della datamap agli elementi visuali corrispondenti tramite la Sorgente dati, come vediamo nell’immagine seguente relativa al nome del prodotto.
Inoltre notiamo che siccome la datamap non ha un template, essa non effettua alcuna operazione di clonazione per la generazione delle righe, ma semplicemente opera sugli elementi già esistenti nella videata. Ecco perché una datamap senza template è la struttura adatta ad una videata di dettaglio.
Vediamo adesso il codice dei pulsanti di conferma modifiche e di cancellazione:
$btnSave.onClick = function (event)
{
let b = yield view.product.save();
if (b) {
App.Pages.pop(app);
}
};
Si può notare che viene salvato direttamente il documento product invece che la datamap. Avremmo potuto salvare i dati anche scrivendo $dmProdotto.save(), ma abbiamo scelto una via più diretta. Se il salvataggio ha successo, la videata viene chiusa e si ritorna alla lista.
Vediamo ora il codice per cancellare il prodotto:
$btnDelete.onClick = function (event)
{
view.product.deleted = true;
yield $btnSave.onClick();
};
Esso viene marcato per la cancellazione e poi si procede come nel caso di salvataggio delle modifiche.
Si noti che il codice mostrato è solo esemplificativo. Nei casi reali occorre chiedere conferma e gestire eventuali errori di salvataggio o cancellazione.
Provando la videata a runtime è interessante notare che modificando il nome del prodotto nella videata di dettaglio, tale modifica viene riflessa automaticamente anche nella videata in lista, in quanto la stessa istanza del documento prodotto è parte della sorgente dati di entrambe le datamap, quella della lista (dmProdotti) e quella del dettaglio (dmProdotto). Se quindi una delle due datamap ne modifica le proprietà, anche l’altra se ne accorge e sincronizza la corrispondente parte visuale in autonomia.
Gestione delle modifiche pendenti #
In queste videate può essere interessante controllare il pulsante back che permette di ritornare alla lista, presente nella videata al momento della creazione della pagina. Se infatti l’utente modifica i dati del prodotto e poi torna indietro senza prima confermare le modifiche, è opportuno avvertire che esse verranno perse, e poi effettivamente annullarle prima di tornare alla lista.
Il codice da utilizzare è il seguente:
$navbar.onBackButton = function (event)
{
if (view.product.isModified()) {
let b = yield app.confirm("Tornando alla lista perderai le modifiche.
Vuoi continuare?");
if (!b)
return;
view.product.restoreOriginal();
}
App.Pages.pop(app);
};
L’evento onBackButton viene notificato quando l’utente preme il pulsante back nell’intestazione. Nell’implementazione mostrata, viene controllato se il documento è stato modificato e in tal caso si chiede conferma all’utente se vuole veramente uscire. Se l’utente risponde “No”, l’evento termina senza chiudere la videata. Se invece l’utente conferma l’uscita, le modifiche al documento vengono annullate tramite il metodo restoreOriginal e poi la videata di dettaglio viene chiusa.
Gestione dello stato dei pulsanti dell’intestazione #
Un’ulteriore caratteristica che può migliorare l’esperienza utente è quella di visualizzare i pulsanti dell’intestazione in funzione dello stato del documento. In particolare, il pulsante di salvataggio può apparire solo se il documento è stato modificato, mentre quello di cancellazione solo se il documento non è stato modificato.
Per ottenere questo comportamento possiamo implementare l’evento onDataChange, che viene notificato ogni volta che cambia il contenuto della sorgente dati della datamap.
$dmProdotto.onDataChange = function ()
{
let mod = view.product.isModified();
$btnSave.visible = mod;
$btnDelete.visible = !mod;
};
Selezione di dati provenienti da altre tabelle #
Vediamo infine come implementare elementi visuali per la modifica dei dati quando essi sono basati su dati provenienti da altre tabelle. Ad esempio, possiamo implementare la selezione della categoria del prodotto tramite un elemento di tipo IonSelect.
Nell’immagine possiamo notare che l’elemento select, di tipo IonSelect, ha come Sorgente dati la proprietà CategoryID del prodotto; inoltre esso contiene una datamap chiamata vlCategory, acronimo di lista valori delle categorie. Le uniche proprietà impostate di questa datamap sono AutoLoad a true e Tipo contenuto al documento Category.
Vediamo quindi un terzo tipo di utilizzo delle datamap: esse possono fungere da sorgente dati per un determinato elemento visuale. Possiamo riconoscere questa modalità di funzionamento dal fatto che esse non compaiono al primo livello, subito sotto la videata, ma all’interno dell’elemento che devono servire.
Quando la videata si apre, la datamap vlCategory effettua il caricamento dei suoi dati e poi li invia come lista valori alla IonSelect che li utilizzerà per decodificare il valore di CategoryID del prodotto in fase di modifica, e anche per consentire all’utente di scegliere un’altra categoria.
Possiamo vedere il risultato nell’immagine seguente:
Notiamo che questo è un modo semplice di ottenere il risultato, ma è utilizzabile solo in alcuni casi. Infatti la datamap vlCategory carica i dati di tutte le categorie, che in questo caso sono solo 9 e quindi è accettabile. Inoltre non carica solo i campi CategoryID e CategoryName, ma tutte le colonne della tabella Categories. Un modo più performante può essere quello di utilizzare una query strutturata al posto di indicare il documento nella proprietà Tipo contenuto della datamap.
Possiamo notare che è possibile controllare le datamap usate come lista valori come tutte le altre datamap. Se, ad esempio, dovessimo implementare una select la cui lista valori dipende da quello che viene selezionato in un’altra select, non dovremmo fare altro che implementare l’evento onChange della seconda select e in esso ricaricare la datamap della prima select con i dati appropriati.
Uso di controlli di tipo autocomplete #
Una soluzione più adeguata al caso di selezione dati da una tabella molto grande si può ottenere utilizzando un elemento IonAutoComplete che contiene una datamap basata su una query strutturata dipendente dal valore dell’elemento stesso, come mostrato nell’immagine seguente.
Le proprietà della datamap sono AutoLoad impostata a true e maxRows impostata a 30. Al momento dell’apertura della videata la datamap esegue il caricamento e, siccome la query strutturata ha una dipendenza dal valore della proprietà value dell’elemento visuale autocomplete, essa installa un osservatore su tale proprietà: in questo modo tutte le volte che la proprietà cambia, la query verrà automaticamente eseguita.
La clausola where dipendente dal valore dell’autocomplete fa sì che la datamap esegua delle query mirate alla decodifica del valore della proprietà SupplierID caricando però solo i dati che servono. La query effettuata da vlSupplier all’apertura della videata è la seguente:
select SupplierID, CompanyName from Suppliers where SupplierID = 1 limit 30
È un caricamento con selezione per chiave primaria, accettabile anche se la tabella Suppliers avesse milioni di righe. Oltre alla visualizzazione della decodifica del valore, l’accoppiamento fra autocomplete e datamap attiva altre due modalità di funzionamento.
La prima modalità entra in gioco quando l’utente clicca sull’autocomplete se essa ha la proprietà OpenOnFocus attivata. In tal caso, infatti, l’utente si aspetta di vedere tutti i fornitori dell’archivio. Questo non è possibile se essi sono migliaia, ma comunque viene caricata la lista dei primi 30 in modo tale da soddisfare le aspettative dell’utente. Al momento del clic sull’autocomplete, la query eseguita è quindi la seguente:
select SupplierID, CompanyName from Suppliers limit 30
Quando l’utente comincia a scrivere nella parte di input dell’autocomplete, si attiva una seconda modalità di funzionamento nella quale la datamap cerca i record richiesti dall’utente eseguendo una serie di query sulla tabella Suppliers fino a trovare almeno un record. Scrivendo, ad esempio, la lettera E, la datamap esegue le seguenti query:
select SupplierID, CompanyName from Suppliers where (CompanyName = 'e') limit 30
select SupplierID, CompanyName from Suppliers where (CompanyName ILIKE 'e%') limit 30
Dopo la seconda query non ne vengono eseguite altre perché vengono trovati dei dati. In caso contrario la datamap continuerebbe ad eseguire altre query utilizzando operatori più inclusivi, come ad esempio:
CompanyName ILIKE '%e%'.
È possibile controllare da codice questo procedimento implementando l’evento onSearch della datamap oppure tramite l’evento onFilter dell’elemento IonAutoComplete.
Si noti infine che la query della datamap può contenere altre clausole where, sia collegate ad elementi visuali che ad altre proprietà della videata. In questo modo è possibile implementare elementi di ricerca basati su dati contenuti in altri elementi o comunque in un contesto specifico.
Uso di datamap basate su documenti e controlli di tipo autocomplete #
Nel paragrafo precedente abbiamo visto l’utilizzo di una datamap basata su query come sorgente di valori per un elemento IonAutoComplete: se la query contiene una clausola where collegata all’elemento, si attiva una modalità efficiente di ricerca dei possibili valori.
Ci sono casi in cui è preferibile ottenere lo stesso comportamento usando una datamap basata su documenti, ad esempio quando si sta sviluppando un’applicazione locale basata sulla Document Orientation Remota, come illustrato nel libro relativo alla Sincronizzazione.
Per ottenere lo stesso comportamento del caso precedente usando i documenti, si inizia specificando il documento da selezionare nella proprietà Tipo contenuto della datamap. Siccome le datamap basate su documenti non hanno una query in cui inserire clausole where, al loro posto occorre scrivere la seguente riga nell’evento onLoad della videata:
$vlSupplier.addDOFilter("SupplierID", $autocomplete, "value");
Il metodo addDOFilter aggiunge un criterio di filtro alla datamap basata su documenti, specificando che il valore del filtro deve essere dinamicamente reperito dalla proprietà value dell’elemento $autocomplete. Così la datamap si comporta come nel caso precedente.
Una seconda modifica riguarda le proprietà del documento contenute nella cartella Properties nella datamap: siccome essa è basata su un documento, la cartella ne contiene tutte le proprietà. È importante eliminare tutte le proprietà a parte quella che fornisce il valore all’autocomplete, in questo caso SupplierID, e quella che deve essere usata come decodifica, in questo caso CompanyName. La datamap, infatti, utilizza questi riferimenti per scegliere quali dati comunicare all’autocomplete.
Anche in questo caso è possibile aggiungere alla datamap diversi filtri, sia verso altri elementi che verso proprietà della videata, chiamando più volte il metodo addDOFilter.
Selezione di dati provenienti da altre tabelle in layout lista #
Le stesse tecniche viste finora per una videata di dettaglio possono essere utilizzate anche quando il controllo IonAutoComplete è contenuto in una lista. L’unica differenza è che se la datamap è basata su una query, la clausola where non può referenziare direttamente $autocomplete perché tutte le righe punterebbero sempre allo stesso elemento, cioè quello del template. Al posto di $autocomplete.value è possibile scrivere this.parentElement.value, dove this è la datamap e la proprietà parentElement si riferisce all’elemento che la contiene. In questo modo la datamap di ogni riga punta al proprio elemento.
Se la datamap è basata su documenti, la modifica riguarda la riga di codice da inserire nell’evento onLoad che diventa:
$vlSupplier.addDOFilter("SupplierID", $vlSupplier.parentElement, "value");