Paginação — Dividir a lista em páginas editar arquivo

0:00 / 0:00

A tabela departamento.produto cresceu: 75 registros já em desenvolvimento, e isso vai escalar em produção. O setPageSize(9999) do CRUD básico despeja tudo de uma vez — renderização lenta, scroll infinito, experiência ruim. A Paginação resolve: setPageSize(50), um navegador de páginas no rodapé, e integração com o QuickSearch pra voltar à página 1 a cada nova busca.

4 mudanças pequenas em um único arquivo (DepartamentoProdutoList). Nada no Bean, nada no DAO, nada no Manager.

CÓDIGO COMPLETO — DepartamentoProdutoList
package br.xt.app.departamento.produto;

import br.jasap.core.Effect;
import br.jasap.effect.Response;
import br.jasap.gui.JasapPage;
import br.jasap.gui.ListColumn;
import br.jasap.gui.ListLine;
import br.jasap.gui.ListView;
import br.jasap.gui.Bar;
import br.jasap.gui.Button;
import br.jasap.gui.Table;
import br.jasap.gui.Toast;
import br.jasap.gui.form.Text;
import br.jasap.util.JasapFunctions;
import br.jasap.util.Js;
import br.jasap.util.ModalConfig;
import br.jasap.util.exceptions.SQLConstraintException;
import br.xt.acore.view.IconButton;
import br.xt.acore.view.XtPage;

public class DepartamentoProdutoList extends DepartamentoProdutoAction {

    // ... execute(), render(), window() (sem alteração)

    protected Text qs_produto = null;
    public String qs_produto() throws Exception {
        qs_produto = ui().text(DepartamentoProdutoBean.QS_PRODUTO)
                .setLabel("Consulta")
                .setStyle("width:400;height:30px;font-size:14px")
                .setMaxlength(300)
                .setValue(getFiltro().getQs_produto())
                .setOnkeyup(Js.pressEnter(
                        link(QuickSearch.class)
                                .putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
                                .putInteger(lView().getPAGE(), 0)
                                .ajax()));

        // ... restante da barra de busca (sem alteração)
    }

    public Bar br() throws Exception {
        Button cmd_novo = ui().button("  Novo Registro  ").setCss("btn btn-success btn-lg").setNoSize()
                .setOnClick(link(DepartamentoProdutoForm.ShowInsert.class)
                        .modal(new ModalConfig().setWidth("750").setHeight("570")
                                .setOnCloseURL(url(Sort.class))));

        return ui().bar()
                .addCenter(lView().nav(lView().getNavForm()))
                .addRight(cmd_novo);
    }

    private ListView lv = null;
    public ListView lView() throws Exception {
        if (lv == null) {
            lv = ui().lView();

            String orderBy = getSession().getString(LIST.concat(lv.getORDER_BY()), getInput());
            if (orderBy == null) orderBy = DepartamentoProdutoBean.NOME_PRODUTO;
            lv.setSortAct(url(Sort.class))
              .setPageAction(url(Sort.class))
              .setPage(getSession().getInteger(LIST.concat(lv.getPAGE()), getInput()))
              .setOrderBy(orderBy)
              .setSort(getSession().getString(LIST.concat(lv.getSORT()), getInput()))
              .ajax();

            lv.setPageSize(50);

            lv.setFiltro(getFiltro());
            getFactory().departamento().proModel().daoList(lv.getData());

            // ... colunas e while (sem alteração)
        }
        return lv;
    }

    // ... Sort, QuickSearch, DeleteFromList (sem alteração)

    // ... constantes LIST, FILTRO, CONFIRM_LIST (sem alteração)

}

Mudanças nesta sequência: 4 alterações no mesmo arquivo — .putInteger(PAGE, 0) no onkeyup do qs_produto, .addCenter(...) no br(), .setPageAction + .setPage na chain do lv, e setPageSize(9999) virou setPageSize(50). Nenhum import novo.

O mecanismo — 4 mudanças pra paginar

A paginação é coordenação entre 3 coisas: quantos registros por página, qual página mostrar, e como navegar. A quarta mudança é uma integração com o QuickSearch — sem ela, buscar na página 3 mostraria resultados da página 3 da nova busca em vez de começar do início.

MudançaOndePapel
setPageSize(50)lView()Define tamanho da página — antes era 9999 (uma "página" só)
setPageAction + setPagechain do lvRegistra a action que redesenha ao trocar de página e lê a página atual da sessão/input
nav(getNavForm())br()Renderiza os botões "Anterior / Próxima / N de M" no rodapé
.putInteger(PAGE, 0)onkeyup do qs_produtoZera a página no momento em que o usuário digita a busca

As 3 primeiras mudanças fazem a paginação existir. A quarta garante que a paginação se comporta bem com a busca rápida.

Mudança 1 — setPageSize(9999)setPageSize(50)
lv.setPageSize(50);

Uma linha, um número. Mas é a mudança mais fundamental — define quantos registros o DAO busca de uma vez e quantos o HTML renderiza.

Antes: setPageSize(9999) — truque do CRUD básico pra desligar a paginação. O DAO fazia LIMIT 9999, que na prática trazia tudo. Com 75 registros, trazia os 75. Com 10.000, traria 9.999 e silenciosamente perderia 1 — pior que paginar errado.

Depois: setPageSize(50) — DAO faz LIMIT 50 OFFSET ?. Com 75 registros, o framework calcula 2 páginas (50 + 25). O getNavForm() usa esse cálculo pra mostrar "Página 1 de 2".

Por que 50 e não 10 ou 100? Convenção do XT em produção (ver CpesList, PusuList, LabProdutoList). 50 é um equilíbrio: ocupa a tela bem em resoluções comuns, mantém o usuário rolando no máximo uma página, e não estoura na performance do DAO pra tabelas com índices razoáveis.

Mudança 2 — setPageAction + setPage na chain do lv
lv.setSortAct(url(Sort.class))
  .setPageAction(url(Sort.class))
  .setPage(getSession().getInteger(LIST.concat(lv.getPAGE()), getInput()))
  .setOrderBy(orderBy)
  .setSort(getSession().getString(LIST.concat(lv.getSORT()), getInput()))
  .ajax();

Duas linhas adicionadas na chain do lv, logo depois do setSortAct. Ambas são o mínimo pra paginação funcionar.

.setPageAction(url(Sort.class))

Registra a action que será chamada ao trocar de página. Quando o usuário clica "Próxima" no nav(), o framework dispara essa action pra redesenhar a lista.

Por que Sort.class? Reaproveitamento. A inner class Sort já atualiza header + body + navegação — exatamente o que precisa acontecer ao trocar de página. Criar uma ChangePage.class separada seria duplicação — o Sort já faz o trabalho, só com input diferente (PAGE em vez de ORDER_BY/SORT).

.setPage(getSession().getInteger(LIST.concat(lv.getPAGE()), getInput()))

Lê a página atual em duas fontes: input (prioridade) e sessão (fallback).

  • O lv.getPAGE() retorna a string "PAGE" — o nome do parâmetro que o framework usa.
  • LIST.concat(lv.getPAGE()) gera "XT.PAINEL_CONTROLE.ACESSO_MODULO.DEPARTAMENTO__PRODUTO/__LIST/PAGE" — chave única por lista. Sem o namespace do LIST, duas listas na mesma tela colidiriam.
  • getSession().getInteger(chave, getInput()) — padrão Jasap: primeiro olha o input, depois a sessão. Se o usuário clica "Página 3", o link manda PAGE=3 no input; o framework lê e grava na sessão. Próximo render, a sessão já tem o valor, mesmo sem input.

É o mesmo padrão de setOrderBy e setSort. Qualquer estado de navegação da lista segue essa dupla camada.

Mudança 3 — lView().nav(...) no br()
public Bar br() throws Exception {
    Button cmd_novo = ui().button("  Novo Registro  ") /* ... */;

    return ui().bar()
            .addCenter(lView().nav(lView().getNavForm()))
            .addRight(cmd_novo);
}

O br() é a barra inferior da lista. Antes só tinha o botão "Novo Registro" à direita. Agora ganha um navegador de páginas no centro.

lView().getNavForm()

Retorna o ID do form HTML que o framework usa pra envolver os botões de navegação (<form id="navform_XXX">). Esse ID é preciso pra que os botões "Anterior / Próxima" peguem o valor do PAGE corretamente no submit AJAX.

lView().nav(...)

Renderiza o componente visual: botões "Anterior", "Próxima", e texto "Página N de M". Bootstrap, estilo XT. Recebe o ID do form como parâmetro pra amarrar os cliques ao submit certo.

.addCenter(...)

O Bar tem 3 regiões: addLeft, addCenter, addRight. O navegador no centro, botão "Novo Registro" na direita — layout clássico de lista paginada.

Sem essa mudança: a paginação até funcionaria no backend (setPageSize(50) cortaria a lista), mas o usuário ficaria preso na página 1 — sem botão pra avançar. O nav() é a UI pra controlar o estado que a Mudança 2 lê.

Mudança 4 — .putInteger(PAGE, 0) no onkeyup do qs_produto
.setOnkeyup(Js.pressEnter(
        link(QuickSearch.class)
                .putScript(DepartamentoProdutoBean.QS_PRODUTO, Js.SELF_VALUE)
                .putInteger(lView().getPAGE(), 0)
                .ajax()));

Uma linha no link do QuickSearch: adiciona o parâmetro PAGE=0 ao submit. Sem isso, a busca rápida teria um bug visual grave.

O cenário do bug (sem a mudança)

  1. Usuário está na página 3 da lista, vendo produtos 101-150
  2. Digita "notebook" na barra de busca rápida
  3. O filtro corta drasticamente — só sobra 5 produtos no total
  4. O framework recebe qs_produto="notebook" mas mantém PAGE=3 (fonte: sessão, que ainda tem o valor antigo)
  5. DAO faz LIMIT 50 OFFSET 100 — offset maior que o total — retorna zero registros
  6. Usuário vê lista vazia, pensa "não tem notebook". Erro.

A correção

.putInteger(lView().getPAGE(), 0) adiciona PAGE=0 ao input do submit. Como o setPage da Mudança 2 prioriza input sobre sessão, o framework usa 0 — volta pra primeira página. A busca mostra os resultados começando do início.

Por que no onkeyup e não no QuickSearch.execute()? Duas abordagens possíveis:

  • Via input (escolhido).putInteger(PAGE, 0) no link: explícito, visível no código do form, uma linha só.
  • Via sessão — método resetPage() que grava uma flag, consumida no lView(): helper reutilizável, mas adiciona 3 elementos no código (método + constante + bloco consumer).

Pra só a paginação, a via input é suficiente e minimalista. A via sessão só compensa quando múltiplas actions precisam resetar página (Tabs de status, filtros avançados) — aí o helper justifica o custo. Ver a seção "O que NÃO faz parte da Paginação" abaixo.

O que NÃO faz parte da Paginação

Três coisas parecem fazer parte da paginação, mas são infraestrutura pra features futuras:

ItemFaz parte?Quando vem
Método resetPage()NãoTabs de status
Constante RESET_PAGENãoTabs de status
Bloco consumer if (isSet(RESET_PAGE)) no lView()NãoTabs de status
Inner class ChangePageNãoNunca — reaproveitamos Sort
Registro no ManagerNãoNunca — Sort já está registrado

Por que resetPage() não entrou aqui: o .putInteger(PAGE, 0) da Mudança 4 já resolve o único caso da paginação — reset na busca rápida. O resetPage() vira útil quando outras actions precisam resetar página sem conseguir injetar o parâmetro no link: tabs (TabAtivos.execute()), botões "Aplicar Filtros" / "Limpar Filtros", etc.

Adicionar o helper agora, sem ter quem chame, deixaria código morto no projeto — o bloco consumer nunca dispararia, o método nunca seria chamado. Memória do curso: cada episódio só adiciona o que o escopo exige. O que vem a seguir adiciona o que a seguir exige.

Por que ChangePage não existe: nas primeiras versões de listas paginadas do XT, havia uma inner class específica pra trocar de página. Evoluiu pra reaproveitar o Sort — que já atualiza header + body + navegação. ChangePage seria duplicação. O atalho é .setPageAction(url(Sort.class)).

Resumo — o que mudou
ArquivoTipoEdição
DepartamentoProdutoListeditar4 mudanças — .putInteger(PAGE, 0) no onkeyup, .addCenter(nav) no br(), .setPageAction + .setPage na chain do lv, e setPageSize(9999)setPageSize(50)
DepartamentoManagerNada — Sort já estava registrado desde a feature de ordenação
DepartamentoProdutoBeanNada
DepartamentoProdutoDAONada — o LIMIT / OFFSET é gerenciado pelo ListView automaticamente via lv.getData()

1 arquivo editado, nenhuma mudança no banco, nenhum import novo, nenhum arquivo Java novo. Resultado: lista dividida em páginas de 50 registros, navegador "Anterior / Próxima" no rodapé, e busca rápida voltando à página 1 a cada pesquisa.