Pragiva — Tecnologia na prática

Os eventos de um modelo MVC: onde cada regra entra

bPre, bPost, bCommit e bCancel não são constantes, são posição. Onde cada evento do modelo MVC entra e qual barra a gravação.

Protheus Referência técnica 5 min de leitura

Assista o vídeo

Resposta rápida

O que dispara
quatro blocos passados por posição, nunca por nome — bPre, bPost, bCommit, bCancel.
Onde entram
MPFormModel():New, :AddFields e :AddGrid, com uma posição diferente em cada um.
O que barra a operação
a pós-validação — bLinePost e o bPost do modelo. Nunca o commit.
Armadilha mais cara
o bPost do modelo roda também na exclusão; sem filtrar GetOperation(), uma regra de inclusão trava o apagar.
Extensão sem tocar o fonte
FWModelEvent, registrado com InstallEvent — é assim que o padrão declara a própria classe de eventos.
Consulta rápida9 seções

Quem aprendeu MVC lendo um ModelDef pronto tende a procurar um catálogo de eventos em algum .ch, do tipo MODEL_EVENT_ONVALIDATE. Não existe. Os quatro blocos que decidem o comportamento de um modelo — bPre, bPost, bCommit e bCancel — são parâmetros posicionais. O nome da variável é cosmético; vale o lugar dela na chamada. Errar a posição não gera erro de compilação: gera um bloco que nunca dispara.

A tabela: posição, não nome

Três construções diferentes recebem esses blocos, e cada uma tem a própria ordem:

blocoonde entraposição
bPre (modelo)MPFormModel():New2ª
bPost (modelo)idem3ª
bCommitidem4ª
bCancelidem5ª
bPre (formulário):AddFields4ª
bPost (formulário):AddFields5ª
bLoad:AddFields6ª
bLinePre:AddGrid4ª
bLinePost:AddGrid5ª
bPre (formulário):AddGrid6ª
bPost (formulário):AddGrid7ª
bLoad:AddGrid8ª

A inversão entre as duas colunas é o ponto a decorar: em AddGrid, os blocos de linha entram antes dos de formulário e empurram bPre/bPost da quarta e quinta posição para a sexta e a sétima — detalhado aqui.

bTudoOk não existe nesta API

Quem chega do modelo 3 tradicional procura TudoOk na assinatura de FWFormModel e não acha. O conceito existe, com outro nome e outro lugar: é o bPost do modelo, a terceira posição de New. Decide a mesma coisa — se a confirmação do cadastro inteiro segue adiante.

bPost do modelo dispara também na exclusão

PGV020.prw usa esse terceiro parâmetro assim:

ADVPL / TLPP
oModel := MPFormModel():New("PGV020M", /*bPre*/, {|oMdl| PGV020Pos(oMdl)}, ;
 {|oMdl| PGV020Com(oMdl)}, /*bCancel*/)

PGV020Pos é a pós-validação do modelo inteiro:

ADVPL / TLPP
Static Function PGV020Pos(oModel)

 Local lOk := .T.

 If oModel:GetOperation() == MODEL_OPERATION_DELETE
 Return FWFormCanDel(oModel)
 EndIf

 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

E repare no que ele devolve nesse desvio: FWFormCanDel(oModel), e não um .T. solto. O motivo aparece duas seções abaixo, e é a armadilha mais cara deste texto.

Commit não é validação

bCommit é o quarto parâmetro de New, e o retorno dele significa GRAVOU, não PODE GRAVAR. PGV020Com mostra por quê:

ADVPL / TLPP
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

O bloco soma os itens e chama FWFormCommit(oModel), que é quem grava. Devolver .F. depois desse ponto não desfaz nada: deixa os dados no banco e a tela aberta. Quem precisa barrar a operação faz isso antes, na pós-validação — em PGV020Pos ou em PGV020Lin —, nunca aqui.

Quem grava por padrão

PGV020.prw escreve um bCommit próprio porque precisa somar os itens antes de gravar. Um ModelDef sem regra nenhuma não escreve bloco algum: o MPFormModel já instala os quatro.

ADVPL / TLPP
DEFAULT bPre := {||.T.}
DEFAULT bPost := {|| FWFormCanDel(Self) }
DEFAULT bCommit := {|| FWFormCommit(Self)}
DEFAULT bCancel := {|| FWFormCancel(Self)}

Preencher uma dessas posições substitui o default dela. É por isso que PGV020Com termina chamando FWFormCommit(oModel) explicitamente: sem essa linha, a gravação nunca aconteceria, porque o bloco próprio tomou o lugar do que gravava sozinho.

E é por isso, também, que PGV020Pos devolve FWFormCanDel(oModel) na exclusão. O default da terceira posição não é .T. — é FWFormCanDel, a função que confere as chaves estrangeiras antes de deixar apagar. Quem topa com a armadilha da seção anterior e a resolve com um Return .T. na exclusão desliga essa conferência sem perceber: o cadastro passa a aceitar que se apague um pai que ainda tem filhos. O erro é silencioso nos dois sentidos — não avisa quando acontece, nem quando estraga. É a diferença entre pular a sua regra e pular a do framework.

Validação de linha: o formulário responde, não o modelo

bLinePost, quinta posição de AddGrid, valida uma linha do grid por vez, na confirmação. A documentação do próprio método é literal sobre onde a mensagem precisa nascer: “Caso o code-block retorne um valor lógico negativo, um erro será atribuido, sendo necessário indicar a natureza do erro através do método SetErrorMessage do objeto do formulário e não do modelo principal.” PGV020Lin é essa chamada:

ADVPL / TLPP
Static Function PGV020Lin(oGrid)

 Local lOk := .T.

 If oGrid:IsDeleted()
 Return .T.
 EndIf

 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

O IsDeleted() / Return .T. no início não é zelo: sem ele, a linha marcada para excluir continua passando pela validação, e ninguém consegue apagar o item errado que o fez abrir a tela.

Falta explicar de onde sai o SetErrorMessage. A documentação manda usar o do formulário, não o do modelo principal — só que o submodelo de grid não implementa o método. Quem o tem é o modelo, alcançado pelo próprio objeto recebido, com oGrid:GetModel(). Não é preciso FWModelActive(): ela resolve por estado global, e o bloco já tem o objeto em mãos.

Nove parâmetros, nove constantes

SetErrorMessage tem nove posições, e o FWMVCDef.ch define uma constante para cada uma:

ADVPL / TLPP
#DEFINE MODEL_MSGERR_IDFORM 1
#DEFINE MODEL_MSGERR_IDFIELD 2
#DEFINE MODEL_MSGERR_IDFORMERR 3
#DEFINE MODEL_MSGERR_IDFIELDERR 4
#DEFINE MODEL_MSGERR_ID 5
#DEFINE MODEL_MSGERR_MESSAGE 6
#DEFINE MODEL_MSGERR_SOLUCTION 7
#DEFINE MODEL_MSGERR_VALUE 8
#DEFINE MODEL_MSGERR_OLDVALUE 9

Os nomes dizem o que a ordem esconde: 1 e 2 são o formulário e o campo em validação; 3 e 4 são onde o erro ocorreu — e é a terceira que o framework lê para saber que houve erro. Preencher 1 e 2 e deixar 3 e 4 vazias, que é o arranjo intuitivo, faz a mensagem não aparecer: o usuário leva um “modelo inválido” genérico, sem pista do campo. Por isso as duas chamadas de PGV020.prw começam com duas vírgulas — e o próprio FWFormCanDel faz igual.

O fonte de eventos do cadastro de produtos resume num comentário a única forma aceita de avisar o usuário: “Importante: Use somente a função Help para exibir mensagens ao usuario, pois apenas o help é tratado pelo MVC.” MsgAlert e FWAlertError não substituem SetErrorMessage — empilham uma segunda mensagem sobre a do framework.

FWModelEvent: pendurar regra sem tocar o fonte

Há um segundo mecanismo, para quando a regra precisa vir de outro fonte sem editar o ModelDef original. É um padrão consolidado: centenas de rotinas declaram uma classe própria a partir de FWModelEvent, com ganchos que o framework chama pelo nome. VldActivate e ModelPosVld são os mais usados — o primeiro dispara ao abrir a tela, o segundo na pós-validação do modelo; InTTS, BeforeTTS e AfterTTS cercam a transação de gravação, e GridLinePosVld, GridLinePreVld e FieldPreVld cobrem validação de linha e de campo. O desenho é sempre o mesmo:

ADVPL / TLPP
CLASS PGV020EV FROM FWModelEvent
 METHOD New() CONSTRUCTOR
 METHOD ModelPosVld()
ENDCLASS

// no ModelDef, depois de montar o modelo:
oModel:InstallEvent("PE",, PGV020EV():New())

InstallEvent aparece sempre com o segundo parâmetro vazio; o primeiro é etiqueta livre ("PADRAO", "PE", "TEMPLATE", "INT.LOJA"), e o padrão é empilhar um objeto por origem da regra. Um limite a declarar: a classe FWModelEvent não é distribuída como fonte — só se observa o resultado de quem herda dela.

Nomes que enganam

Três não existem: GridPreLineVld (o certo é GridLinePreVld), ModelPosEnd e SetEvent como método do modelo — quem registra evento é InstallEvent, e todo :SetEvent( que aparece pertence a objetos de EAI. Outros dois existem e não são o que parecem: GridPreVld é método de uma classe de outro módulo, sem relação com evento de modelo MVC; e FormPos é nome comum de variável local do bPost de formulário, nunca evento.

O PGV020.prw completo, com as três funções citadas aqui no lugar delas, está anexado em MVC pai e filho: cabeçalho e itens na mesma tela.


Se uma validação sua nunca dispara — ou dispara na operação errada —, fale com a gente.

Referências

  1. TDNtdn.totvs.com

    TDN — FWFormModel
  2. TDNtdn.totvs.com

    TDN — Interceptação do Commit e Validação MVC (FWModelEvent)

Continue por aqui