Archive

Posts Tagged ‘bookmarking’

Como trabalhar com ViewScope e Page

November 23rd, 2010 7 comments

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.

URLs amigáveis no JSF 2.0

May 27th, 2009 9 comments

Hoje vou falar de mais uma novidade do JSF 2, cuja falta era motivo de muita reclamação: URLs amigávies, bookmarking, método GET e outros nomes que podemos dar. O suporte a essa nova funcionalidade é dado por dois pares de componentes, de um lado h:button e h:link e do outro f:metadata e f:viewParam.

Os componentes h:button e h:link servem para originar as ações jsf assim como os componentes h:command{Button|Link}, porém usando GET em vez de POST. Esses componentes possuem um atributo chamado outcome, que representa a regra de navegação do JSF, assim como seria colocar uma String diretamente na action do h:command{Button|Link}. Para passar parâmetros colocamos a tag f:param dentro do h:link ou h:button.

Como exemplo vamos ver uma aplicaçãozinha que tem uma tela de listagem e outra de visualização de Pessoas. A seguir um trecho da página de listagem de pessoas, listarPessoas.xhtml:

<h:form>
	<h:dataTable value="#{pessoaController.pessoas}" var="pessoa">
		<h:column>
			#{pessoa.nome}
		</h:column>
		<h:column>
			<h:link outcome="/verPessoa" value="Editar">
				<f:param name="pessoa" value="#{pessoa.nome}"/>
			</h:link>
		</h:column>
	</h:dataTable>
</h:form>

Nesse exemplo, no outcome eu já estou usando o esquema novo de navegação comentado no post passado. O link acima vai gerar uma url parecida com http://localhost:8080/exemplojsf2/verPessoa.jsf?pessoa=fulano_0.

Agora do lado da página que recebe a requisição temos os componentes f:metadata e f:viewParam. A primeira é apenas uma tag que engloba as f:viewParam. Já as tags f:viewParam se comportam de forma muito parecida com um h:inputText, podemos dizer que praticamente a única diferença é no input a gente digita em um formulário, enquanto na f:viewParam escrevemos na URL.

A tag f:viewParam possue os seguintes atributos: converter, converterMessage, required, requiredMessage, validator, validatorMessage, value, valueChangeListener, maxlength e for (este último voltado para o novo esquema de component composition do Facelets). Como podemos ver, é praticamente um h:inputText.

Vamos ver então um trecho do código da página que recebe a requisição, verPessoa.xhtml:

<f:view>
	<f:metadata>
	    	<f:viewParam name="pessoa" value="#{pessoaController.pessoaSelecionada}" />
   	</f:metadata>
<h:head>
    <title>Ver Pessoa</title>
</h:head>
<h:body>
	<h:form>
		Nome: #{pessoaController.pessoaSelecionada.nome}
	</h:form>
</h:body>
</f:view>

Não precisei colocar um converter no f:viewParam pois configurei um converter “forClass” como veremos mais a frente.

Agora a nossa classe

public class Pessoa {
 
	private String nome;
 
	public Pessoa() {
	}
	public Pessoa(String nome) {
		this.nome = nome;
	}
 
	//getter e setter suprimido
}

Isso mesmo, a classe é complexa desse jeito  :D

Como o intuito é só um exemplo, eu nem me preocupei com banco de dados ou algum mecanismo mais interessante, apenas criei um conversor para mostrar que o novo esquema não permite apenas o uso de Strings. Segue o código do conversor:

@FacesConverter(forClass=Pessoa.class)
public class PessoaConverter implements Converter {
 
	@Override
	public Object getAsObject(FacesContext context, UIComponent component, String string) {
 
		return new Pessoa(string);
	}
 
	@Override
	public String getAsString(FacesContext context, UIComponent component, Object object) {
 
		return ((Pessoa)object).getNome();
	}
 
}

E agora nosso managed bean que recebe não uma String, e sim objetos do nosso domínio. Eu falo isso o tempo todo pois preciso deixar isso bem claro, senão eu fico doido de ver uma aplicação usando JSF passando String e Integer de um lado pro outro :(

Mas tudo bem, deixando o desabafo pra lá vamos ao código:

@ManagedBean(name="pessoaController")
@RequestScoped
public class PessoaController {
 
	private Pessoa pessoaSelecionada;
	private List<pessoa> pessoas;
 
	@PostConstruct
	public void init()
	{
		pessoas = new ArrayList<pessoa>();
		for (int i = 0; i < 10; i++) {
			pessoas.add(new Pessoa("fulano_" + i));
		}
	}
 
	//getters e setters suprimidos
}

Acredito que por hoje seja suficiente. Vou ver se em breve escrevo algo mais específico sobre facelets.