Java JSF: Como usar os escopos ConversationScoped e ViewScoped
Veja neste artigo como usar os escopos ViewScoped e ConversationScoped para controlar o tempo de vida dos beans gerenciáveis (BackingBeans).
Os escopos descrevem os limites superiores e inferiores de determinado assunto, assim, quando tratamos do escopo de variáveis sabemos até onde elas poderão ser acessadas, como no escopo local, escopo global e etc.
Os escopos do JSF também seguem o mesmo princípio, pois definem até quando aquele bean deve permanecer sendo gerenciado, ou seja, com seu estado mantido. Logo, teremos um projeto bem estruturado.
Neste artigo trataremos de dois escopos diferentes: o ViewScoped, proveniente do JSF 2.0, e o ConversationScoped, proveniente do CDI. Explicaremos qual a função de cada um e daremos exemplos práticos de como utilizá-los.
ViewScoped
Este escopo está entre outros dois muito utilizados até hoje, mas que perderam um pouco o seu “trono” com a vinda do ViewScoped: o Request e o Session.
O Request mantém o estado do bean apenas durante uma requisição, ou seja, era normal realizar várias consultas ao banco de dados a cada requisição feita pelo usuário, consequentemente, tornando a aplicação mais lenta e sobrecarregada quando há diversos usuários fazendo requisições simultâneas. Por outro lado, temos o Session, que mantém o estado do bean durante toda a sessão, ou seja, até a sua destruição, seja por timeout ou explicitamente. Resolvia usar o Session, já que o estado do bean é mantido e não precisamos fazer várias consultas ao banco de dados, porém, quando temos um sistema com muitos beans gerenciáveis e todos em sessão, podemos sofrer com lentidões ou até estouro de memória dependendo da quantidade de informações que cada bean mantém.
Percebemos aqui que temos um problema, pois o escopo de Request é curto demais para manter o bean na quantidade de tempo que desejamos e o escopo Session é longo demais e acaba mantendo o bean além do que desejamos. O ViewScoped veio para tentar resolver esse problema, criando um meio-termo entre o Request e o Session.
O ViewScoped mantém o estado do bean enquanto houver requisições da mesma view/página, e quando ele muda de página o estado do bean é descartado.
A cada requisição realizada o bean é gravado em um “map”, chamado de “view map” e, por conta disso, este bean deve implementar a interface Serializable. Vejamos um exemplo prático do uso deste escopo na Listagem 1.
Listagem 1. Usando @ViewScoped
1 package lab;
2
3 import java.io.Serializable;
4 import java.util.ArrayList;
5 import java.util.List;
6
7 import javax.annotation.PostConstruct;
8 import javax.faces.bean.ManagedBean;
9 import javax.faces.bean.ViewScoped;
10 import javax.faces.model.DataModel;
11 import javax.faces.model.ListDataModel;
12
13 @ManagedBean
14 @ViewScoped
15 public class Bean implements Serializable {
16
17 private List<Item> list;
18 private transient DataModel<Item> model;
19 private Item item = new Item();
20 private boolean edit;
21
22 @PostConstruct
23 public void init() {
24 // list = dao.list();
25 // APenas para exemplificar, a lista deveria ser capturada através do DAO, que faz um HINT ao banco de dados.
26 list = new ArrayList<Item>();
27 list.add(new Item(1L, "item1"));
28 list.add(new Item(2L, "item2"));
29 list.add(new Item(3L, "item3"));
30 }
31
32 public void add() {
33 // dao.create(item);
34 // Apenas para exemplificar, mas o ID deveria ser configurado pelo banco
35 item.setId(list.isEmpty() ? 1 : list.get(list.size() - 1).getId() + 1);
36 list.add(item);
37 item = new Item(); // Para resetar o placeholder
38 }
39
40 public void edit() {
41 item = model.getRowData();
42 edit = true;
43 }
44
45 public void save() {
46 // dao.update(item);
47 item = new Item(); // Para resetar o placeholder
48 edit = false;
49 }
50
51 public void delete() {
52 // dao.delete(item);
53 list.remove(model.getRowData());
54 }
55
56 public List<Item> getList() {
57 return list;
58 }
59
60 public DataModel<Item> getModel() {
61 if (model == null) {
62 model = new ListDataModel<Item>(list);
63 }
64
65 return model;
66 }
67
68 public Item getItem() {
69 return item;
70 }
71
72 public boolean isEdit() {
73 return edit;
74 }
75
76 public void setList(List<Item> list) {
77 this.list = list;
78 }
79
80 public void setModel(DataModel<Item> model) {
81 this.model = model;
82 }
83
84 public void setItem(Item item) {
85 this.item = item;
86 }
87
88 public void setEdit(boolean edit) {
89 this.edit = edit;
90 }
91
92 }
Vamos entender como funciona o código:
- Linha 9: Atente para fazer o import correto da anotação ViewScoped, pois existe outra classe ViewScoped em outro pacote, porém com objetivo diferente do nosso;
- Linha 15: Veja que estamos implementando Serializable para possibilitar ao ViewScoped armazenar o estado do nosso bean no “view map”;
- Linhas 22-29: A anotação @PostConstruct faz com que nosso método init() seja chamado logo após a inicialização do bean gerenciado, ou seja, isso ocorre depois da chamada do construtor do método. Dentro do init() temos uma lista de objetos Item, onde colocamos o dao.list apenas para exemplificar que devemos retornar uma lista da camada DAO. Para evitar que o artigo perca o seu foco decidimos inicializar a lista de forma manual, criando item a item;
- Linhas 32-38: O método add() adiciona um objeto do tipo Item à lista de itens; colocamos aqui também um dao.create() para demonstrar que a criação do objeto deve ser feita na camada DAO, onde este deverá preencher corretamente o ID do bean. Decidimos abstrair a camada DAO e logo em seguida fazemos a criação manual do mesmo e adicionamos esta a lista de itens. Perceba que após criamos um item, substituindo os valores da variável “item”, possibilitando que novos valores sejam inseridos;
- Linhas 40-43: Através de um DataModel conseguimos capturar o item que foi selecionado para edição e habilitamos a flag “edit” para true, assim a nossa camada de visualização saberá que estamos trabalhando com uma edição;
- Linhas 45-49: Quando o usuário clicar em salvar deve-se chamar um método na camada DAO que faça a atualização do registro no banco de dados, por isso deixamos comentado esse trecho, já que não utilizaremos o mesmo neste momento. Depois disso criamos um item para que os campos sejam “limpos”, já que novos valores deverão ser inseridos. Logo após colocamos a flag “edit” igual a false, sinalizando que terminamos de realizar a edição no item desejado;
- Linhas 51-54: Assim como podemos inserir/atualizar um registro no banco de dados, também podemos deletar e, para isso, deveríamos chamar um método responsável pela remoção do registro na camada DAO. Em seguida removemos o item da lista de itens que está sendo mostrada ao usuário.
O restante do código é apenas getters e setters dos atributos e dispensa maiores explicações. Na Listagem 2 mostraremos a classe Item apenas para exemplificar o uso do objeto “item” da Listagem 1.
Listagem 2. Classe Item
import java.io.Serializable;
public class Item implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Long id;
private String value;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((value == null) ? 0 : value.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Item other = (Item) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (value == null) {
if (other.value != null)
return false;
} else if (!value.equals(other.value))
return false;
return true;
}
}
A classe apresentada possui apenas dois atributos que vamos utilizar em nosso XHTML: ID e VALUE. É importante salientar que a sua classe deve implementar Serializable, pois ela está dentro do bean gerenciável, mesmo que ela não seja diretamente o bean anotado com o @ViewScoped. Além disso, por padrão, sempre colocamos o equals() e hashCode() em nossos Java Beans para que comparações sejam feitas de forma correta.
Vamos agora ver a nossa página XHTML, que fará uso do bean anotado com o @ViewScoped, conforme mostra a Listagem 3.
Listagem 3. crud.xhtml
1 <!DOCTYPE html>
2 <html xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:f="http://java.sun.com/jsf/core"
4 xmlns:h="http://java.sun.com/jsf/html">
5 <h:head>
6 <title>CRUD para ViewScoped</title>
7 </h:head>
8 <h:body>
9 <h3>Lista de Itens</h3>
10 <h:form rendered="#{not empty bean.list}">
11 <h:dataTable value="#{bean.model}" var="item">
12 <h:column><f:facet name="header">ID</f:facet>#{item.id}</h:column>
13 <h:column><f:facet name="header">Value</f:facet>#{item.value}</h:column>
14 <h:column><h:commandButton value="edit" action="#{bean.edit}" /></h:column>
15 <h:column><h:commandButton value="delete" action="#{bean.delete}" /></h:column>
16 </h:dataTable>
17 </h:form>
18 <h:panelGroup rendered="#{empty bean.list}">
19 <p>A tabela está vazia, por favor adicione itens</p>
20 </h:panelGroup>
21 <h:panelGroup rendered="#{!bean.edit}">
22 <h3>Add item</h3>
23 <h:form>
24 <p>Value: <h:inputText value="#{bean.item.value}" /></p>
25 <p><h:commandButton value="add" action="#{bean.add}" /></p>
26 </h:form>
27 </h:panelGroup>
28 <h:panelGroup rendered="#{bean.edit}">
29 <h3>Edit item #{bean.item.id}</h3>
30 <h:form>
31 <p>Value: <h:inputText value="#{bean.item.value}" /></p>
32 <p><h:commandButton value="save" action="#{bean.save}" /></p>
33 </h:form>
34 </h:panelGroup>
35 </h:body>
36 </html>
Quando citamos o termo “Bean gerenciável” trata-se do bean que o JSF, CDI ou qualquer outro framework que possua injeção de dependências está gerenciando, evitando a instanciação manualmente desta classe. Assim, chamaremos o bean gerenciado de BK (BackingBean) para facilitar as explicações.
Ao abrir a página crud.xhmtl, o BK é inicializado no contexto da aplicação e o método init() é chamado após a sua construção ter sido finalizada com sucesso. A partir deste momento o ViewScoped é acionado e o estado do BK será mantido enquanto estivermos fazendo requisições na mesma página, ou seja, na crud.xhtml.
Repare que na nossa página temos a seguinte divisão:
- Linhas 10-20: Temos a listagem de itens contidos no atributo List<Item> do BK. O dataTable só é mostrado/renderizado se houverem itens a serem mostrados, caso contrário, o panelGroup será renderizado mostrando a mensagem “A tabela está vazia, por favor adicione itens”;
- Linhas 21-27: Seção para adicionar novos itens digitando o valor que deseja colocar e depois em “add”. Assim, este item será adicionado na lista e um novo será criado, permitindo adicionar novos valores. Aqui o ViewScoped já começa a atuar, pois quando clicamos em add veremos que o datatable crescerá, ou seja, o estado do BK será mantido. Prova disto é que a lista cresce sem perder seu valor anterior;
- Linhas 28-34: Podemos escolher um item na lista e clicar em editar para alterarmos o seu valor. Logo em seguida, clicando em salvar, persistiremos os dados alterados no banco de dados.
ConversationScoped
O ViewScoped não resolve qualquer problema a qualquer hora, pois imagine precisar trabalhar com várias páginas diferentes para um mesmo bean gerenciável: este não atenderá, pois ele tem uma relação 1:1 com a página XHTML. Essa situação de muitas páginas para um mesmo bean gerenciável é muito comum em wizards ou mesmo em processos complexos que exijam muitas telas para o preenchimento correto das informações. Então aqui entra o ConversationScoped que, diferentemente do ViewScoped, que é proveniente do JSF, é proveniente do CDI, ou seja, estamos misturando escopos de dois frameworks distintos.
O Escopo de conversação difere dos outros pelo fato de poder dizer onde ele terminar e onde começa, ou seja, você delimita as fronteiras. Se quiser que ele comece ao acessar a pagina001.xhtml e termine quando o usuário acessar a pagina004.xhtml, isso é totalmente possível. Vejamos um exemplo simples de início e fim de conversação na Listagem 4.
Listagem 4. Limitando fronteiras do ConversationScoped
1 import javax.enterprise.context.Conversation;
2
3 private Conversation conversation;
4
5 public void beginConversation() {
6 if (conversation.isTransient()) {
7 conversation.setTimeout(1800000L);
8 conversation.begin();
9 }
10 }
11
12 public void endConversation() {
13 if (!conversation.isTransient()) {
14 conversation.end();
15 }
16 }
O código consiste na exemplificação básica de uma inicialização e finalização de uma conversação:
- Linha 3: É necessário termos o atributo do tipo javax.enterprise.context.Conversation para que possamos manipular a conversação;
- Linhas 5-9: Verificamos aqui se a conversação é transiente, ou seja, se ainda não foi inicializada. Caso verdadeiro, então iniciamos uma nova com o timeout de 30 minutos;
- Linhas 12-14: Verificamos se a conversação não é transiente e então finalizamos a mesma, garantindo que apenas conversações que foram inicializadas podem ser finalizadas.
Poderíamos usar o ConversationScoped para apenas uma página sem problemas, ou seja, 1:1 do XHTML com o seu bean gerenciável. Mas, porque usar o ConversationScoped neste caso se podemos usar o ViewScoped?
Em casos que temos várias páginas para um mesmo bean gerenciável cabe perfeitamente o uso da conversação.
Lembre-se que devemos explicitamente iniciar a conversação quando a página for aberta e fechá-la quando alguma ação for realizada, por exemplo, quando o usuário clicar no botão salvar.
Listagem 5. Iniciando a conversação de uma página XHTML
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:h="http://java.sun.com/jsf/html"
4 xmlns:f="http://java.sun.com/jsf/core"
5 xmlns:ui="http://java.sun.com/jsf/facelets">
6
7 <h:head>
8
9 </h:head>
10 <h:body>
11 <h:form>
12 <f:metadata>
13 <f:event listener="#{bean.beginConversation()}"
14 type="preRenderView" />
15 </f:metadata>
16 <h:commandButton value="Prox.Pagina" action="#{begin.proxPagina()}" />
17 </h:form>
18
19
20 </h:body>
21 </html>
O que nos importa na Listagem 5 é verificar que iniciamos a conversação antes da renderização da página com o f:event da linha 12. Assim, teremos certeza que as chamadas ao bean que a página fará não darão erro, pois a conversação já foi inicializada.
Na linha 16 o commandButton tem a função de chamar a action “proxPagina()” que irá levar o usuário a outra página.
Listagem 6. Finalizando a conversação
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:h="http://java.sun.com/jsf/html"
4 xmlns:f="http://java.sun.com/jsf/core"
5 xmlns:ui="http://java.sun.com/jsf/facelets">
6
7 <h:head>
8
9 </h:head>
10 <h:body>
11 <h:form>
12 <h:commandButton value="Concluir" action="#{begin.endConversation()}" />
13 </h:form>
14
15
16 </h:body>
17 </html>
Nesta segunda página da Listagem 6 não precisamos inicializar a conversação, pois ela já foi feita na primeira página. Agora, quando o usuário clicar em Concluir, o método endConversation() será disparado e a conversação será finalizada.
O CDI envia um parâmetro dentro das requisições do JSF chamado CID, que é uma etiqueta para a sua conversação. É com este que o CDI sabe qual bean deve construir. Quando mudar de página através de uma action do seu BackingBean (Bean Gerenciável), este CID é acoplado automaticamente na sua requisição, mas se decidir mudar de página sem passar por uma requisição do JSF, então precisará explicitamente colocar o CID como parâmetro, como vemos na Listagem 7.
Listagem 7. Usando CID
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
</h:head>
<h:body>
<h:form>
<h:link outcome="/prox_pagina.xhtml" value="Pagina para Finalizar a Conversação">
<f:param name="cid" value="#{bean.conversation.id}" />
</h:link>
</h:form>
</h:body>
</html>
Perceba que não fazemos uso do JSF para navegarmos para a próxima página, então neste caso devemos explicitamente usar o f:param para dizer o valor do CID, assim, quando a próxima página for renderizada carregará automaticamente o bean gerenciável correto. Se não passar o CID, neste caso, quando a próxima página for aberta, ela não encontrará nenhum bean gerenciável e trará um erro na tela do usuário.
Se estiver usando Maven como gerenciador de dependências poderemos adicionar o código da Listagem 8 ao seu pom.xml para usar o Weld, uma implementação do CDI.
Listagem 8. Importando CDI no pom.xml
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
<version>2.3.0.Final</version>
</dependency>
Concluímos que o ViewScoped serve para situações onde precisamos ligar uma página para um bean gerenciável, enquanto o ConversationScoped liga N páginas para um bean gerenciável. Assim, temos a seguinte regra:
- ViewScoped – 1 xhtml: 1 BackingBean
- ConversationScoped – N xhtml: 1 BackingBean
Isso não significa que não podemos usar o ConversationScoped em 1:1, mas a prática e a teoria dizem que essa não é a melhor forma de usá-lo. É importante também entender que existem algumas outras variáveis de escopos de conversação, como as propostas pelo JBoss Seam e o próprio JSF 2.0, mas neste caso demos preferência ao CDI pelo seu uso frequente e grande público no mercado.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo