Resposta rápida
- O que dispara
- quatro blocos passados por posição, nunca por nome —
bPre,bPost,bCommit,bCancel. - Onde entram
MPFormModel():New,:AddFieldse:AddGrid, com uma posição diferente em cada um.- O que barra a operação
- a pós-validação —
bLinePoste obPostdo modelo. Nunca o commit. - Armadilha mais cara
- o
bPostdo modelo roda também na exclusão; sem filtrarGetOperation(), uma regra de inclusão trava o apagar. - Extensão sem tocar o fonte
FWModelEvent, registrado comInstallEvent— é 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:
| bloco | onde entra | posição |
|---|---|---|
bPre (modelo) | MPFormModel():New | 2ª |
bPost (modelo) | idem | 3ª |
bCommit | idem | 4ª |
bCancel | idem | 5ª |
bPre (formulário) | :AddFields | 4ª |
bPost (formulário) | :AddFields | 5ª |
bLoad | :AddFields | 6ª |
bLinePre | :AddGrid | 4ª |
bLinePost | :AddGrid | 5ª |
bPre (formulário) | :AddGrid | 6ª |
bPost (formulário) | :AddGrid | 7ª |
bLoad | :AddGrid | 8ª |
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:
oModel := MPFormModel():New("PGV020M", /*bPre*/, {|oMdl| PGV020Pos(oMdl)}, ;
{|oMdl| PGV020Com(oMdl)}, /*bCancel*/)PGV020Pos é a pós-validação do modelo inteiro:
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 lOkE 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ê:
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 lCommitO 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.
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:
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 lOkO 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:
#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 9Os 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:
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
TDNtdn.totvs.com
TDN — FWFormModelTDNtdn.totvs.com
TDN — Interceptação do Commit e Validação MVC (FWModelEvent)
Continue por aqui
- Vídeo
ProtheusTutorial
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.