Como trabalhar com ViewScope e Page
Uma coisa que não é muito intuitiva é a forma como o ViewScope do JSF e o scope Page do Seam funcionam. Como estamos acostumados com o escopo request, que termina quando a próxima view é renderizada, tendemos a pensar que esses escopos funcionam da mesma forma. Mas na verdade o escopo morre no momento que uma nova view é setada. O problema é que depois que isso acontece ainda temos toda a fase 6 do jsf.
Para entendermos melhor o funcionamento, vamos considerar como exemplo uma tela de listagem de produtos (produtoLista.xhtml) onde selecionamos um produto e este é exibido em outra view, que mostra os detalhes desse produto (produtoForm.xhtml). Nessa aplicação vou usar o mesmo managed bean com @ViewScope para a listagem e para a tela do produto.
Usando o escopo view, quando clicamos num h:command(Button | Link) que tem dentro um f:setPropertyActionListener temos a impressão que o jsf não colocou o produto selecionado no target, no caso o produtoController.produto. Na verdade ele fez isso sim, mas assim que mudou da view de listagem para a de produto o produtoController, que continha o produto selecionado foi descartado. Então um novo produtoController é instanciado, e esse obviamente não conhece o produto selecionado. O funcionamento é simples, só não é intuitivo (vou fazer essa afirmação várias vezes que é pra ficar no subconsciente hehehe).
Na minha opinião, um bom comportamento padrão seria como o @ViewConversationScoped. Mas como ninguém liga para a minha opinião, o jeito é usarmos os parâmetros de url para segurar esses valores. Pra variar já escrevi muito, então vamos ver na prática como fazer isso.
Na classe Produto vou simplesmente ignorar os getters e setters, Groovy like =)
Entidade Produto
@Entity public class Produto { @Id @GeneratedValue private Integer id; private String nome; private String descricao; @Override public String toString() { return "Produto [descricao=" + descricao + ", id=" + id + ", nome=" + nome + "]"; } }
O Controlador
@ManagedBean @ViewScope public class ProdutoController { private Produto produto; private List<Produto> produtos; //método init serve só para vermos em que momento o bean é destruído @PostConstruct public void init(){ System.out.println("ProdutoController.init()"); atribuirEstadoInicial(); } /** * Deixa o bean em um estado inicial válido tanto para edição quanto para listagens */ private void atribuirEstadoInicial() { System.out.println("ProdutoController.atribuirEstadoInicial()"); //serve para deixar o bean em um estado onde pode acontecer uma nova edição produto = new Produto(); //limpa a listagem previamente carregada pois ela não contém um elemento novo ou contém um recém excluído produtos = null; } public void salvar() { System.out.println("ProdutoController.salvar()"); JpaUtil.getEntityManager().getTransaction().begin(); JpaUtil.getEntityManager().merge(produto); JpaUtil.getEntityManager().getTransaction().commit(); atribuirEstadoInicial(); } public Produto getProduto() { return produto; } public void setProduto(Produto produto) { System.out.println("ProdutoController.setProduto(): " + produto); this.produto = produto; } @SuppressWarnings("unchecked") public List<Produto> getProdutos() { if (produtos == null) { produtos = JpaUtil.getEntityManager().createQuery("select p from Produto p").getResultList(); } return produtos; } public void setProdutos(List<Produto> produtos) { this.produtos = produtos; } }
E o converter
@FacesConverter(forClass=Produto.class) public class ProdutoConverter implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String string) { System.out.println("ProdutoConverter.getAsObject(): " + string); if(string == null || string.isEmpty()){ return null; } return JpaUtil.getEntityManager().find(Produto.class, Integer.valueOf(string)); } @Override public String getAsString(FacesContext context, UIComponent component, Object object) { Produto produto = (Produto) object; System.out.println("ProdutoConverter.getAsString(): " + produto); if(produto == null || produto.getId() == null){ return null; } return String.valueOf(produto.getId()); } }
Na verdade, até aqui não tem muita novidade. No resto também não vai ter novidade
mas vamos lá.
A listagem de produtos:
... <h:dataTable value="#{produtoController.produtos}" var="produto"> <h:column> <f:facet name="header">ID</f:facet> #{produto.id} </h:column> <h:column> <f:facet name="header">Nome</f:facet> #{produto.nome} </h:column> <h:column> <f:facet name="header">Descrição</f:facet> #{produto.descricao} </h:column> <h:column> <f:facet name="header">Ações</f:facet> <h:link value="editar 1" outcome="produtoForm"> <f:param name="id" value="#{produto.id}"/> </h:link> <h:commandLink value="editar 2" action="produtoForm?faces-redirect=true&includeViewParams=true"> <f:setPropertyActionListener value="#{produto}" target="#{produtoController.produto}"/> </h:commandLink> </h:column> </h:dataTable> ...
E o form de produto:
... <f:view> <f:metadata> <f:viewParam name="id" value="#{produtoController.produto}" /> </f:metadata> <h:head> <title>Detalhes do Produto</title> </h:head> <h:body> <h:form> <h:panelGrid columns="2"> Nome: <h:inputText value="#{produtoController.produto.nome}" /> Descrição: <h:inputText value="#{produtoController.produto.descricao}" /> <h:commandButton action="#{produtoController.salvar}" value="Salvar" /> </h:panelGrid> </h:form> </h:body> </f:view> ...
Por fim, vamos analisar o log do click nos links “editar 1″ e “editar 2″
link “editar 1″
ProdutoController.init() ProdutoController.atribuirEstadoInicial() ProdutoConverter.getAsObject(): 1 ProdutoController.setProduto(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento] ProdutoConverter.getAsString(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento]
link “editar 2″
ProdutoController.setProduto(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento] ProdutoConverter.getAsString(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento] ProdutoController.init() ProdutoController.atribuirEstadoInicial() ProdutoConverter.getAsObject(): 1 ProdutoController.setProduto(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento] ProdutoConverter.getAsString(): Produto [descricao=Fermento em Pó, id=1, nome=Fermento]
Beleza, agora sim tem código pra caramba… boa parte dele aliás bem parecido com o desse post. No meio disso tudo o que temos que prestar atenção é nos dois botões editar da produtoLista.xhtml. O link “editar 1″ é exatamente igual ao apresentado no post que acabei de citar. O valor é passado por GET e o converter do viewParam faz o trabalho de nos deixar trabalhar sempre OO.
Agora vamos ver o link “editar 2″. Nesse exemplo a gente tem um post para uma view que usa um ManagedBean com escopo @ViewScope para uma outra view cujo MB é o mesmo, mas isso é um detalhe.
Na primeira linha temos o f:setPropertyActionListener trabalhando e chamando o set da propriedade, e na segunda linha vimos o converter gerando o texto (nesse caso id) que irá representar esse objeto na url da próxima view, pois deixamos o includeViewParams=true. Note que em momento algum passamos a propriedade que vai representar o produto na url como fizemos no “editar 1″. Quem vai fazer isso é o conversor.
Depois, entre as linhas 2 e 3 a view é trocada e o MB é perdido, mas como a url agora já tem o valor a ser mantido, fica igual o exemplo anterior. A única coisa que pode parecer é que teremos buscas desnecessárias ao banco. Mas como você vai estar usando algo mais esperto do que buscar no braço, a JPA já vai estar com esse objeto no cache de primeiro nível – pois estou usando o padrão OpenEntityManagerInView – e não haverá nenhum overhead por causa dessa outra forma de fazer. E isso é muito importante, apesar de termos um converter no meio, e do POST em vez de GET rodar o restore view do jsf, o objeto selecionado não será em momento algum trazido mais de uma vez no banco pois o EntityManager está com ele no cache (para isso não precisa de configuração nenhuma). Como estamos com o bean em escopo view, também não será buscado novamente a lista do banco. Então a única perda real nesse caso é não termos a url montada já na tela de listagem – o que pode nem ser uma perda. De fato todo o “overhead” dessa abordagem resume-se a chamadas de métodos locais como getters. Então provavelmente se sua aplicação ficar lenta aqui, o problema é outro.
Novamente o que incomoda é a falta de intuitividade dessa abordagem. Mas o funcionamento é simples. Só temos que lembrar que nessa abordagem do “editar 2″ só vai funcionar se tivermos o includeViewParams ativo, seja no link ou na regra de navegação do faces-config.xml. Sem isso o JSF não se preocupa em incluir na próxima view os parâmetros de url.
Importante! (update)
Apesar da abordagem do link “editar 1″, que usa GET ser a forma mais bacana de se trabalhar, e inclusive é a “novidade” do JSF 2, a abordagem do “editar 2″ tem se mostrado mais segura. Isso porque até a versão atual do JSF (2.1) a remoção do bean no escopo view não ocorre da forma esperada quando usamos GET para sair da página, porém quando usamos POST (jeitão que o JSF já está bem acostumado) a coisa rola corretamente.
Agora caso você queria usar um escopo que dure mais que uma página como comentado pelo Rodrigo a melhor solução na minha opinião é usar conversação. Solução que inclusive permite trocar de páginas usando GET sem o problema do escopo view, que desse modo não remove o bean, pois na conversação, se você não matar, o timeout mata.
Uma forma simples de usar é iniciar a conversação quando abrimos a view. Para isso podemos fazer de várias formas, mas a mais simples é usando o seam-faces:
Código da view
<f:metadata> <s:viewAction action="#{meuBean.init}" if="#{conversation.transient}" /> </f:metadata>
Código do Bean
@Named @ConversationScoped public class MeuBean{ @Begin public void init(){ } }
Ou
Código do Bean (alternativo)
@Named @ConversationScoped public class MeuBean{ @In Conversation conversation public void init() { conversation.begin(); } }
Mas não estou dizendo para criar conversação e largar, tem que matar ela. Só estou falando que se for pra largar pra trás (coisa feia
) é melhor fazer com conversação do que com view ou session.
E ainda outra forma de usar um escopo view em mais de uma página é usar o @ViewAccessScope do apache CODI (citado também pelo João). Ele funciona como o “bom e velho” Keep Alive (anotação ao tag), e em vez de matar o bean na troca de página, ele espera o fim do response, e se o bem não for usado, aí sim é removido. O única problema é que a configuração do apache CODI, principalmente quando já estamos rodando o seam-faces, é um pouquinho mais charope. Mas funciona.
Concluindo…
Nada do que mostrei aqui é novo ou difícil. Mas resolvi escrever pois em uma semana tive três dúvidas iguais aqui no blog sobre esse assunto. E nos cursos de Seam (escopo Page) e JSF 2 que ministro vejo que esse assunto demora para ser digerido também. Então espero que esse post tenha sido útil para minimizar essas dúvidas. Usar esse recurso do JSF 2 (ou Seam) é simples, mas se te incomodar muito, ou se você quiser usar uma conversação em uma única view (@ViewScope não segura o EntityManager aberto e com isso não evita LazyinitializationException), lembre-se que JEE6 define extensões portáveis. Então uma boa coisa é procurar coisas como o escopo que eu citei no início do post.
Sei que o pessoal do Java é meio purista, as vezes torce o nariz para o que não é especificado, mas se ganha muito procurando a solução para o seu problema em um projeto opensource bacana em vez de passar raiva e esperar até sair a próxima versão de alguma especificação, o que obviamente vai demorar mais do que uma novidade nascida direto da comunidade (apache, jboss.org, etc). Mas isso é assunto para um próximo post.
Recent Comments