Aula 13: Navegação, Templating e Conversores com Jakarta Faces
Aprenda a criar interfaces consistentes e implementar navegação eficiente usando o sistema de templating Facelets e os mecanismos de navegação do Jakarta Faces, incluindo templates reutilizáveis, passagem de parâmetros, conversores e gerenciamento de estado entre páginas.
Objetivos
- Dominar o sistema de templating Facelets (
ui:composition,ui:insert,ui:define,ui:include) - Implementar navegação implícita, explícita e programática com redirect (PRG pattern)
- Utilizar
f:viewParamef:viewActionpara passagem de parâmetros e pré-carregamento - Compreender Flash Scope para preservação de mensagens após redirect
- Construir layouts reutilizáveis com menu, breadcrumbs e componentes compostos
Conteúdo
Contexto e Motivação
Aplicações web corporativas enfrentam dois desafios fundamentais:
- Consistência Visual: Como manter uma identidade visual padronizada em todas as páginas sem duplicar código?
- Navegação: Como conduzir o usuário entre telas, passando parâmetros e preservando estado de forma segura?
O Jakarta Faces oferece soluções completas para ambos os desafios.
1. Sistema de Templating (Facelets)
Permite criar layouts reutilizáveis através de:
ui:composition: Define uma página que usa um templateui:insert: Marca áreas do template que serão preenchidasui:define: Fornece conteúdo para as áreas do templateui:include: Inclui fragmentos reutilizáveis (menu, rodapé)ui:param: Passa parâmetros para templates e includes
2. Sistema de Navegação
Controla o fluxo entre páginas através de:
- Navegação Implícita: Retornar uma string como
"filmes"navega parafilmes.xhtml - Navegação Explícita: Regras centralizadas em
faces-config.xml - Redirecionamento:
faces-redirect=trueevita re-submissão de formulários - Parâmetros:
f:viewParameincludeViewParams=truepara passar dados na URL
No CritiqueHub, templating garante consistência visual entre catálogo, detalhes e avaliações, enquanto a navegação permite transições fluidas com preservação de contexto.
Templating com Facelets
Facelets é o sistema de templating padrão do Jakarta Faces que permite criar layouts reutilizáveis através da composição de componentes. A estrutura básica envolve:
Template Base (layout-base.xhtml) — define a estrutura comum da aplicação com áreas que serão preenchidas:
<ui:insert name="title">Título Padrão</ui:insert>
<ui:insert name="content">Conteúdo</ui:insert>
Página que Usa o Template — referencia o template e fornece conteúdo para as áreas definidas:
<ui:composition template="/templates/layout-base.xhtml">
<ui:define name="title">Meu Título</ui:define>
<ui:define name="content">Meu Conteúdo</ui:define>
</ui:composition>
Benefícios:
- Eliminação de duplicação de código
- Manutenção centralizada do layout
- Consistência visual automática
- Facilita mudanças globais na interface
Navegação no Jakarta Faces
O sistema de navegação controla qual página será exibida após uma ação do usuário.
Navegação Implícita
O outcome retornado mapeia diretamente para o arquivo XHTML:
public String listar() {
return "filmes"; // Navega para filmes.xhtml
}
Navegação Explícita
Regras centralizadas em faces-config.xml:
<navigation-rule>
<from-view-id>/index.xhtml</from-view-id>
<navigation-case>
<from-outcome>listar</from-outcome>
<to-view-id>/filmes.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
Redirecionamento (PRG Pattern)
Adicione faces-redirect=true para forçar uma nova requisição GET:
return "filmes?faces-redirect=true";
Benefícios do redirect:
- Evita re-submissão de formulário ao atualizar (F5)
- Gera URLs compartilháveis
- Segue o padrão Post-Redirect-Get (PRG)
Use sempre redirect após operações de escrita (criar, atualizar, deletar) para evitar problemas com atualização de página.
Parâmetros de View e Ações de Pré-carregamento
f:viewParam — Recebendo Parâmetros da URL
Declara parâmetros esperados na URL e os vincula automaticamente a propriedades do bean:
<f:metadata>
<f:viewParam name="id" value="#{detalheBean.id}"/>
</f:metadata>
URL: filme-detalhe.xhtml?id=5 → detalheBean.id receberá o valor 5.
f:viewAction — Pré-carregamento de Dados
Executa um método antes da renderização da página, ideal para carregar dados baseados em parâmetros:
<f:metadata>
<f:viewParam name="id" value="#{detalheBean.id}"/>
<f:viewAction action="#{detalheBean.carregar}"/>
</f:metadata>
O método carregar() é executado automaticamente após a injeção do parâmetro id.
Incluindo Parâmetros no Redirect
Use includeViewParams=true para preservar parâmetros durante navegação:
return "filme-detalhe?faces-redirect=true&includeViewParams=true";
Flash Scope e View Scope
Flash Scope
Armazena dados temporariamente entre duas requisições (útil com redirect):
FacesContext.getCurrentInstance()
.getExternalContext()
.getFlash()
.setKeepMessages(true);
Mantém mensagens de feedback visíveis após o redirect.
View Scope
Mantém o estado do bean enquanto o usuário permanece na mesma página:
| Escopo | Ciclo de Vida | Uso Recomendado |
|---|---|---|
@RequestScoped | Uma requisição | Operações simples sem estado |
@ViewScoped | Enquanto na mesma página | Páginas com AJAX, filtros, tabs |
@SessionScoped | Toda a sessão do usuário | Dados globais (usuário logado) |
| Flash | Duas requisições (redirect) | Mensagens após redirect |
Use @ViewScoped para páginas com interações AJAX (filtros, paginação, painéis). Use Flash Scope para preservar mensagens após redirect.
Exercícios (Checkpoints)
Laboratório 9 — Implementando Navegação e Templating
Neste laboratório, você implementará um sistema completo de templates e navegação, incluindo um layout base reutilizável, página de listagem com filtros, página de detalhes com parâmetros e navegação com preservação de mensagens.
Passo 1: Dependências e Configuração
No pom.xml, certifique-se de ter as dependências do Faces e PrimeFaces (as mesmas já usadas no CRUD):
<dependencies>
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId>
<version>3.15.7</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
</dependencies>
Em application.properties, defina a tela inicial e tema:
quarkus.faces.welcome-files=index.xhtml
quarkus.primefaces.theme=nova-light
Este laboratório utiliza a entidade Filme do capítulo anterior. Os conceitos aplicam-se a qualquer entidade do CritiqueHub (Livro, Jogo, etc).
Passo 2: Template Base (layout-base.xhtml)
Crie src/main/resources/META-INF/resources/templates/layout-base.xhtml:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<h:head>
<title>
CritiqueHub – <ui:insert name="title">Título</ui:insert>
</title>
<h:outputStylesheet>
.container { max-width: 1200px; margin: 0 auto; padding: 1rem; }
.header, .footer { padding: .75rem 0; color: #333; }
.breadcrumb { margin: .5rem 0 1rem; font-size: .9rem; color: #666; }
.content { background: #fff; padding: 1rem; border-radius: .5rem; }
</h:outputStylesheet>
</h:head>
<h:body>
<div class="container">
<div class="header">
<h1 style="margin: 0;">CritiqueHub</h1>
<small>Plataforma social de descoberta cultural</small>
<ui:include src="/templates/menu.xhtml"/>
</div>
<div class="breadcrumb">
<ui:insert name="breadcrumbs">Início</ui:insert>
</div>
<p:messages id="msgs" autoUpdate="true" closable="true"/>
<div class="content">
<ui:insert name="content">Conteúdo</ui:insert>
</div>
<div class="footer">
<hr/>
<span>
© <h:outputText value="#{java.time.Year.now()}"/>
CritiqueHub
</span>
</div>
</div>
</h:body>
</html>
Crie o menu em src/main/resources/META-INF/resources/templates/menu.xhtml:
<ui:fragment xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<p:menubar style="margin-top: .75rem;">
<p:menuitem value="Início" outcome="index" icon="pi pi-home"/>
<p:submenu label="Catálogo" icon="pi pi-list">
<p:menuitem value="Filmes" outcome="filmes" icon="pi pi-video"/>
<p:menuitem value="Livros" outcome="livros" icon="pi pi-book"/>
<p:menuitem value="Jogos" outcome="jogos" icon="pi pi-desktop"/>
</p:submenu>
<p:menuitem value="Sobre" outcome="sobre" icon="pi pi-info-circle"/>
</p:menubar>
</ui:fragment>
O atributo outcome em p:menuitem usa navegação implícita: outcome="filmes" navega para filmes.xhtml automaticamente.
Passo 3: Página de Lista usando o Template (filmes.xhtml)
Crie src/main/resources/META-INF/resources/filmes.xhtml com templating e filtros mantidos em View Scope:
<ui:composition template="/templates/layout-base.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://primefaces.org/ui"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:define name="title">Filmes</ui:define>
<ui:define name="breadcrumbs">
Início / Catálogo / Filmes
</ui:define>
<ui:define name="content">
<h:form id="formFilmes">
<p:panel header="Catálogo de Filmes"
style="margin-bottom: 1rem;">
<h:panelGrid columns="4"
columnClasses="label,value,button,button"
cellpadding="5">
<h:outputText value="Título:"/>
<p:inputText
value="#{navegacaoFilmeBean.filtroTitulo}"
placeholder="Busque por título"/>
<p:commandButton value="Buscar"
action="#{navegacaoFilmeBean.buscar}"
update="formFilmes tabela"
icon="pi pi-search"/>
<p:commandButton value="Limpar"
action="#{navegacaoFilmeBean.limpar}"
update="formFilmes tabela"
icon="pi pi-refresh"/>
</h:panelGrid>
</p:panel>
<p:dataTable id="tabela"
value="#{navegacaoFilmeBean.filmes}" var="f"
paginator="true" rows="10"
emptyMessage="Nenhum filme encontrado">
<p:column headerText="Título" sortBy="#{f.titulo}">
<h:outputLink value="filme-detalhe.xhtml">
<f:param name="id" value="#{f.id}"/>
<h:outputText value="#{f.titulo}"/>
</h:outputLink>
</p:column>
<p:column headerText="Diretor" sortBy="#{f.diretor}">
<h:outputText value="#{f.diretor}"/>
</p:column>
<p:column headerText="Ano" sortBy="#{f.ano}"
style="width: 100px; text-align:center;">
<h:outputText value="#{f.ano}"/>
</p:column>
<p:column headerText="Ações"
style="width: 160px; text-align:center;">
<p:commandButton value="Detalhes"
icon="pi pi-external-link"
action="#{navegacaoFilmeBean.irParaDetalhe(f.id)}"
process="@this"/>
</p:column>
</p:dataTable>
</h:form>
</ui:define>
</ui:composition>
Duas formas de navegação implementadas:
- Link direto:
<h:outputLink>com<f:param>— gera URL com parâmetro - Método de ação:
action="#{navegacaoFilmeBean.irParaDetalhe(f.id)}"— navegação programática
O bean @ViewScoped preserva o filtro entre requisições AJAX.
Passo 4: Página de Detalhes com viewParam e viewAction
Crie src/main/resources/META-INF/resources/filme-detalhe.xhtml:
<ui:composition template="/templates/layout-base.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:p="http://primefaces.org/ui"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<f:metadata>
<f:viewParam name="id" value="#{detalheFilmeBean.id}"/>
<f:viewAction action="#{detalheFilmeBean.preparar}"/>
</f:metadata>
<ui:define name="title">Detalhes do Filme</ui:define>
<ui:define name="breadcrumbs">
Início / Catálogo /
<h:outputText value="#{detalheFilmeBean.tituloBreadcrumb}"/>
</ui:define>
<ui:define name="content">
<h:panelGroup rendered="#{not empty detalheFilmeBean.filme}">
<p:panel header="#{detalheFilmeBean.filme.titulo}">
<h:panelGrid columns="2"
columnClasses="label,value" cellpadding="5">
<h:outputText value="Diretor:"/>
<h:outputText
value="#{detalheFilmeBean.filme.diretor}"/>
<h:outputText value="Ano:"/>
<h:outputText
value="#{detalheFilmeBean.filme.ano}"/>
<h:outputText value="Gênero:"/>
<h:outputText
value="#{detalheFilmeBean.filme.genero}"/>
<h:outputText value="Sinopse:"/>
<h:outputText
value="#{detalheFilmeBean.filme.sinopse}"/>
</h:panelGrid>
</p:panel>
<p:commandButton value="Voltar" icon="pi pi-arrow-left"
outcome="filmes"/>
</h:panelGroup>
<h:panelGroup rendered="#{empty detalheFilmeBean.filme}">
<p:message severity="warn" summary="Filme não encontrado"/>
<p:commandButton value="Voltar" icon="pi pi-arrow-left"
outcome="filmes"/>
</h:panelGroup>
</ui:define>
</ui:composition>
Fluxo de carregamento da página:
- URL recebida:
filme-detalhe.xhtml?id=5 f:viewParaminjeta5emdetalheFilmeBean.idf:viewActionexecutadetalheFilmeBean.preparar()- Método
preparar()carrega o filme do banco - Página é renderizada com os dados carregados
Passo 5: Beans de Navegação — View Scope e Flash Messages
Crie NavegacaoFilmeBean (lista) e DetalheFilmeBean (detalhe). Ambos usam a entidade Filme do CRUD. O uso de Flash mantém mensagens após redirect:
// NavegacaoFilmeBean.java
package br.upf.ads175.critiquehub.controller;
import br.upf.ads175.critiquehub.entity.Filme;
import br.upf.ads175.critiquehub.service.FilmeService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.List;
@Named
@ViewScoped
public class NavegacaoFilmeBean implements Serializable {
@Inject
FilmeService filmeService;
private String filtroTitulo;
private List<Filme> filmes;
@PostConstruct
void init() {
filmes = filmeService.listarTodos();
}
public void buscar() {
if (filtroTitulo == null || filtroTitulo.isBlank()) {
filmes = filmeService.listarTodos();
} else {
filmes = filmeService.buscarPorTitulo(filtroTitulo);
}
}
public void limpar() {
filtroTitulo = "";
filmes = filmeService.listarTodos();
}
// Navegação programática com redirect e flash messages
public String irParaDetalhe(Long id) {
FacesContext.getCurrentInstance()
.getExternalContext()
.getFlash()
.setKeepMessages(true);
return "filme-detalhe?faces-redirect=true"
+ "&includeViewParams=true&id=" + id;
}
// getters/setters
public String getFiltroTitulo() { return filtroTitulo; }
public void setFiltroTitulo(String filtroTitulo) {
this.filtroTitulo = filtroTitulo;
}
public List<Filme> getFilmes() { return filmes; }
}
// DetalheFilmeBean.java
package br.upf.ads175.critiquehub.controller;
import br.upf.ads175.critiquehub.entity.Filme;
import br.upf.ads175.critiquehub.service.FilmeService;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
@Named
@ViewScoped
public class DetalheFilmeBean implements Serializable {
@Inject
FilmeService filmeService;
private Long id; // mapeado por f:viewParam
private Filme filme;
public void preparar() {
if (id != null) {
filme = filmeService.buscarPorId(id);
if (filme == null) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN,
"Filme não encontrado", null));
}
}
}
// Exemplo de ação com redirect e Flash message
public String favoritar() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO,
"Favoritado!",
filme != null ? filme.getTitulo() : ""));
FacesContext.getCurrentInstance()
.getExternalContext()
.getFlash()
.setKeepMessages(true);
return "filme-detalhe?faces-redirect=true"
+ "&includeViewParams=true";
}
public String getTituloBreadcrumb() {
return filme != null ? filme.getTitulo() : "Filme";
}
// getters/setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Filme getFilme() { return filme; }
}
Pontos importantes da implementação:
@ViewScopedpreserva estado (filtros) durante interações AJAXFlash.setKeepMessages(true)mantém mensagens após redirectfaces-redirect=trueforça nova requisição GETincludeViewParams=truepreserva parâmetros na URL
Passo 6: Navegação Explícita (Opcional) com faces-config.xml
Em aplicações legadas ou cenários específicos, você pode centralizar regras de navegação em faces-config.xml:
<!-- src/main/webapp/WEB-INF/faces-config.xml -->
<faces-config xmlns="https://jakarta.ee/xml/ns/jakartaee"
version="4.0">
<navigation-rule>
<from-view-id>/filmes.xhtml</from-view-id>
<navigation-case>
<from-outcome>detalhe</from-outcome>
<to-view-id>/filme-detalhe.xhtml</to-view-id>
<redirect include-view-params="true"/>
</navigation-case>
</navigation-rule>
</faces-config>
Com essa configuração, um método pode simplesmente retornar "detalhe" para navegar.
A navegação explícita é útil para aplicações grandes onde centralizar regras facilita manutenção e documentação.
Passo 7: Composite Component para Breadcrumb (Opcional)
Componentes composite permitem criar componentes reutilizáveis personalizados:
<!-- src/main/resources/META-INF/resources/components/breadcrumb.xhtml -->
<ui:component xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:composite="http://xmlns.jcp.org/jsf/composite">
<composite:interface>
<composite:attribute name="itens" required="true"/>
</composite:interface>
<composite:implementation>
<h:panelGroup styleClass="breadcrumb">
<ui:repeat value="#{cc.attrs.itens}" var="i" varStatus="st">
<h:outputText value="#{i}"/>
<h:outputText value=" / " rendered="#{!st.last}"/>
</ui:repeat>
</h:panelGroup>
</composite:implementation>
</ui:component>
Uso numa página filha:
<ui:define name="breadcrumbs">
<components:breadcrumb itens="#{['Início','Catálogo','Filmes']}"/>
</ui:define>
View Scope em Ação: Filtros Avançados
Para demonstrar a importância do @ViewScoped, implementaremos filtros avançados que mantêm estado durante interações AJAX.
Por que View Scope é Necessário?
| Funcionalidade | Motivo para View Scope |
|---|---|
| Filtros persistentes durante paginação | Paginação usa AJAX — dados precisam ser mantidos |
| Estado de painéis (expandido/colapsado) | Preferências visuais devem persistir na página |
| Busca incremental | Usuário refina busca sem perder resultados anteriores |
| Seleções múltiplas | Checkboxes e seleções mantidos entre requisições |
Implementação:
// No NavegacaoFilmeBean
private boolean painelAvancadoAberto = false;
private String filtroDiretor;
public void alternarPainel() {
painelAvancadoAberto = !painelAvancadoAberto;
}
public void aplicarAvancado() {
// Combina filtros (ex.: título + diretor)
if ((filtroTitulo == null || filtroTitulo.isBlank())
&& (filtroDiretor == null || filtroDiretor.isBlank())) {
filmes = filmeService.listarTodos();
} else {
filmes = filtroTitulo != null && !filtroTitulo.isBlank()
? filmeService.buscarPorTitulo(filtroTitulo)
: Filme.list("LOWER(diretor) LIKE LOWER(?1)",
"%" + filtroDiretor + "%");
}
}
public boolean isPainelAvancadoAberto() { return painelAvancadoAberto; }
public String getFiltroDiretor() { return filtroDiretor; }
public void setFiltroDiretor(String filtroDiretor) {
this.filtroDiretor = filtroDiretor;
}
<!-- Fragmento extra em filmes.xhtml dentro de ui:define content -->
<p:panel header="Filtros Avançados" toggleable="true"
collapsed="#{!navegacaoFilmeBean.painelAvancadoAberto}">
<h:panelGrid columns="3" cellpadding="5">
<h:outputText value="Diretor:"/>
<p:inputText value="#{navegacaoFilmeBean.filtroDiretor}"
placeholder="Ex.: Meirelles"/>
<p:commandButton value="Aplicar" icon="pi pi-filter"
action="#{navegacaoFilmeBean.aplicarAvancado}"
update="formFilmes tabela"/>
</h:panelGrid>
</p:panel>
Comparação com @RequestScoped:
Com @RequestScoped, cada requisição AJAX cria um novo bean, resultando em:
- Perda de filtros ao paginar
- Painéis voltam ao estado inicial
- Busca incremental impossível
- Performance degradada (criação constante de beans)
Com @ViewScoped:
- Estado mantido durante toda interação
- Experiência fluida para o usuário
- Performance otimizada
Resumo
Neste capítulo, você implementou um sistema completo de templating e navegação para aplicações Jakarta Faces:
Templating com Facelets:
- Template base reutilizável com áreas customizáveis
- Fragmentos compartilhados (menu, rodapé)
- Eliminação de duplicação de código
- Manutenção centralizada do layout
Sistema de Navegação:
- Navegação implícita (simples e direta)
- Navegação explícita (centralizada em faces-config.xml)
- Navegação programática com redirect (padrão PRG)
- Passagem de parâmetros via URL com
f:viewParam
Gerenciamento de Estado:
@ViewScopedpara manter estado durante interações AJAX- Flash Scope para preservar mensagens após redirect
f:viewActionpara pré-carregamento de dados
Principais aprendizados:
- Use templating para garantir consistência visual
- Aplique redirect após operações de escrita (PRG pattern)
- Escolha
@ViewScopedpara páginas com interações AJAX - Use Flash Scope para mensagens após redirect
- Combine
f:viewParamef:viewActionpara páginas de detalhes
Exercícios Extras
- Adapte o template para ter uma área de sidebar e crie uma página que a utilize com recomendações personalizadas.
- Converta a navegação para usar apenas outcomes explícitos via
faces-config.xmle compare com a abordagem implícita. - Crie um composite component de "cartão de item cultural" e utilize-o na lista de filmes.
- Modifique
DetalheFilmeBeanpara exibir recomendações de filmes semelhantes carregadas emf:viewAction. - Implemente uma página "Minhas Listas" reutilizando o template e mantenha filtros/paginação com
@ViewScoped.