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êsStatic 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:
| Campo | Tipo | Tamanho | O que é |
|---|---|---|---|
Z50_FILIAL | Caractere | 8 | Filial do registro — todo campo _FILIAL de uma tabela própria segue esse padrão |
Z50_COD | Caractere | 6 | Código do tipo de contrato — metade da chave primária |
Z50_DESC | Caractere | 40 | Descrição do tipo de contrato |
Z50_STATUS | Caractere | 1 | 1 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:
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:
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:
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:
SELECT X3_CAMPO, X3_TITULO, X3_TIPO, X3_TAMANHO
FROM SX3010
WHERE X3_ARQUIVO = 'Z50'
AND D_E_L_E_T_ = ' '
ORDER BY X3_ORDEMSe 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:
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
#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 oViewReferências
TDNtdn.totvs.com
TDN — AdvPL utilizando MVC
Continue por aqui
- Vídeo
ProtheusReferência técnica
ModelDef: o modelo de dados de uma rotina MVC
A sequência real de FWFormStruct, MPFormModel e AddFields que monta o modelo — e por que instanciar FWFormModel direto não grava nada.