Pular para o conteúdo principal

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:viewParam e f:viewAction para 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 template
  • ui:insert: Marca áreas do template que serão preenchidas
  • ui:define: Fornece conteúdo para as áreas do template
  • ui: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 para filmes.xhtml
  • Navegação Explícita: Regras centralizadas em faces-config.xml
  • Redirecionamento: faces-redirect=true evita re-submissão de formulários
  • Parâmetros: f:viewParam e includeViewParams=true para passar dados na URL
observação

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

O sistema de navegação controla qual página será exibida após uma ação do usuário.

O outcome retornado mapeia diretamente para o arquivo XHTML:

public String listar() {
return "filmes"; // Navega para filmes.xhtml
}

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)
dica

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=5detalheBean.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:

EscopoCiclo de VidaUso Recomendado
@RequestScopedUma requisiçãoOperações simples sem estado
@ViewScopedEnquanto na mesma páginaPáginas com AJAX, filtros, tabs
@SessionScopedToda a sessão do usuárioDados globais (usuário logado)
FlashDuas requisições (redirect)Mensagens após redirect
important

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
observação

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>
&copy; <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>
dica

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>
important

Duas formas de navegação implementadas:

  1. Link direto: <h:outputLink> com <f:param> — gera URL com parâmetro
  2. 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>
important

Fluxo de carregamento da página:

  1. URL recebida: filme-detalhe.xhtml?id=5
  2. f:viewParam injeta 5 em detalheFilmeBean.id
  3. f:viewAction executa detalheFilmeBean.preparar()
  4. Método preparar() carrega o filme do banco
  5. 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; }
}
dica

Pontos importantes da implementação:

  • @ViewScoped preserva estado (filtros) durante interações AJAX
  • Flash.setKeepMessages(true) mantém mensagens após redirect
  • faces-redirect=true força nova requisição GET
  • includeViewParams=true preserva 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.

observação

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?

FuncionalidadeMotivo para View Scope
Filtros persistentes durante paginaçãoPaginação usa AJAX — dados precisam ser mantidos
Estado de painéis (expandido/colapsado)Preferências visuais devem persistir na página
Busca incrementalUsuário refina busca sem perder resultados anteriores
Seleções múltiplasCheckboxes 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>
aviso

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:

  • @ViewScoped para manter estado durante interações AJAX
  • Flash Scope para preservar mensagens após redirect
  • f:viewAction para pré-carregamento de dados
important

Principais aprendizados:

  1. Use templating para garantir consistência visual
  2. Aplique redirect após operações de escrita (PRG pattern)
  3. Escolha @ViewScoped para páginas com interações AJAX
  4. Use Flash Scope para mensagens após redirect
  5. Combine f:viewParam e f:viewAction para páginas de detalhes

Exercícios Extras

  1. Adapte o template para ter uma área de sidebar e crie uma página que a utilize com recomendações personalizadas.
  2. Converta a navegação para usar apenas outcomes explícitos via faces-config.xml e compare com a abordagem implícita.
  3. Crie um composite component de "cartão de item cultural" e utilize-o na lista de filmes.
  4. Modifique DetalheFilmeBean para exibir recomendações de filmes semelhantes carregadas em f:viewAction.
  5. Implemente uma página "Minhas Listas" reutilizando o template e mantenha filtros/paginação com @ViewScoped.

Referências