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&amp;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.

13 thoughts on “Como trabalhar com ViewScope e Page

  1. Legal, seu post.
    f:setPropertyActionListener com @ViewScoped SUCKS….
    Talvez ficaria mas elegante com f:param ou f:attribute…
    Se a @ViewConversationScoped resolver meu problema… ADEUS Mojarra…

  2. Valeu @Célio.
    Não sei se você reparou, mas o intuito do post é mostrar como cada forma de fazer se comporta, e por isso fiz o link “editar 2″ usando f:setPropertyActionListener e o “editar 1″ usando f:param.
    Além disso, não tem muita relação a implementação de JSF que você usar (seja Mojarra ou outra qualquer) com o @ViewConversationScoped, que pelo menos e teoria deveria ser uma extensão portável da CDI (e não do JSF ou MyFaces ou qualquer outra coisa assim).
    Mas valeu pelo comentário.

  3. muito bom eim, me ajudo com o view, parabens e continue assim, abraço

  4. Eae @João Bosco O. Monteiro!
    Valeu pelo comentário e pelo link.
    Eu acabei não desenvolvendo muito o que eu considerei ser um comportamento legal, que poderia ser padrão. Acabei (por preguiça e) para não ter que reescrever algo que já estava escrito, apenas linkando o condeúto da apache. Mas o que quis dizer é que particularmente gosto de trabalhar com uma conversação por view (igual ao escopo que comentei).
    Agora esse link que você passou tem mais a ver com o que seria mais “correto” ou menos pessoal eu comentar… que é um ViewScope sem matar o escopo antes de terminar de renderizar a view. Valeu pelo comentário, acabou deixando o post mais completo.

  5. Olá Gilliard
    Eu tava seguindo o post funcionou tudo certinho mas tive problemas com o ViewScope e o back do browser(pelo que entendi de ViewScope não ia funcionar mesmo).Na aplicação tenho um Ente Federativo que tem uma coleção de Órgãos e cada Órgão tem uma coleção de Unidades e cada Unidade uma coleção de Mandatos ,quando tou na página de listagem de órgão por exemplo e vou pra pagina de editar órgão e dessa pagina volto pra pagina de listagem de órgãos novamente e se quiser ir novamente pra página de edição de órgão tenho que clicar no link duas veses.Existe como solucionar este problema???

  6. obs: esse problema só está ocorrendo quando utilizo a abordagem que vc utilizou no
    “editar 2″ a abordagem com está funcionando perfeitamente

  7. Olá Gilliard,

    Acompanhei alguns posts seus pois estou desenvolvendo meu projeto da pós em Prime e JSF 2.

    Acredito que todos que estão começando a desenvolver algum projeto com JSF 2 x Primefaces deveriam ler esse seu post. É de sua importância e volto a salientar o que citei no GUJ; não há na internet (se há, são poucos os artigos e sem aplicação direta como neste caso) referente a um processo CRUD com páginas distintas. Sempre vejo casos simples (com managedBean e um p:dialog na mesma tela para edição de dados) porém para o meu caso, por exemplo, que tenho um formulário com vários atributos, ficaria inviável inserir tudo numa janela modal (p:dialog).

    Este seu exemplo me ajudou a matar vários problemas

    1. Estava colocando o escopo de Session (não “perder” o objeto) ao invés de view para poder navegar entre as páginas de pesquisa e alteraçao de dados do usuário ;
    2. Não sabia ainda como iria fazer para injetar diretamente na URL os parâmetros para que seja possível realizar o compartilhamento da informação (facebook, etc);
    3. E finalmente (batendo palmas estou) há aqui uma demonstração clara e objetiva do “bendito” converter.

    Eu estava relutante em utilizar o converter mas depois de ver o que ele fez neste processo utilizando-se da propriedade includeViewParams=true e demais detalhes, agora realmente vejo como ele pode ajudar.

    O único empecilho que vejo é que a todo momento que é clicado em um registro, ele realiza a busca no banco de dados, diferente do que teríamos se fosse utilizar o escopo Session, por exemplo, em que o managedBean estaria “vivo” e na próxima tela poderíamos popular os dados da tela de alteração de dados. Mas aí caímos na situação que comentei em que estava tendo que utilizar escopo Session, solução que não estava apreciando nem um pouco!

    Resumindo tudo isso ai que escrevi, rs, “matei 3 coelhos numa paulada só”.

    Meus parabéns por este post e reforço novamente a todos -> estudem os detalhes sobre este artigo e sobre converters. Vale a pena !!

    Obs.: Gilliard, uma dúvida que me passou pela cabeça agora, e se neste caso que estou implementando (manutenção usuários) utilizar escopo Session ? Achei interessante pois antes de implementar o seu exemplo, eu estava pesquisando usuários (filtros na tela de pesquisa) e ao retornar da tela de alteração de usuário, a tela de pesquisa continuava com a lista de 3 usuários filtrada. No atual exemplo que voltei para o escopo view, ao retornar para a tela de pesquisa, a lista é recarregada devido ao mb ter sido instanciado novamente.

    Abraço,
    Rodrigo Bortolon

  8. Olá Gilliard bom dia.

    Prezado procurando solução para um incoveniente encontrei este seu post que tem similaridades com o que estou fazendo, estou adotando as mesmas tecnologias, um so MB ele com @SessionScoped o CRUD funciona bem, qdo coloco View os dados não persistem.
    Sei que aqui não é o local para se questionar isto, so o estou fazendo por conta da semelhança com o que estou fazendo com JSF 2.1, Prime e JPA

  9. Olá Gilliard!

    No caso de querer usar o mesmo form para inserir um novo produto, como não será passado nenhum parametro pela url para f:viewParam o converter vai acusar NullException! Qual a melhor maneira de resolver?

    Abraço

  10. Fala @Marcos!
    Olha, o que costumo fazer é iniciar o converter checando se o valor convertido é válido (não nulo ou vazio), se for eu já retorno null e nem entro no corpo do converter.
    Eu faço essa validação justamente porque o parâmetro que geralmente é esperado pode não vir.
    Fazendo isso vc não terá NPE.
    Aí é só no teu Bean instanciar o objeto a ser alterado/inserido no @PostConstruct, aí se vier alguém do converter ele será substituído, se não vier ele fica como está (new).

    Espero ter ajudado. Abraço!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>