SeamFramework.orgCommunity Documentation
<s:link>
e <s:button>
E' ora di capire il modello di conversazione di Seam con maggior dettaglio.
Storicamente la nozione di "conversazione" Seam si presenta come unificatrice di tre differenti idee:
L'idea di uno spazio di lavoro (workspace), che ho incontrato in un progetto per il governo Vittoriano nel 2002. In questo progetto fui obbligato ad implementare la gestione del workspace sopra Struts, un'esperienza che prego di non ripetere mai più.
L'idea di una transazione per l'applicazione con semantica ottimista, e il convincimento che i framework esistenti basati su un'architettura stateless non potessero fornire una gestione efficiente dei contesti di persistenza estesa. (La squadra di Hibernate è veramente stanca di sentire lamentele per le LazyInitializationException
, che non è in verità colpa di Hibernate, ma piuttosto colpa di un modello di contesto di persistenza estremamente limitato delle architetture stateless come il framework Spring o il tradizionale (anti)pattern stateless session facade in J2EE.)
L'idea di un task a workflow.
Unificando queste idee e fornendo un supporto profondo nel framework, si ha un costrutto potente che consente di creare applicazioni più ricche e più efficienti con meno codice di prima.
Gli esempi visti finora usano un modello di conversazione molto semplice che segue queste regole:
C'è sempre un contesto di conversazione attivo durante le fasi di apply request values, process validations, update model values, invoke application e render response del ciclo di vita della richiesta JSF.
Alla fine della fase restore view del ciclo di vita della richiesta JSF, Seam tenta di ripristinare ogni precedente contesto di conversazione long-running. Se non ne esiste nessuno, Seam crea un nuovo contesto di conversazione temporanea.
Quando viene incontrato un metodo @Begin
, il contesto della conversazione temporanea è promosso a conversazione long-running.
Quando viene incontrato un metodo @End
, il contesto della conversazione long-running è ridotto a conversazione temporanea.
Alla fine della fase di render response del ciclo di vita della richiesta JSF, Seam memorizza i contenuti del contesto di conversazione long-running o distrugge i contenuti del contesto di conversazione temporanea.
Qualsiasi richiesta faces (un postback JSF) si propagherà nel contesto della conversazione. Di default le richieste non-faces (richieste GET, per esempio) non si propagano nel contesto di conversazione, si veda sotto per maggiori informazioni.
Se il ciclo di vita della richiesta JSF viene accorciato da un redirect, Seam memorizza e ripristina in modo trasparente il contesto dell'attuale conversazione — amenoché la conversazione sia già stata terminata tramite @End(beforeRedirect=true)
.
Seam propaga in modo trasparente il contesto della conversazione (includendo il contesto della conversazione temporanea) lungo i postback JSF e i redirect. Se non si fa niente di speciale, una richiesta non-faces (per esempio una richiesta GET) non verrà propagata nel contesto di conversazione e non verrà processata in una conversazione temporanea. Questo è solitamente - ma non sempre - il comportamento desiderato.
Se si vuole propagare una conversazione Seam lungo una richiesta non-faces, non occorre esplicitamente codificare l'id della conversazione come parametro di richiesta:
<a href="main.jsf?#{manager.conversationIdParameter}=#{conversation.id}"
>Continue</a
>
O in stile più JSF:
<h:outputLink value="main.jsf">
<f:param name="#{manager.conversationIdParameter}" value="#{conversation.id}"/>
<h:outputText value="Continue"/>
</h:outputLink
>
Se si utilizza la libreria di tag Seam, questo è l'equivalente:
<h:outputLink value="main.jsf">
<s:conversationId/>
<h:outputText value="Continue"/>
</h:outputLink
>
Se si desidera disabilitare la propagazione del contesto di conversazione per il postback, viene usato un simile trucchetto:
<h:commandLink action="main" value="Exit">
<f:param name="conversationPropagation" value="none"/>
</h:commandLink
>
Se si utilizza la libreria di tag Seam, questo è l'equivalente:
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="none"/>
</h:commandLink
>
Si noti che disabilitando la propagazione del contesto della conversazione non è assolutamente la stessa cosa che terminare la conversazione:
Il parametro di richiesta conversationPropagation
, o il tag <s:conversationPropagation>
possono anche essere usati per iniziare e terminare una conversazione, distruggere l'intero stack di conversazione, o iniziare una conversazione innestata.
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="end"/>
</h:commandLink
>
<h:commandLink action="main" value="Exit">
<s:conversationPropagation type="endRoot"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Child">
<s:conversationPropagation type="nested"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="begin"/>
</h:commandLink
>
<h:commandLink action="main" value="Select Hotel">
<s:conversationPropagation type="join"/>
</h:commandLink
>
Il modello di conversazione rende semplice costruire applicazioni che si comportano in modo corretto in presenza di operazioni con finestre multiple. Per molte applicazioni, questo è quello che serve. Alcune applicazioni complesse hanno uno ed entrambi dei seguenti requisiti aggiuntivi:
Una conversazione propaga diverse unità più piccole di interazione utente, che vengono eseguite in modo seriale o anche concorrente. Le più piccole conversazioni innestate hanno il proprio stato isolato di conversazione, ed hanno anche accesso allo stato della conversazione più esterna.
L'utente è in grado di passare tra più conversazioni dentro la stessa finestra del browser. Questa caratteristica è chiamata gestione del workspace.
Una conversazione innestata viene creata invocando un metodo marcato con @Begin(nested=true)
dentro lo scope di una conversazione esitente. Una conversazione innestata ha un proprio contesto di conversazione, ma può leggere i valori dal contesto della conversazione più esterna. Il contesto della conversazione più esterna è in sola lettura in una conversazione innestata, ma poiché gli oggetti sono ottenuti per riferimento, i cambiamenti agli oggetti stessi si rifletteranno nel contesto più esterno.
Innestando una conversazione si inizializza un contesto che viene messo sullo stack del contesto della conversazione originale, o più esterna. La conversazione più esterna è considerata padre.
Ogni valore messo in outjection o direttamente impostato nel contesto della conversazione innestata non influenza gli oggetti accessibili nel contesto della conversazione padre.
L'iniezione o la ricerca nel contesto da un contesto di conversazione per prima cosa cercherà il valore nell'attuale contesto e, se non viene trovato alcun valore, procederà lungo lo stack della conversazione se questa è innestata. Come si vedrà, questo comportamento può essere ridefinito.
Quando si incontra una @End
, la conversazione innestata verrà distrutta, togliendola dallo stack (pop), e la conversazione più esterna verrà ripristinata. Le conversazioni possono essere annidate con gradi di profondità arbitrari.
Certe attività utente (gestione del workspace, o il pulsante indietro) possono causare il ripristino della conversazione più esterna prima che venga terminata la conversazione innestata. In questo caso è possibile avere più conversazioni innestate concorrenti che appartengono alla stessa conversazione più esterna. Se la conversazione più esterna finisce prima che termini la conversazione innestata, Seam distrugge tutti i contesti delle conversazioni innestate assieme a quella più esterna.
La conversazione alla fine dello stack delle conversazioni è la conversazione radice. Distruggendo questa conversazione si distruggono sempre tutti i suoi discendenti. Si può ottenere questo in modo dichiarativo specificando @End(root=true)
.
Una conversazione può essere pensata come uno stato continuo. Le conversazioni innestate consentono all'applicazione di catturare lo stato continuo consistente in vari punti durante l'interazione utente, quindi assicurando un comportamento corretto rispetto al pulsante indietro ed alla gestione del workspace.
Come menzionato in precedenza, se un componente si trova in una conversazione padre dell'attuale conversazione innestata, la conversazione innestata userà la stessa istanza. Occasionalmente, è utile avere diverse istanze in ciascuna conversazione innestata, cosicché l'istanza del componente che si trova nella conversazione padre sia invisibile alle sue conversazioni figlie. Si può ottenere questo comportamento annotando il componente @PerNestedConversation
.
JSF non definisce alcun tipo di action listener da lanciare quando una pagina viene acceduta tramite una richiesta non-faces (per esempio, una richiesta HTTP GET). Questo può succedere se l'utente memorizza la pagina come segnalibro, o se si naviga nella pagina tramite un <h:outputLink>
.
A volte si vuole immediatamente iniziare una conversazione all'accesso della pagina. Poiché non c'è alcun metodo d'azione JSF, non si può risolvere il problema nel consueto modo, annotando l'azione con @Begin
.
Sorge un altro problema se la pagina ha bisogno di recuperare uno stato da una variabile di contesto. Si sono già visti due modi per risolvere questo problema. Se lo stato è mantenuto in un componente Seam, si può recuperare lo stato in un metodo @Create
. Se non lo è, si può definire un metodo @Factory
per la variabile di contesto.
Se nessuna di queste opzioni funziona, Seam permette di definire una pagina d'azione nel file pages.xml
.
<pages>
<page view-id="/messageList.jsp" action="#{messageManager.list}"/>
...
</pages
>
Il metodo d'azione viene chiamato all'inizio della fase di render response, ogni volta che la pagina sta per essere generata. Se l'azione della pagina ritorna un esito non-null, Seam processerà ogni opportuna regola di JSF e Seam, e genererà un'altra pagina.
Se tutto ciò che si vuole fare prima di generare una pagina è iniziare una conversazione, si può utilizzare un metodo d'azione predefinito che fa questo:
<pages>
<page view-id="/messageList.jsp" action="#{conversation.begin}"/>
...
</pages
>
Si noti che si può chiamare quest'azione ridefinita da un controllo JSF, ed in modo simile si può usare #{conversation.end}
per terminare le conversazioni.
Se si vuole più controllo, per unirsi a conversazioni esistenti od iniziare una conversazione innestata, per iniziare un pageflow od una conversazione atomica, occorre usare l'elemento <begin-conversation>
.
<pages>
<page view-id="/messageList.jsp">
<begin-conversation nested="true" pageflow="AddItem"/>
<page>
...
</pages
>
C'è anche un elemento <end-conversation>
.
<pages>
<page view-id="/home.jsp">
<end-conversation/>
<page>
...
</pages
>
Per risolvere il primo problema, si hanno cinque opzioni:
Annotare il metodo @Create
con @Begin
Annotare il metodo @Factory
con @Begin
Annotare il metodo d'azione della pagina Seam con @Begin
Usare <begin-conversation>
in pages.xml
.
Usare #{conversation.begin}
come metodo d'azione della pagina Seam
Certe pagine sono rilevanti solo nel contesto di conversazione long-running. Un modo per "proteggere" tale pagina è richiedere una conversazione long-running come prerequisito per renderizzare la pagina. Fortunatamente, Seam ha un meccanismo predefinito per forzare questa richiesta.
Nel descrittore di pagina di Seam si può indicare che la conversazione corrente sia long-running (o innestata) come requisito per poter renderizzare la pagina, usando l'attributo conversation-required
come mostrato:
<page view-id="/book.xhtml" conversation-required="true"/>
L'unico inconveniente è che non c'è alcun modo predefinito per indicare quale conversazione long-running sia richiesta. Si può costruire un'autorizzazione base controllando se è presente un valore specifico nella conversazione dentro l'azione di pagina.
Quando Seam determina che questa pagina è richiesta fuori da una conversazione long-running, vengono intraprese le seguenti azioni:
Viene sollevato un evento contestuale chiamato org.jboss.seam.noConversation
Un messaggio di avviso di stato viene registrato usando la chiave di bundle org.jboss.seam.NoConversation
L'utente viene rediretto alla pagina alternativa, se definita
La pagina alternativa è definita nell'attributo no-conversation-view-id
in un elemento <pages>
nel descrittore di pagina Seam come mostrato:
<pages no-conversation-view-id="/main.xhtml"/>
Al momento si può solo definire una sola pagina per l'intera applicazione.
I comandi link JSF eseguono sempre un invio di form tramite JavaScript, che rompe le caratteristiche dei browser "Apri in nuova finestra" o "Apri in nuova scheda". Nel semplice JSF occorre usare un <h:outputLink>
se si vuole questa finzionalità. Ma ci sono due grandi limitazioni in <h:outputLink>
JSF non fornisce un modo per agganciare un action listener a <h:outputLink>
.
JSF non propaga la riga selezionata di un DataModel
poiché non c'è alcun invio di form.
Seam fornisce la nozione di pagina d'azione per aiutare a risolvere il primo problema, ma questo non aiuta per niente il secondo problema. Si può aggirare questo usando l'approccio RESTful di passare un parametro di richiesta e di riottenere l'oggetto selezionato lato server. In alcuni casi — come nell'esempio Seam del Blog — questo è il migliore approccio. Lo stile RESTful supporta i segnalibri, poiché non richiede uno stato lato server. In altri casi, dove non interessanno i segnalibri, l'uso di un @DataModel
e di @DataModelSelection
è conveniente e trasparente!
Per riempiere questa mancanza di funzionalità e rendere semplicela propagazione delle conversazioni da gestire, Seam fornisce il tag JSF <s:link>
.
Il link può specificare solo l'id della vista JSF:
<s:link view="/login.xhtml" value="Login"/>
Oppure può spcificareil metodo d'azione (nel qual caso l'esito dell'azione determina la pagina di destinazione):
<s:link action="#{login.logout}" value="Logout"/>
Se si specificano entrambi l'id della vista JSF ed il metodo d'azione, verrà usata la 'vista' amenoché il metodo d'azione ritorni un esito non-null:
<s:link view="/loggedOut.xhtml" action="#{login.logout}" value="Logout"/>
Il link propaga automaticamente la riga selezionata del DataModel
usando all'interno <h:dataTable>
:
<s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>
Si può lasciare lo scope di una conversazione esistente:
<s:link view="/main.xhtml" propagation="none"/>
Si può iniziare, terminare, o innestare le conversazioni:
<s:link action="#{issueEditor.viewComment}" propagation="nest"/>
Se il link inizia una conversazione, si può anche specificare il pageflow da usare:
<s:link action="#{documentEditor.getDocument}" propagation="begin"
pageflow="EditDocument"/>
L'attributo taskInstance
è per l'uso nelle liste di task jBPM:
<s:link action="#{documentApproval.approveOrReject}" taskInstance="#{task}"/>
(Si veda l'applicazione demo di Negozio DVD come esempio.)
Infine se il "link" deve essere visualizzato come pulsante, si usi <s:button>
:
<s:button action="#{login.logout}" value="Logout"/>
E' abbastanza comune visualizzare all'utente un messaggio indicante il successo od il fallimento di un'azione. E' conveniente usare FacesMessage
di JSF per questo scopo. Sfortunatamente un'azione di successo spesso richiede un redirect del browser, e JSF non propaga i messaggi faces lungo i redirect. Questo rende difficile visualizzare i messaggi nel semplice JSF.
Il componente Seam predefinito con scope conversazione chiamato facesMessages
risolve questo problema. (Occorre avere installato il filtro redirect di Seam.)
@Name("editDocumentAction")
@Stateless
public class EditDocumentBean implements EditDocument {
@In EntityManager em;
@In Document document;
@In FacesMessages facesMessages;
public String update() {
em.merge(document);
facesMessages.add("Document updated");
}
}
Qualsiasi messaggio aggiunto a facesMessages
viene usato nella fase render response più prossima per la conversazione corrente. Questo funziona anche quando non ci sono conversazioni long-running poiché Seam preserva anche i contesti di conversazioni temporanee lungo i redirect.
Si possono anche includere espressioni JSF EL in un sommario di messaggi faces:
facesMessages.add("Document #{document.title} was updated");
Si possono visualizzare i messaggi nella solita maniera, per esempio:
<h:messages globalOnly="true"/>
Lavorando con le conversazioni che trattano oggetti persistenti, può essere desiderabile utilizzare la chiave naturale di business dell'oggetto invece dello standard, id di conversazione "surrogato":
Redirect facile verso conversazioni esistenti
Può essere utile redirigersi verso una conversazione esistente se l'utente richiede la stessa operazione due volte. Si prenda quest'esempio: «Sei su eBay, stai per pagare un oggetto che hai scelto come regalo per i tuoi genitori. Diciamo che lo stai per inviare a loro - stai per inserire i dettagli di pagamento ma non ti ricordi il loro indirizzo. Accidentalmente riutilizzi la stessa finestra del browser per cercare il loro indirizzo. Ora devi ritornare al pagamento di quell'oggetto.»
Con una conversazione naturale è molto facile riunire l'utente alla conversazione esistente, e riprendere dove aveva lasciato - riunirsi alla conversazione pagaOggetto con l'idOggetto come id di conversazione.
URL user friendly
Questo si concretizza in una gerarchia navigabile (si può navigare editando l'url) e in URL significativi (come mostrato in Wiki - quindi non si identifichino gli oggetti con id casuali). Per alcune applicazioni gli URL user friendly sono sicuramente meno importanti.
Con una conversazione naturale, quando si costruisce un sistema di prenotazione hotel (od una qualsiasi applicazione) si può generare un URL del tipo http://seam-hotels/book.seam?hotel=BestWesternAntwerpen
(sicuramente un qualsiasi parametro hotel
, che mappi sul modello di dominio, deve essere univoco) e con URLRewrite si può facilmente trasformare questo in http://seam-hotels/book/BestWesternAntwerpen.
Molto meglio!
Le conversazioni naturali sono definite in pages.xml
:
<conversation name="PlaceBid"
parameter-name="auctionId"
parameter-value="#{auction.auctionId}"/>
La prima cosa da notare dalla definizione di cui sopra è che la conversazione ha un nome, in questo caso PlaceBid
. Questo nome identifica univocamente questa particolare conversazione e viene usato dalla definizione di pagina
per identificare una conversazione con nome in cui partecipare.
Il prossimo attributo parameter-name
definisce il parametro di richiesta che conterrà l'id della conversazione naturale, al posto del parametro dell'id della conversazione di default. In quest'esempio, il parameter-name
è auctionId
. Questo significa che invece di un parametro di conversazione come cid=123
che appare nell'URL della pagina, conterrà invece auctionId=765432
.
L'ultimo attributo della configurazione di cui sopra, parameter-value
, definisce un'espressione EL usata per valutare il valore della chiave naturale di business da usare come id di conversazione. In quest'esempio, l'id della conversazione sarà il valore della chiave primaria dell'istanza auction
attualmente nello scope.
Poi si definirà quali pagine parteciperanno nella conversazione con nome. Questo è fatto specificando l'attributo conversation
per una definizione di pagina
:
<page view-id="/bid.xhtml" conversation="PlaceBid" login-required="true">
<navigation from-action="#{bidAction.confirmBid}"
>
<rule if-outcome="success">
<redirect view-id="/auction.xhtml">
<param name="id" value="#{bidAction.bid.auction.auctionId}"/>
</redirect>
</rule
>
</navigation>
</page
>
Avviando o facendo redirect verso una conversazione naturale ci sono un numero di opzioni possibili per specificare il nome della conversazione naturale. Si guardi alla seguente definizione di pagina:
<page view-id="/auction.xhtml">
<param name="id" value="#{auctionDetail.selectedAuctionId}"/>
<navigation from-action="#{bidAction.placeBid}">
<redirect view-id="/bid.xhtml"/>
</navigation>
</page
>
Da qua si può vedere che invocando l'azione #{bidAction.placeBid}
dalla vista auction (comunque, tutti questi esempio sono presi dall'esempio seamBay), che verrà rediretta a /bid.xhtml
, che come si è visto, è configurata con la conversazione naturale PlaceBid
. La dichiarazione del metodo d'azione appare come:
@Begin(join = true)
public void placeBid()
Quando le conversazioni con nome vengono specificate nell'elemento <page/>
, la redirezione alla conversazione con nome avviene come parte delle regole, dopo che il metodo d'azione è già stato invocato. Questo è un problema quando ci si redirige ad una conversazione esistente, poiché la redirezione deve avvenire prima che sia invocato il metodo d'azione. Quindi è necessario specificare il nome della conversazione quando si invoca l'azione. Un metodo per fare questo è usare il tag s:conversationName
:
<h:commandButton id="placeBidWithAmount" styleClass="placeBid" action="#{bidAction.placeBid}">
<s:conversationName value="PlaceBid"/>
</h:commandButton
>
Un'altra alternativa è specificare l'attributo conversationName
quando si usano o s:link
o s:button
:
<s:link value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>
La gestione del workspace è la capacità di "cambiare" le conversazioni all'interno di una singola finestra. Seam rende la gestione del workspace completamente trasparente a livello di codice Java. Per abilitare la gestione del workspace, occorre fare questo:
Fornire un testo di descrizione ad ogni view-id (quando si usa JSF o le regole di navigazione JSF) od il nodo della pagina (quando si usano i pageflow jPDL). Questo testo di descrizione viene mostrato all'utente attraverso lo switcher di workspace.
Includere uno o più switcher JSP standard di workspace oppure frammenti facelets nelle proprie pagine. I frammenti standard supportano la gestione del workspace attraverso un menu dropdown, una lista di conversazioni, oppure breadcrumb.
Quando si usano JSF o le regole di navigazione di Seam, Seam cambia la conversazione ripristinando l'attuale view-id
per quella conversazione. Il testo descrittivo per il workspace viene definito in un file chiamato pages.xml
che Seam si aspetta di trovare nella directory WEB-INF
, giusto dove si trova faces-config.xml
:
<pages>
<page view-id="/main.xhtml">
<description
>Search hotels: #{hotelBooking.searchString}</description>
</page>
<page view-id="/hotel.xhtml">
<description
>View hotel: #{hotel.name}</description>
</page>
<page view-id="/book.xhtml">
<description
>Book hotel: #{hotel.name}</description>
</page>
<page view-id="/confirm.xhtml">
<description
>Confirm: #{booking.description}</description>
</page>
</pages
>
Si noti che se questo file manca, l'applicazione Seam continuerà a funzionare perfettamente! La sola cosa che mancherà sarà la possibilità di cambiare workspace.
Quando si usa una definizione di pageflow jPDL, Seam cambia la conversazione ripristinando il corrente stato del processo jBPM. Questo è un modello più flessibile poiché consente allo stesso view-id
di avere descrizioni differenti a seconda del nodo corrente <page>
. Il testo di descrizione è definito dal nodo <page>
:
<pageflow-definition name="shopping">
<start-state name="start">
<transition to="browse"/>
</start-state>
<page name="browse" view-id="/browse.xhtml">
<description
>DVD Search: #{search.searchPattern}</description>
<transition to="browse"/>
<transition name="checkout" to="checkout"/>
</page>
<page name="checkout" view-id="/checkout.xhtml">
<description
>Purchase: $#{cart.total}</description>
<transition to="checkout"/>
<transition name="complete" to="complete"/>
</page>
<page name="complete" view-id="/complete.xhtml">
<end-conversation />
</page>
</pageflow-definition
>
Includere il seguente frammento nella pagina JSP o facelets per ottenere un menu dropdown che consenta di cambiare la conversazione, o la pagina dell'applicazione:
<h:selectOneMenu value="#{switcher.conversationIdOrOutcome}">
<f:selectItem itemLabel="Find Issues" itemValue="findIssue"/>
<f:selectItem itemLabel="Create Issue" itemValue="editIssue"/>
<f:selectItems value="#{switcher.selectItems}"/>
</h:selectOneMenu>
<h:commandButton action="#{switcher.select}" value="Switch"/>
In quest'esempio si ha un menu che include un item per ogni conversazione, assieme a due item aggiuntivi che consentono all'utente di iniziare una nuova conversazione.
Solo le conversazioni con una descrizione (specificata in pages.xml
) verranno incluse nel menu dropdown.
La lista delle conversazioni è molto simile allo switcher delle conversazioni, tranne che viene mostrata come tabella:
<h:dataTable value="#{conversationList}" var="entry"
rendered="#{not empty conversationList}">
<h:column>
<f:facet name="header"
>Workspace</f:facet>
<h:commandLink action="#{entry.select}" value="#{entry.description}"/>
<h:outputText value="[current]" rendered="#{entry.current}"/>
</h:column>
<h:column>
<f:facet name="header"
>Activity</f:facet>
<h:outputText value="#{entry.startDatetime}">
<f:convertDateTime type="time" pattern="hh:mm a"/>
</h:outputText>
<h:outputText value=" - "/>
<h:outputText value="#{entry.lastDatetime}">
<f:convertDateTime type="time" pattern="hh:mm a"/>
</h:outputText>
</h:column>
<h:column>
<f:facet name="header"
>Action</f:facet>
<h:commandButton action="#{entry.select}" value="#{msg.Switch}"/>
<h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/>
</h:column>
</h:dataTable
>
Si immagini di voler personalizzare questo per la propria applicazione.
Solo le conversazioni con una descrizione verranno incluse nella lista.
Si noti che la lista delle conversazioni consente all'utente di distruggere i workspace.
I breadcrumb sono utili nelle applicazioni che usano il modello di conversazioni innestato. I breadcrumb sono una lista di link alle conversazioni nell'attuale stack di conversazioni:
<ui:repeat value="#{conversationStack}" var="entry">
<h:outputText value=" | "/>
<h:commandLink value="#{entry.description}" action="#{entry.select}"/>
</ui:repeat
I componenti conversazionali hanno una piccola limitazione: non possono essere usati per mantenere un riferimento ai componenti JSF. (In generali si preferisce non usare questa funzionalità di JSF amenoché sia assolutamente necessario, poiché questo crea una dipendenza stretta della logica dell'applicazione con la vista.) Sulle richieste postback, i binding dei componenti vengono aggiornati durante la fase restore view, prima che il contesto della conversazione di Seam venga ripristinato.
Per aggirare questo si usa un componente con scope evento per memorizzare i binding dei componenti ed per iniettarlo nel componente a scope conversazione che lo richiede.
@Name("grid")
@Scope(ScopeType.EVENT)
public class Grid
{
private HtmlPanelGrid htmlPanelGrid;
// getters and setters
...
}
@Name("gridEditor")
@Scope(ScopeType.CONVERSATION)
public class GridEditor
{
@In(required=false)
private Grid grid;
...
}
Inoltre non si può iniettare un componente con scope conversazione in un componente con scope evento a cui associare il controllo JSF. Questo include i componenti predefiniti Seam come facesMessages
.
In alternativa si può accedere all'albero del componente JSF attraverso l'handle implicito uiComponent
. Il seguente esempio accede a getRowIndex()
del componente UIData
che è retrostante alla tabella dei dati durante l'interazione, e stampa il numero della riga corrente:
<h:dataTable id="lineItemTable" var="lineItem" value="#{orderHome.lineItems}">
<h:column>
Row: #{uiComponent['lineItemTable'].rowIndex}
</h:column>
...
</h:dataTable
>
I componenti JSF UI sono disponibili in questa mappa con il loro identificatore di client.
Una discussione generale sulle chiamate concorrenti ai componenti Seam può essere trovata in Sezione 4.1.10, «Modello di concorrenza». Qua si discuterà la situazione più comune in cui si troverà la concorrenza — accedendo a componenti conversazionali da richieste AJAX. Si dicuteranno le opzioni che la libreria client Ajax dovrebbe fornire per controllare gli eventi originati nel client — e si vedranno le opzioni che fornisce RichFaces.
I componenti conversazionali non consentono una vero accesso concorrente, e quindi Seam accoda ogna richiesta per poi processarla in modo seriale. Questo consente ad ogni richiesta di venir eseguita in un modo deterministico. Comunque, una semplice cosa non è cosa ottima — in primo luogo, se un metodo per qualche ragione impiega molto tempo a terminare, eseguirlo più volte quando il client genera una richiesta, è una cattiva idea (potenziale per attacchi di tipo Denial of Service), ed in secondo luogo, AJAX spesso viene usato per fornire un veloce aggiornamento dello stato all'utente, e così continuare ad eseguire l'azione per lungo tempo non è utile.
Quindi quando si lavora all'interno di una conversazione long-running, Seam accoda l'evento azione per un periodo di tempo (il timeout della richiesta concorrente); se non si può processare l'evento in tempo, si crea una conversazione temporanea e si stampa un messaggio all'utente per rendergli noto cosa sta succedendo. E' quindi molto importante non inondare il server con eventi AJAX!
Si può impostare un valore di default sensibile per il timeout delle richieste concorrenti (in ms) dentro il file components.xml:
<core:manager concurrent-request-timeout="500" />
Si può anche perfezionare questo timeout a livello di ogni singola pagina:
<page view-id="/book.xhtml"
conversation-required="true"
login-required="true"
concurrent-request-timeout="2000" />
Finora si è discusso delle richieste AJAX che appaiono in serie all'utente - il client dice al server quale evento avviene, e quindi rigenera parte della pagina a seconda del risultato. Questo approccio è ottimo quando la richiesta AJAX è leggera (i metodi chiamati sono semplici, per esempio il calcolo della somma di una colonna di numeri): Ma cosa succede se occorre un calcolo più complesso che dura diversi minuti?
Per una computazione pesante occorre usare un approccio basato sull'interrogazione — il client spedisce una richiesta AJAX al server, che causa un'azione asincrona sul server (la risposta al client è immediata) ed il client quindi interroga il server per gli aggiornamenti. Questo è un buon approccio quando si ha un'azione long-running per la quale è importante che ciascuna azione venga eseguita (non si vuole che qualcuna vada in timeout).
In primo luogo occorre decidere se si vuole usare una richiesta semplice "seriale" e se si vuole l'approccio con interrogazione.
Nel caso di richiesta "seriale" occorre stimare quando tempo occorrerà alle richieste per completarsi - è più breve del timeout della richiesta concorrente? Altrimenti si potrebbe volere probabilmente modificare il timeout per questa pagina (come discusso sopra). Si vuole che la coda lato client prevenga l'inondazione di richiesta al server. Se l'evento si verifica spesso (es. keypress, onblur di un campo d'input) e l'aggiornamento immediato del client non è una priorità si deve ritardare la richiesta lato client. Quando si lavora sul ritardo delle richieste, si tenga presente che l'evento può venire accodato anche lato server.
Infine la libreria del client può fornire un'opzione su come abbandonare le richieste duplicate non terminate a favore di quelle più recenti.
Usando un design di tipo interrogazione richiede un minor fine-tuning. Basta marcare il metodo d'azione con @Asynchronous
e decidere l'intervallo di interrogazione:
int total;
// This method is called when an event occurs on the client
// It takes a really long time to execute
@Asynchronous
public void calculateTotal() {
total = someReallyComplicatedCalculation();
}
// This method is called as the result of the poll
// It's very quick to execute
public int getTotal() {
return total;
}
Comunque per quanto in modo attento si progetti la propria applicazione in modo che accodi le richieste concorrenti nel componente conversazionale, c'è il rischio che il server venga sovraccaricato e sia incapace di processare tutte le richieste prima che la richiesta debba aspettare più a lungo del concurrent-request-timeout
. In questo caso Seam lancerà una ConcurrentRequestTimeoutException
che potrà venir gestita in pages.xml
. Si raccomanda di inviare un errore HTTP 503:
<exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">
<http-error error-code="503" />
</exception
>
Il server è incapace di gestire la richiesta a causa di un temporaneo sovraccarico o di una manutenzione al server. L'implicazione è che questa è una condizione temporanea che verrà risolta dopo un qualche ritardo.
In alternativa si può fare il redirect ad una pagina d'errore:
<exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">
<end-conversation/>
<redirect view-id="/error.xhtml">
<message
>The server is too busy to process your request, please try again later</message>
</redirect>
</exception
>
ICEfaces, RichFaces e Seam Remoting possono tutti gestire i codici d'errore HTTP. Seam Remoting mostrerà una finestra di dialogo con l'errore HTTP e ICEfaces indicherà l'errore nel suo componente di stato. RichFaces fornisce il supporto più completo per la gestione degli errori HTTP e chiamate callback definibili dall'utente. Per esempio, per mostrare il messaggio d'errore all'utente:
<script type="text/javascript"> A4J.AJAX.onError = function(req,status,message) { alert("An error occurred"); }; </script >
Invece di un codice d'errore, il server riporta che la vista è scaduta, forse per un timeout di sessione, si usi una funzione callback separata in RichFaces per gestire questo scenario.
<script type="text/javascript"> A4J.AJAX.onExpired = function(loc,message) { alert("View expired"); }; </script >
In alternativa si può consentire a RichFaces di gestire quest'errore, nel qual caso all'utente verrà presentato un prompt che chiede "Lo stato della vista non può essere ripristinato - ricaricare la pagina?" Si può personalizzare questo messaggio impostando la seguente chiave in un resource bundle dell'applicazione.
AJAX_VIEW_EXPIRED=View expired. Please reload the page.
RichFaces (Ajax4jsf) è la libreria AJAX più usata in Seam e fornisce tutti i controlli discussi sopra:
eventsQueue
— fornisce una coda in cui vengono messi gli eventi. Tutti gli eventi vengono accodati e le richieste vengono inviate al server in modo seriale. Questo è utile se la richiesta al server può prendersi un pò di tempo per essere eseguita (es. computazione pesante, recupero di informazioni da una sorgente lenta) ed il server non viene inondato.
ignoreDupResponses
— ignora la risposta prodotta dalla richiesta se una recente richiesta simile è già in coda. ignoreDupResponses="true" non cancella l'elaborazione della richiesta lato server — previene solamente aggiornamenti non necessari lato client.
Quest'opzione dovrebbe essere usata con cautela nelle conversazioni Seam poiché consente di eseguire richeste concorrenti multiple.
requestDelay
— definisce il tempo (in ms) in cui la richiesta rimane in coda. Se la richiesta non è stata processata dopo questo tempo, la richiesta verrà inviata (anche se la risposta è stata ricevuta) o scartata (se c'è in coda una richiesta recente simile).
Quest'opzione dovrebbe essere usata con cautela nelle conversazioni Seam poiché consente richieste concorrenti multiple- Occorre accertarsi che il ritardo impostato (in combinazione con il timeout delle richieste concorrenti) sia più lungo dell'azione da eseguire.
<a:poll reRender="total" interval="1000" />
— interroga il server, e rigenera un'area come occorre