Pragiva — Tecnologia na prática

MVC pai e filho: cabeçalho e itens na mesma tela

MVC pai e filho: AddGrid inverte a ordem dos blocos de AddFields, SetRelation liga cabeçalho e item. O que muda quando a tela ganha um grid.

Protheus Tutorial 6 min de leitura

Assista o vídeo

Resposta rápida

O que muda
o modelo ganha um segundo formulário, de várias linhas, criado por AddGrid em vez de AddFields.
A armadilha central
em AddGrid, os blocos de linha vêm antes dos de formulário — o oposto de AddFields.
Quem liga cabeçalho e item
SetRelation, chamado no modelo pai, com o ID do filho como primeiro parâmetro.
Onde some o campo de vínculo
da estrutura da VIEW; na do modelo ele fica, senão para de gravar.
Como se varre o grid
Length() e, por índice, GetValue(campo, nLinha) e IsDeleted(nLinha) — sem GoLine.
Nesta página8 seções

Um cadastro com grid começa igual a qualquer outro MVC: ModelDef monta o modelo, ViewDef monta a tela. A diferença é o método do segundo componente — cabeçalho usa AddFields, item usa AddGrid, e AddGrid não é AddFields com um parâmetro a mais: é uma assinatura com a ordem dos blocos trocada. Quem copia o hábito de posição do formulário simples para o grid erra de um jeito que o compilador nunca aponta.

PGV020.prw é um contrato de exemplo; duas tabelas no dicionário antes de qualquer código:

CampoTipoTamanhoO que é
Z51_FILIALCaractere8Filial do cabeçalho
Z51_NUMCaractere6Número do contrato — a outra metade da chave
Z51_CLIENTECaractere6Código do cliente
Z51_VALORNumérico14,2Total do contrato, somado dos itens no commit
Z52_FILIALCaractere8Filial do item — preenchida pelo SetRelation
Z52_NUMCaractere6Contrato a que o item pertence — idem
Z52_ITEMCaractere4Sequencial, preenchido pela view
Z52_PRODCaractere15Produto
Z52_QUANTNumérico12,2Quantidade
Z52_VALORNumérico14,2Valor do item

Os índices são Z51_FILIAL+Z51_NUM e Z52_FILIAL+Z52_NUM+Z52_ITEM — o segundo é o que SetRelation usa como ordenação, e precisa existir no Configurador antes de a tela abrir. O modelo é um MPFormModel só, com dois submodelos: um de formulário (Z51MASTER), um de grid (Z52DETAIL).

A ordem que a assinatura inverte

Em AddFields, a dupla de validação de formulário — bPre e bPost — é o quarto e o quinto parâmetro, depois de cId, cOwner e a estrutura. Em AddGrid, ela desce para a sexta e a sétima posição, porque dois blocos de LINHA entram antes:

posiçãoAddFieldsAddGrid
1cIdcId
2cOwnercOwner
3oModelStructoModelStruct
4bPrebLinePre
5bPostbLinePost
6bLoadbPre
7—bPost
8—bLoad

O corpo do método confirma: AddGrid cria um FWFormGridModel():New e chama, em sequência, SetLPre(bLinePre), SetLPost(bLinePost), SetPre(bPre), SetPost(bPost) e SetLoad(bLoad) — os nomes dos setters seguem a ordem dos parâmetros, LINHA primeiro, formulário depois. Passar a pós-validação de linha na posição errada compila sem erro e simplesmente nunca é chamada.

É por isso que a chamada de PGV020.prw marca as posições vazias com comentário, em vez de contar vírgulas no olho:

ADVPL / TLPP
oModel:AddGrid("Z52DETAIL", "Z51MASTER", oStruItem, ;
 /*bLinePre*/, {|oGrid| PGV020Lin(oGrid)}, ;
 /*bPre*/, /*bPost*/, /*bLoad*/)

A pós-validação de linha está na quinta posição, onde bLinePost mora. Uma casa adiante, viraria bPre de formulário — e nunca se comportaria como validação de linha.

SetRelation: quem aponta para quem

SetRelation é chamado no modelo pai; o ID do formulário FILHO é o primeiro parâmetro — não o do pai. Em PGV020.prw, função ModelDef, o desenho é este:

ADVPL / TLPP
oModel:SetRelation("Z52DETAIL", { ;
 {"Z52_FILIAL", 'xFilial("Z52")'}, ;
 {"Z52_NUM", "Z51_NUM"} ;
}, Z52->(IndexKey(1)))

O cOrderBy não é string escrita à mão: é IndexKey(1) lido da própria tabela filha. E o lado direito de cada par de aRelation tem dois comportamentos: string de expressão, como 'xFilial("Z52")', o framework AVALIA; nome de campo, como "Z51_NUM", lê o VALOR no modelo PAI. Mesma sintaxe, duas semânticas.

RemoveField mora na view, não no modelo

Os campos que SetRelation preenche precisam continuar na estrutura do MODELO — senão não seriam gravados. O que muda é a VIEW: PGV020.prw tira dela os DOIS campos do relacionamento, com oStruItem:RemoveField("Z52_FILIAL") e oStruItem:RemoveField("Z52_NUM"). O campo some da tela, mas continua recebendo o valor do pai — na estrutura de modelo, RemoveField apagaria o dado; na de view, só esconde.

A filial merece atenção à parte: FWFormStruct(2,...) não a descarta sozinha. Uma coluna “Filial” repetida em toda linha do grid, sempre igual, é sintoma de quem esqueceu dela.

A numeração do item é da tela

AddIncrementField(cIDView, cField) é método de FWFormView, não de modelo — a numeração do item (Z52_ITEM) é da camada de apresentação.

Dois comportamentos vistos no uso, sem assinatura documentada

SetOptional(.F.), no submodelo do grid, torna o grid obrigatório — o registro não fecha sem ao menos uma linha. É o caso sem rede: nenhuma classe do framework MVC o declara. As poucas declarações desse nome que existem pertencem a classes de outros módulos, com outra assinatura, recebendo um alias — não são a que esse uso chama. Trate-o como comportamento observado, não como assinatura garantida entre versões.

SetUniqueLine({"Z52_PROD"}) faz o framework recusar sozinho uma segunda linha com o mesmo produto, sem validação escrita por quem programa. É caso diferente: FWFormGridModel declara e implementa SetUniqueLine(aUnique, bUnique) — array de campos que identifica a linha e bloco opcional de comparação. Os dois métodos costumam ser citados juntos como “não documentados”; só um deles é.

Varrer o grid por índice, sem mexer na posição

O commit de PGV020.prw soma o valor de cada item no total do cabeçalho antes de gravar, e para isso precisa percorrer TODAS as linhas do grid:

ADVPL / TLPP
For nLinha := 1 To nLinhas
 If !oGrid:IsDeleted(nLinha)
 nTotal += oGrid:GetValue("Z52_VALOR", nLinha)
 EndIf
Next nLinha

Repare no que esse laço não tem: nenhum GoLine. É a parte que mais se escreve errado. GetValue e IsDeleted aceitam o número da linha como argumento — omitido, valem para a linha corrente, mas o parâmetro existe. Ler por índice não move o grid, então não há posição de usuário a guardar nem restaurar. A versão com GoLine no início do laço e outro no fim para devolver o cursor funciona, e circula em exemplo de curso — mas faz trabalho que o método já resolve.

Length() merece a mesma honestidade: não está declarado na classe do grid, e ainda assim é o método que se usa na prática para saber quantas linhas existem, enquanto GetMaxLine(), esse sim declarado, praticamente não aparece em código real. O que sustenta Length() é o uso, não um contrato documentado.

A chave primária pode apontar para o filho

SetPrimaryKey também pode apontar para os campos da tabela FILHA, não para os da master — desenho deliberado quando o cabeçalho existe só para mostrar um dado já gravado em outro lugar, como um cliente: logo depois de fechar a chave pelo filho, SetOnlyView(.T.) no submodelo do cabeçalho marca aquele lado como só-consulta. A tela existe para mostrar o dado do pai, não gravá-lo; a chave primária de verdade, nesse desenho, é a do grid.

As caixas na tela

PGV020.prw divide a tela com CreateHorizontalBox("SUPERIOR", 35) e CreateHorizontalBox("INFERIOR", 65). Não há regra fixa para essa proporção; ela segue quantos campos e linhas cada bloco precisa mostrar — um cabeçalho enxuto cede mais espaço ao grid, um cabeçalho cheio de campos pede o contrário.

Nenhum dos pontos acima é exclusivo de contrato: o mesmo par — AddFields no cabeçalho, AddGrid nos itens, ligados por SetRelation — é a base de pedido de compra, pedido de venda, nota fiscal de entrada e qualquer cadastro em que itens dependem de uma tabela mestre. A ordem invertida dos blocos, onde mora o campo de vínculo e como varrer o grid sem perder a posição do usuário resolvem o mesmo problema nos quatro.


Se o grid de um cadastro seu não grava os itens, fale com a gente.

Anexos

PGV020.prw
Cadastro de contratos em MVC com cabecalho e itens. Exemplo didatico: crie as tabelas Z51 e Z52 antes de compilar.
Baixar fonte
PGV020.prwADVPL / TLPP
#Include "Protheus.ch"
#Include "FWMVCDef.ch"

/*/{Protheus.doc} PGV020
Cadastro de contratos em MVC com cabecalho e itens -- o "modelo 3" do ERP.

Cabecalho na tabela ficticia Z51, itens na Z52. Alem do que o PGV010 ja
mostra, este fonte tem os quatro pontos que so aparecem quando existe grid:
AddGrid, SetRelation, a pos-validacao de linha e um commit proprio que soma
os itens no cabecalho antes de gravar.

Exemplo didatico do site. As tabelas Z51 e Z52 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_PGV020()
/*/
User Function PGV020()

    Local oBrowse := Nil

    oBrowse := FWMBrowse():New()
    oBrowse:SetAlias("Z51")
    oBrowse:SetDescription("Contratos")
    oBrowse:SetMenuDef("PGV020")
    oBrowse:Activate()

Return Nil

/*/{Protheus.doc} MenuDef
As opcoes do browse. Identicas as do modelo 1: ter grid nao muda o menu.

@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.PGV020" OPERATION 2 ACCESS 0
    ADD OPTION aRotina TITLE "Incluir"    ACTION "VIEWDEF.PGV020" OPERATION 3 ACCESS 0
    ADD OPTION aRotina TITLE "Alterar"    ACTION "VIEWDEF.PGV020" OPERATION 4 ACCESS 0
    ADD OPTION aRotina TITLE "Excluir"    ACTION "VIEWDEF.PGV020" OPERATION 5 ACCESS 0

Return aRotina

/*/{Protheus.doc} ModelDef
O modelo pai-filho.

A ordem dos blocos de AddGrid nao e a mesma de AddFields: aqui os dois
primeiros sao os de LINHA (bLinePre, bLinePost) e so depois vem os do
formulario (bPre, bPost) e a carga. Passar a pos-validacao de linha na
posicao errada compila e nao roda nunca.

@type static function
@return object, o modelo com cabecalho e itens
/*/
Static Function ModelDef()

    Local oModel    := Nil
    Local oStruCab  := FWFormStruct(1, "Z51")
    Local oStruItem := FWFormStruct(1, "Z52")

    // Posicao 3: pos-validacao do modelo. Preenche-la SUBSTITUI o default
    // do MPFormModel, que e FWFormCanDel -- dai PGV020Pos devolver
    // FWFormCanDel na exclusao, e nao .T.
    oModel := MPFormModel():New("PGV020M", /*bPre*/, {|oMdl| PGV020Pos(oMdl)}, ;
                                {|oMdl| PGV020Com(oMdl)}, /*bCancel*/)

    oModel:AddFields("Z51MASTER", /*cOwner*/, oStruCab)

    oModel:AddGrid("Z52DETAIL", "Z51MASTER", oStruItem, ;
                   /*bLinePre*/, {|oGrid| PGV020Lin(oGrid)}, ;
                   /*bPre*/, /*bPost*/, /*bLoad*/)

    // O lado direito de cada par e campo do PAI quando e nome de campo, e
    // expressao avaliada quando e string de expressao. A ordenacao e a chave
    // de indice da tabela filha, lida dela mesma.
    oModel:SetRelation("Z52DETAIL", { ;
        {"Z52_FILIAL", 'xFilial("Z52")'}, ;
        {"Z52_NUM"   , "Z51_NUM"} ;
    }, Z52->(IndexKey(1)))

    oModel:SetDescription("Contratos")
    oModel:GetModel("Z51MASTER"):SetDescription("Cabecalho do contrato")
    oModel:GetModel("Z52DETAIL"):SetDescription("Itens do contrato")

    oModel:SetPrimaryKey({"Z51_FILIAL", "Z51_NUM"})

    // Comportamento observado: .F. torna o grid obrigatorio -- o
    // contrato nao fecha sem pelo menos um item.
    oModel:GetModel("Z52DETAIL"):SetOptional(.F.)

    // Duas linhas do mesmo produto no mesmo contrato viram erro do
    // framework, sem precisar de codigo.
    oModel:GetModel("Z52DETAIL"):SetUniqueLine({"Z52_PROD"})

Return oModel

/*/{Protheus.doc} ViewDef
A tela do pai-filho: cabecalho em cima, itens embaixo.

Os campos de vinculo saem da ESTRUTURA DA VIEW, nao da do modelo. Tirados do
modelo, eles deixariam de ser gravados; tirados so da view, somem da tela e
continuam sendo preenchidos pelo SetRelation.

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

    Local oView     := Nil
    Local oModel    := FWLoadModel("PGV020")
    Local oStruCab  := FWFormStruct(2, "Z51")
    Local oStruItem := FWFormStruct(2, "Z52")

    // Os DOIS campos do SetRelation saem da tela. FWFormStruct(2,...) nao
    // descarta a filial sozinho.
    oStruItem:RemoveField("Z52_FILIAL")
    oStruItem:RemoveField("Z52_NUM")

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

    oView:AddField("VIEW_CAB" , oStruCab , "Z51MASTER")
    oView:AddGrid ("VIEW_ITEM", oStruItem, "Z52DETAIL")

    // Numeracao automatica do item. E da VIEW, nao do modelo.
    oView:AddIncrementField("VIEW_ITEM", "Z52_ITEM")

    oView:CreateHorizontalBox("SUPERIOR", 35)
    oView:CreateHorizontalBox("INFERIOR", 65)

    oView:SetOwnerView("VIEW_CAB" , "SUPERIOR")
    oView:SetOwnerView("VIEW_ITEM", "INFERIOR")

Return oView

/*/{Protheus.doc} PGV020Lin
Pos-validacao de LINHA do grid: roda uma vez por linha, na confirmacao.

Linha marcada para exclusao nao se valida -- senao o usuario nao consegue
apagar justamente a linha errada que o fez abrir a tela.

FWAlertError e MsgAlert aqui fazem o usuario ver DUAS mensagens: a sua e a
que o framework exibe em seguida.

@type static function
@param oGrid, object, o submodelo de grid, ja posicionado na linha
@return logical, .F. barra a confirmacao
/*/
Static Function PGV020Lin(oGrid)

    Local lOk := .T.

    If oGrid:IsDeleted()
        Return .T.
    EndIf

    // O submodelo de grid nao implementa SetErrorMessage; quem tem o metodo
    // e o modelo principal, alcancado por oGrid:GetModel(). E as posicoes 3
    // e 4 sao as do erro -- e a 3a que o framework le.
    If oGrid:GetValue("Z52_QUANT") <= 0
        oGrid:GetModel():SetErrorMessage( , , oGrid:GetId(), "Z52_QUANT", ;
            "PGV020", "Quantidade invalida", ;
            "Informe uma quantidade maior que zero.")
        lOk := .F.
    EndIf

Return lOk

/*/{Protheus.doc} PGV020Pos
Pos-validacao do modelo inteiro.

Ela roda na inclusao, na alteracao E na exclusao. Sem filtrar pela operacao,
uma regra escrita para a inclusao passa a impedir que alguem apague o
registro.

@type static function
@param oModel, object, o modelo principal
@return logical, .F. barra a confirmacao
/*/
Static Function PGV020Pos(oModel)

    Local lOk := .T.

    // FWFormCanDel, e nao .T.: um bPost proprio SUBSTITUI esse default do
    // MPFormModel, que e quem confere chave estrangeira antes de apagar.
    If oModel:GetOperation() == MODEL_OPERATION_DELETE
        Return FWFormCanDel(oModel)
    EndIf

    // Posicoes 3 e 4 sao as do erro, e e a 3a que o framework le. Preencher
    // 1 e 2 e deixar 3 e 4 vazios faz a mensagem se perder.
    If Empty(oModel:GetValue("Z51MASTER", "Z51_CLIENTE"))
        oModel:SetErrorMessage( , , "Z51MASTER", "Z51_CLIENTE", ;
            "PGV020", "Cliente obrigatorio", ;
            "Informe o cliente do contrato.")
        lOk := .F.
    EndIf

Return lOk

/*/{Protheus.doc} PGV020Com
Commit proprio: soma os itens no total do cabecalho e grava.

Duas coisas que o nome nao entrega. Primeira: GetValue e IsDeleted do grid
aceitam o NUMERO DA LINHA, entao se varre por indice sem mover o grid --
nao e preciso GoLine. Segunda: o retorno diz GRAVOU, nao PODE GRAVAR;
retornar .F. aqui deixa os dados no banco e a tela aberta. Quem barra e a
pos-validacao, que roda antes.

@type static function
@param oModel, object, o modelo principal
@return logical, resultado da gravacao
/*/
Static Function PGV020Com(oModel)

    Local lCommit  := .F.
    Local oGrid    := oModel:GetModel("Z52DETAIL")
    Local nTotal   := 0
    Local nLinha   := 0
    Local nLinhas  := oGrid:Length()

    For nLinha := 1 To nLinhas
        If !oGrid:IsDeleted(nLinha)
            nTotal += oGrid:GetValue("Z52_VALOR", nLinha)
        EndIf
    Next nLinha

    oModel:GetModel("Z51MASTER"):SetValue("Z51_VALOR", nTotal)

    lCommit := FWFormCommit(oModel)

Return lCommit

Referências

  1. TDNtdn.totvs.com

    TDN — MPFormModel

Continue por aqui

  1. Vídeo

    ProtheusReferência técnica

    ViewDef: a tela de uma rotina MVC

    ViewDef não valida nem grava: só recebe o modelo pronto e diz onde cada componente aparece. O que acontece sem SetOwnerView e quando o box não é preciso.

    4 min de leituraAssista o vídeo