Pragiva — Tecnologia na prática

Seu primeiro cadastro MVC, do dicionário à tela

Primeiro cadastro MVC do zero: uma tabela nova, os três artigos da série num fonte só, e o que dá errado na primeira compilação.

Protheus Tutorial 5 min de leitura

Assista o vídeo

Resposta rápida

O que sai no fim
um browse funcionando, com pesquisar, incluir, alterar e excluir, sobre uma tabela nova.
O que precisa existir antes
a tabela e os campos cadastrados no dicionário — sem isso não há tela.
O fonte
PGV010.prw, um MVC didático completo — função principal mais três Static Function.
A ordem de execução
dicionário, depois o fonte compila, depois o menu chama.
O que mais demora
não é escrever o ADVPL, é errar uma das armadilhas da seção final.
Nesta página6 seções

Antes de começar

  • Acesso ao Configurador do ambiente de desenvolvimento, com permissão para criar tabela e campo no dicionário (SX2/SX3).
  • Um AppServer de desenvolvimento com RPO próprio, separado de homologação e produção.
  • TDS ou TDS-VSCode configurado contra esse ambiente, com o include padrão do release.
  • Os três artigos anteriores da série lidos — MenuDef, ModelDef e ViewDef — porque este texto não reexplica cada função em profundidade.

Os três artigos anteriores desta série explicaram MenuDef, ModelDef e ViewDef cada um isolado. Este junta os três num fonte que compila e abre uma tela de verdade, na ordem em que você realmente vai trabalhar: primeiro o dicionário, depois o código, depois a lista do que costuma quebrar na estreia.

O dicionário primeiro

MVC lê o SX3 para montar a estrutura de campos — é o que FWFormStruct faz por dentro, tanto para o modelo quanto para a view. Sem tabela e campos cadastrados, as duas chamadas de FWFormStruct do fonte não têm o que carregar, e a tela simplesmente não existe.

Crie a tabela Z50 no Configurador com estes quatro campos:

CampoTipoTamanhoO que é
Z50_FILIALCaractere8Filial do registro — todo campo _FILIAL de uma tabela própria segue esse padrão
Z50_CODCaractere6Código do tipo de contrato — metade da chave primária
Z50_DESCCaractere40Descrição do tipo de contrato
Z50_STATUSCaractere11 ativo, 2 inativo — combo de dicionário

O índice é Z50_FILIAL+Z50_COD, e é essa combinação que SetPrimaryKey recebe no ModelDef. Z50_STATUS precisa do X3_CBOX preenchido como 1=Ativo;2=Inativo — é essa lista que a legenda do browse lê para decidir qual cor mostrar para cada valor.

A função principal e o browse

Com o dicionário pronto, a função principal monta o browse:

ADVPL / TLPP
oBrowse := FWMBrowse():New()
oBrowse:SetAlias("Z50")
oBrowse:SetDescription("Tipos de contrato")

// O browse precisa saber de QUE FONTE ler o MenuDef. E o nome do
// arquivo, nao o da funcao.
oBrowse:SetMenuDef("PGV010")

oBrowse:AddLegend("Z50_STATUS == '1'", "GREEN", "Ativo")
oBrowse:AddLegend("Z50_STATUS == '2'", "RED", "Inativo")

oBrowse:Activate()

Repare que SetMenuDef recebe "PGV010", o nome do fonte — não "MenuDef", não o nome da função principal. É a mesma regra de FWLoadModel que o artigo do ModelDef já mostrou: por dentro, o framework monta uma STATICCALL contra o argumento que você passou, então esse argumento precisa ser exatamente o nome do arquivo onde as três Static Function moram. Cada AddLegend recebe a expressão que testa o campo, a cor e o texto — e é por isso que o X3_CBOX do campo Z50_STATUS precisa bater com os mesmos dois valores usados aqui.

As três Static Function, juntas

MenuDef devolve o aRotina com as cinco opções padrão — pesquisar, visualizar, incluir, alterar, excluir —, todas as quatro últimas apontando para a mesma ação, "VIEWDEF.PGV010", distinguidas só pelo número de OPERATION. O artigo dedicado a essa função já mostrou por que o array tem seis posições fixas e por que a escala de OPERATION não é a mesma dos MODEL_OPERATION_*; aqui o que importa é que o valor citado no ACTION é, de novo, o nome do fonte.

ModelDef monta a estrutura a partir do dicionário, instancia o modelo e fecha a chave primária:

ADVPL / TLPP
Local oStruct := FWFormStruct(1, "Z50")

oModel := MPFormModel():New("PGV010M")

oModel:AddFields("Z50MASTER", /*cOwner*/, oStruct)

oModel:SetPrimaryKey({"Z50_FILIAL", "Z50_COD"})

MPFormModel é quem grava sem uma linha de RecLock no fonte: ela herda de FWFormModel e só troca os valores padrão do construtor, já entregando o bloco de gravação e o de cancelamento prontos. O artigo do ModelDef detalha essa herança linha a linha; aqui basta saber que é essa escolha — MPFormModel em vez de FWFormModel puro — que faz o cadastro gravar de verdade.

ViewDef carrega o modelo pelo nome do fonte, monta a estrutura de novo — agora com o segundo parâmetro 2 — e liga os dois lados:

ADVPL / TLPP
Local oModel := FWLoadModel("PGV010")
Local oStruct := FWFormStruct(2, "Z50")

oView := FWFormView():New()
oView:SetModel(oModel)

oView:AddField("VIEW_Z50", oStruct, "Z50MASTER")

oView:CreateHorizontalBox("TELA", 100)

oView:SetOwnerView("VIEW_Z50", "TELA")

O terceiro parâmetro de AddField é o que amarra o componente da tela ao componente do modelo — aqui os dois IDs são diferentes de propósito, para deixar claro que não precisam coincidir. O box e o SetOwnerView fecham o fonte: sem eles, o próximo item desta lista é exatamente o que você vai ver.

Compilar e chamar

Com o dicionário pronto e o fonte salvo, ele precisa virar objeto no RPO do ambiente antes de qualquer teste — o processo de compilar pela IDE ou pela linha de comando está descrito em Compilar AdvPL e TLPP pela linha de comando, e a organização geral de um fonte AdvPL, com include e função principal, em Estrutura de um fonte AdvPL. Depois de compilado, o cadastro de rotinas do módulo aponta para U_PGV010 — o User Function some do menu se o objeto não estiver no RPO, mesmo com o fonte salvo sem erro de sintaxe.

O que dá errado na estreia

Cinco erros concentram quase toda a primeira tentativa mal-sucedida.

Campo existe e a tela abre vazia. AddField ou AddGrid sem SetOwnerView correspondente compila e roda sem aviso nenhum — o componente foi criado, só não recebeu onde aparecer. Se um campo do modelo simplesmente não aparece na tela, comece checando esse par antes de suspeitar do dicionário.

Erro de tipo em vez de erro de lógica. Passar FWFormStruct(2,...) para AddFields é erro de tipo declarado — o PARAMTYPE do método exige um FWFormModelStruct. Só que ele não aparece na compilação: PARMTYPE.CH expande a declaração para uma checagem de ValType em tempo de execução, e o que se vê é a rotina estourando ao abrir, com um erro de classe esperada no parâmetro. AddFields espera a estrutura do modelo — FWFormStruct(1,...) —, nunca a de view.

SetMenuDef ou FWLoadModel com o nome errado. Os dois recebem o nome do FONTE, não o nome de uma função nem o ID interno do modelo. Trocar "PGV010" por "ModelDef" ou por "PGV010M" nessas duas chamadas faz a macro-execução por trás delas procurar Static Function num lugar que não existe.

Campo no modelo e ausente na view, ou o contrário. AddFields determina o que é gravado; AddField da view, o que é editável. Campo fora do AddField nunca é editado, mesmo correto no modelo — e um AddField sem campo no modelo falha ao se ligar a um componente que não existe.

Tabela ou campo fora do SX3. É a causa mais comum, e o sintoma não aponta para o dicionário: FWFormStruct devolve a estrutura sem o campo, e o efeito visível é igual ao de faltar SetOwnerView — o campo não aparece. A diferença é que aqui ele também não existe no modelo.

Confira o dicionário antes de compilar

Depois de cadastrar a tabela no Configurador, confirme que os quatro campos entraram como esperado:

Confira no seu ambienteSQL
SELECT X3_CAMPO, X3_TITULO, X3_TIPO, X3_TAMANHO
 FROM SX3010
 WHERE X3_ARQUIVO = 'Z50'
 AND D_E_L_E_T_ = ' '
 ORDER BY X3_ORDEM

Se a consulta devolver menos de quatro linhas, ou se Z50_STATUS não trouxer o X3_CBOX esperado, é ali que o problema está — não no ADVPL. Confirme também a tabela em si:

Confira no seu ambienteSQL
SELECT X2_CHAVE, X2_NOME
 FROM SX2010
 WHERE X2_CHAVE = 'Z50'

Com as duas consultas batendo e o fonte compilado sem erro, U_PGV010 deve abrir um browse vazio, com as legendas verde e vermelha prontas para o primeiro registro incluído.


Se o seu primeiro MVC compila e não abre, fale com a gente.

Anexos

PGV010.prw
Cadastro de tipos de contrato em MVC, entidade única. Exemplo didático: crie a tabela Z50 antes de compilar.
Baixar fonte
PGV010.prwADVPL / TLPP
#Include "Protheus.ch"
#Include "FWMVCDef.ch"

/*/{Protheus.doc} PGV010
Cadastro de tipos de contrato (tabela ficticia Z50) escrito em MVC, no
formato de entidade unica -- o "modelo 1" do ERP.

Este fonte e um MVC didatico completo: alem da funcao principal, tem so as
tres Static Function de nome obrigatorio que o framework procura -- MenuDef,
ModelDef e ViewDef. Nao ha uma linha de gravacao, porque quem grava e o
FWFormCommit que o MPFormModel instala sozinho.

Exemplo didatico do site. As tabelas Z50 nao existem no dicionario padrao;
crie-as no Configurador antes de compilar.

@type user function
@author Pragiva
@since 20/09/2026
@version 1.0
@return nil
@example U_PGV010()
/*/
User Function PGV010()

    Local oBrowse := Nil

    oBrowse := FWMBrowse():New()
    oBrowse:SetAlias("Z50")
    oBrowse:SetDescription("Tipos de contrato")

    // O browse precisa saber de QUE FONTE ler o MenuDef. E o nome do
    // arquivo, nao o da funcao.
    oBrowse:SetMenuDef("PGV010")

    oBrowse:AddLegend("Z50_STATUS == '1'", "GREEN", "Ativo")
    oBrowse:AddLegend("Z50_STATUS == '2'", "RED"  , "Inativo")

    oBrowse:Activate()

Return Nil

/*/{Protheus.doc} MenuDef
As opcoes do browse. O comando ADD OPTION e do FWMBrowse.ch, que o
FWMVCDef.ch inclui: ele so faz um aAdd de um array de seis posicoes
-- titulo, acao, 0, operacao, acesso, desabilita-menu.

Todas as opcoes de edicao apontam para a MESMA acao, "VIEWDEF.PGV010". O que
distingue incluir de alterar e o numero do OPERATION.

@type static function
@return array, o aRotina do browse
/*/
Static Function MenuDef()

    Local aRotina := {}

    ADD OPTION aRotina TITLE "Pesquisar"  ACTION "AxPesqui"       OPERATION 1 ACCESS 0
    ADD OPTION aRotina TITLE "Visualizar" ACTION "VIEWDEF.PGV010" OPERATION 2 ACCESS 0
    ADD OPTION aRotina TITLE "Incluir"    ACTION "VIEWDEF.PGV010" OPERATION 3 ACCESS 0
    ADD OPTION aRotina TITLE "Alterar"    ACTION "VIEWDEF.PGV010" OPERATION 4 ACCESS 0
    ADD OPTION aRotina TITLE "Excluir"    ACTION "VIEWDEF.PGV010" OPERATION 5 ACCESS 0

Return aRotina

/*/{Protheus.doc} ModelDef
O modelo de dados: o que existe, o que vale e como grava.

MPFormModel herda de FWFormModel e so muda os valores padrao do construtor:
quem instancia MPFormModel ja recebe FWFormCommit como bloco de gravacao,
FWFormCancel como bloco de cancelamento e FWFormCanDel como pos-validacao.
E por isso que este fonte grava sem ter uma linha de RecLock.

@type static function
@return object, o modelo pronto para a view consumir
/*/
Static Function ModelDef()

    Local oModel  := Nil

    // 1 monta a estrutura do MODELO, a partir do dicionario (SX3).
    Local oStruct := FWFormStruct(1, "Z50")

    oModel := MPFormModel():New("PGV010M")

    // O segundo parametro e o dono (cOwner). O componente principal nao tem
    // dono, entao vai vazio.
    oModel:AddFields("Z50MASTER", /*cOwner*/, oStruct)

    oModel:SetDescription("Tipos de contrato")
    oModel:GetModel("Z50MASTER"):SetDescription("Dados do tipo de contrato")

    oModel:SetPrimaryKey({"Z50_FILIAL", "Z50_COD"})

Return oModel

/*/{Protheus.doc} ViewDef
A tela. Nao decide nada: recebe o modelo pronto e diz onde cada coisa
aparece.

FWLoadModel("PGV010") e, por dentro, uma chamada a
STATICCALL(PGV010, ModelDef) -- e dai vem a regra de que o argumento e o nome
do FONTE e de que ModelDef precisa se chamar exatamente assim.

@type static function
@return object, a view ligada ao modelo
/*/
Static Function ViewDef()

    Local oView   := Nil
    Local oModel  := FWLoadModel("PGV010")

    // 2 monta a estrutura da VIEW. E outra chamada, para o mesmo alias:
    // modelo e tela leem o dicionario cada um para si.
    Local oStruct := FWFormStruct(2, "Z50")

    oView := FWFormView():New()
    oView:SetModel(oModel)

    // O terceiro parametro liga o componente da tela ao componente do
    // modelo. Os dois IDs podem ser diferentes; e esse argumento que os une.
    oView:AddField("VIEW_Z50", oStruct, "Z50MASTER")

    oView:CreateHorizontalBox("TELA", 100)

    // Sem SetOwnerView o componente existe e nao aparece.
    oView:SetOwnerView("VIEW_Z50", "TELA")

Return oView

Referências

  1. TDNtdn.totvs.com

    TDN — AdvPL utilizando MVC

Continue por aqui