PDFlibPas' TPDFlibViewer-kontrol forpligter en in-place-formularfelt-editor gennem editorens eget OnExit-hændelse, og det designvalg gemmer en klassisk Delphi-VCL-fælde: at skjule, reparente, eller destruere en fokuseret kontrol fra inde i dens egen OnExit-handler kan udløse OnExit en anden gang, før det første kald returnerer, og sende forpligtelses-logikken tilbage ind i sig selv
Fejlen dette producerer modstår en ren genskabelse. En bruger tabber hurtigt gennem en serie tekstfelter på en scannet ansøgningsformular, og en gang imellem kaster fremviseren en adgangskrænkelse, eller værre, bliver ved med at køre, mens den i stilhed skriver den forkerte værdi ind i et felt to tab tilbage. Genskab det efter behov, og bugen ser indlysende ud i bakspejlet; jag den fra en enkelt kundes crash-rapport, og den ser ud som et spøgelse, fordi hvorvidt den anden OnExit rent faktisk udløses, afhænger af vindueshandle- og fokus-timing, der skifter med felttype, tastehastighed, og hvad end beskedkøen ellers laver på det øjeblik
Hvordan TPDFlibViewer lægger en rigtig editor oven på en gengivet side
TPDFlibViewer gengiver hver PDF-side til en bitmap og gør ikke formularfelter til levende VCL-kontroller som standard, så BeginEditFormField er metoden der bygger bro mellem de to verdener: kaldt med et felt-indeks, slår den feltets rektangel op og konverterer det til klient-koordinater, og lægger derefter en rigtig TEdit eller TMemo oven på det rektangel til et tekstfelt, eller en TComboBox i csDropDownList-stil til et valg-felt, komplet med feltets aktuelle værdi allerede indlæst. ISO 32000-2 §12.7 definerer, hvad et tekst- eller valg-formularfelt er inde i en PDF, men intet i den specifikation siger, hvordan en Windows-applikation skal lade nogen skrive ind i en, og det hul er præcis, hvad BeginEditFormField findes for at fylde. Både OnKeyDown og OnExit er koblet til de samme to fremviser-metoder, InplaceEditorKeyDown og InplaceEditorExit, på hver editor TPDFlibViewer opretter, en parring der har været sendt uændret siden interaktiv formular-udfyldning først landede i v3.220.0, og OnExit er, hvor problemet begynder
Hvorfor udløser at skjule editoren OnExit en anden gang?
TWinControl i VCL'en behandler en ændring af Visible eller Parent på en fokuseret kontrol som en grund til at flytte fokus væk fra den med det samme, og at flytte fokus væk fra en kontrol er præcis, hvad der udløser den kontrols OnExit-hændelse, synkront, før egenskabs-tildelingen, der udløste den, overhovedet returnerer. CommitInplaceEditor, metoden PDFlibPas bruger til at lukke in-place-editoren og skrive dens værdi tilbage til formularfeltet, skal gøre præcis de to ting på vejen ud: sætte Editor.Visible til False og sætte Editor.Parent til nil, så kontrollen holder op med at tegne oven på siden og holder op med at modtage input. Gør en af de ting, mens editoren stadig har fokus, hvilket den næsten altid gør, da brugeren lige forlod den, og OnExit udløses igen midt i selve kaldet, der skulle have været det sidste, den editors OnExit nogensinde udløste
Hvad går galt, når CommitInplaceEditor genindtræder sig selv?
En naiv forpligtelses-metode betaler for dette på en af to måder. Enten skriver den feltets værdi to gange, én gang fra det oprindelige kald og én gang fra det genindtrædende kald, der sneg sig ind, før det første var færdig med at røre sin egen tilstand, eller den forsøger at frigive editor-kontrollen, mens en frame længere nede i kaldstakken stadig er inde i den samme kontrols egen hændelses-handler, hvilket er udefineret territorium i VCL'en og viser sig som en adgangskrænkelse, der kan pege på næsten hvilken som helst linje, ikke nødvendigvis den, der rent faktisk forårsagede den. Ingen af fejlene har brug for en stor formular for at udløses; et to-felts-dokument er nok, forudsat brugeren forlader det andet felt hurtigt nok til, at OS'et stadig ruller fokus-beskeder tilbage fra det første
// Naive version: reads fine in review, fails only under real typing speed
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
CommitEditor; // still running inside FEditor's own OnExit
end;
procedure TMyPdfViewer.CommitEditor;
begin
if not Assigned(FEditor) then
Exit;
SaveFieldValue(FEditor.Text);
FEditor.Parent := nil; // focused control reparented here: OnExit
// fires again, re-entering this same method
FEditor.Free; // freed while a caller further down the
FEditor := nil; // stack is still inside its OnExit handler
end;
Null referencen, før man rører kontrollen
Fixen PDFlibPas leverer, er én enkelt omrokering: fang editoren i en lokal variabel, ryd feltet der peger på den, og begynd først derefter at ændre kontrollens egenskaber. CommitInplaceEditor læser FInplaceEditor ind i en lokal Editor-variabel, sætter FInplaceEditor til nil med det samme, og først derefter tildeler Editor.Visible og Editor.Parent. Et genindtrædende kald udløst af en af de to tildelinger læser FInplaceEditor selv, finder den allerede nil, og afslutter ved sin allerførste linje, før den kan røre Editor eller skrive feltets værdi en anden gang
procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
Editor: TWinControl;
begin
Editor := FInplaceEditor;
if not Assigned(Editor) then
Exit; // a reentrant call lands here and stops
FInplaceEditor := nil; // detach before the control is touched at all
if Save then
SaveEditorValue(Editor); // safe: FInplaceEditor is already nil
Editor.Visible := False;
Editor.Parent := nil; // may fire OnExit again; the guard above
// turns that reentrant call into a no-op
ReapDeadEditor; // free whatever was parked last cycle
FDeadEditor := Editor; // park this one instead of freeing it here
end;
SaveEditorValue i den udskrift står i stedet for den rigtige gren, som tjekker om Editor er en TComboBox, en TMemo, eller en TEdit og læser dens værdi derefter, da PDFlibPas opretter en anden kontrol afhængigt af, om feltet er et tekstfelt eller et valg-felt. Vagten er ligeglad med, hvilken gren der kører, kun at FInplaceEditor er nil, før noget der er i stand til at udløse OnExit, udføres, hvilket er den ene rækkefølge-begrænsning, der gør resten af metoden sikker at skrive i hvilken som helst stil, der ellers er naturlig
Frigiv aldrig en kontrol fra inde i dens egen hændelse
TPDFlibViewer.CommitInplaceEditor kalder aldrig Editor.Free direkte, og det er bevidst: at frigive en kontrol er usikkert, mens en stack-frame tilhørende den samme kontrols egen hændelses-dispatch måske stadig ruller op over kaldet, der frigiver den, genindtrædende OnExit eller ej. PDFlibPas overdrager i stedet den løsrevne editor til en enkelt-plads-parkeringsplads, FDeadEditor, og frigiver hvad end der sad der fra den forrige redigeringscyklus gennem en lille hjælper, ReapDeadEditor, kaldt ved starten af næste BeginEditFormField og en gang mere fra CloseDocument; hver editor fremviseren opretter, ejes også af selve fremviseren, TEdit.Create(Self) frem for TEdit.Create(nil), så selv en kontrol stadig parkeret i FDeadEditor, når fremviseren destrueres, fejes op af almindeligt VCL-komponent-ejerskab frem for at lække
procedure TPDFlibViewer.ReapDeadEditor;
begin
if Assigned(FDeadEditor) then
begin
FDeadEditor.Free; // safe now: this control's own OnExit
FDeadEditor := nil; // finished at least one edit cycle ago
end;
end;
function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
Edit: TEdit;
begin
Result := 0;
CommitInplaceEditor(True); // flush whatever editor is still open
// ... field lookup and rectangle conversion omitted ...
ReapDeadEditor; // now safe to free last cycle's parked editor
Edit := TEdit.Create(Self);
Edit.Parent := Self;
Edit.OnExit := InplaceEditorExit;
FInplaceEditor := Edit;
FInplaceEditor.SetFocus;
Result := 1;
end;
Hvorfor viser dette sig hårdest under hurtig Tab-navigation?
FocusNextFormField, metoden PDFlibPas tilføjede i v3.226.0 til at drive Tab- og Shift+Tab-navigation på tværs af en formular, kalder BeginEditFormField for det næste kvalificerede felt ved hvert eneste hop, og BeginEditFormField åbner ved at kalde CommitInplaceEditor(True) for at flushe, hvilken editor det forrige felt lod stå åbent. Det betyder, at hvert Tab-tryk en bruger foretager, mens de udfylder en flerfelts-formular, kører præcis den løsriv-så-rør-sekvens beskrevet ovenfor én gang, hvilket er præcis den kodevej, der mest sandsynligt stadig har en kontrol genuint fokuseret i det øjeblik Visible og Parent ændres, fordi Tab er den ene interaktion, der næsten er garanteret at lade den udgående editor beholde fokus lige indtil den nye beder om det
Intet af dette gør bugen pålidelig at demonstrere, og det er værd at sige ligeud frem for at glatte over. Hvorvidt en given Visible- eller Parent-tildeling rent faktisk tvinger en synkron OnExit, afhænger af fokus- og vindueshandle-tilstand, som en debugger ændrer bare ved at være tilknyttet, som en urelateret gentegning eller timer kan forstyrre, og som opfører sig forskelligt afhængigt af, hvilken af TEdit, TMemo eller TComboBox der tilfældigvis er kontrollen i spil. En vagt der kun nogle gange bliver udøvet, er grunden til, at denne slags defekt overlever kodegennemgang og manuel testning alike, og det er også grunden til, at fixen skal være korrekt ved konstruktion, at nulle referencen før noget andet sker, frem for korrekt ved hvilken som helst opførsel en håndfuld manuelle testgennemløb tilfældigvis observerede
Den generelle form af denne fix rejser langt forbi én fremviser-kontrol. Enhver brugerdefineret redigeringsflade bygget ved at lægge en levende VCL-kontrol oven på gengivet indhold, ikke bare et PDF-formularfelt, arver den samme fare, i det øjeblik dens luk-og-forpligt-logik kan udløses af både en eksplicit brugerhandling og en implicit fokusændring, og det samme to-delte svar gælder: ryd referencen der identificerer den aktive kontrol, før man gør noget der måske udløser dens egen exit-hændelse, og kald aldrig Free fra en kodevej, der stadig kunne køre under den kontrols egen hændelses-dispatch. TPDFlibViewers bredere formular-udfyldnings- og gengivelses-flade, inklusive hvordan den afgør hvilken kontroltype der skal vises til hvilket felt, dækkes i oversigten over at bygge en interaktiv PDF-fremviser-kontrol i Delphi VCL med PDFlibPas, og side-bitmap-cachen SetFormFieldValueAndRefresh skal invalidere ved hver forpligtet redigering, dækkes separat i stykket om fremviserens per-monitor-DPI-disk-side-cache
In-place-formularfelt-redigering, Tab-drevet felt-navigation, og den genindtrædelses-sikre forpligtelses-vej bag begge er en del af den interaktive fremviser-kontrol sendt med PDFlibPas, PDF-biblioteket til Delphi og C++Builder, sammen med resten af dens side-gengivelses-, annotations- og formularfelt-API-flade