A PDF Library for Delphi TPDFlibViewer vezérlője a helybeni form-mezőszerkesztőt annak saját OnExit eseményén keresztül kötelezi el, és ez a tervezési döntés egy klasszikus Delphi VCL-csapdát rejt: egy fókuszált vezérlő elrejtése, újraszülőzése, vagy megsemmisítése saját OnExit-kezelőjén belülről kiválthatja az OnExit-et másodszor is, mielőtt az első hívás visszatérne, visszaküldve az elkötelezési logikát önmagába
Az ebből keletkező hiba ellenáll a tiszta reprodukciónak. Egy felhasználó gyorsan tabolgat egy sor szövegmezőn át egy beszkennelt jelentkezési lapon, és időnként a megjelenítő hozzáférési hibát dob, vagy rosszabb, tovább fut, miközben csendben a rossz értéket írja egy mezőbe két tabbal korábban. Reprodukáld igény szerint, és a hiba visszatekintve nyilvánvalónak tűnik; üldözd egy egyetlen ügyfél összeomlási jelentéséből, és szellemnek tűnik, mert az, hogy a második OnExit ténylegesen kiváltódik-e, olyan ablak-handle- és fókusz-időzítéstől függ, amely mezőtípussal, gépelési sebességgel, és bármi mással, amit az üzenetsor abban a pillanatban csinál, vált
Hogyan tesz a TPDFlibViewer egy valódi szerkesztőt egy renderelt oldal tetejére
A TPDFlibViewer minden PDF-oldalt bitképre rendereli, és alapértelmezetten nem alakítja a formmezőket élő VCL-vezérlőkké, így a BeginEditFormField az a metódus, amely áthidalja a két világot: egy mezőindexszel meghívva, felkeresi a mező téglalapját, és kliens-koordinátákká alakítja, majd egy valódi TEdit-et vagy TMemo-t helyez arra a téglalapra egy szövegmezőhöz, vagy egy TComboBox-ot csDropDownList stílusban egy választómezőhöz, teljesen betöltve a mező aktuális értékével. Az ISO 32000-2 §12.7 definiálja, mi egy szöveg- vagy választó-formmező egy PDF-en belül, de semmi abban a specifikációban nem mondja meg, hogyan kellene egy Windows-alkalmazásnak lehetővé tennie, hogy valaki begépeljen egyet, és pontosan ezt a rést hivatott betölteni a BeginEditFormField. Mind az OnKeyDown, mind az OnExit ugyanahhoz a két megjelenítő-metódushoz van bekötve, az InplaceEditorKeyDown-hoz és az InplaceEditorExit-hez, minden szerkesztőn, amit a TPDFlibViewer létrehoz, egy párosítás, amely változatlanul szállít, mióta az interaktív formkitöltés először landolt v3.220.0-ban, és az OnExit az, ahol a baj kezdődik
Miért váltja ki a szerkesztő elrejtése másodszor is az OnExit-et?
A VCL-ben a TWinControl a Visible vagy a Parent egy fókuszált vezérlőn történő megváltozását okként kezeli, hogy azonnal elmozdítsa a fókuszt onnan, és a fókusz elmozdítása egy vezérlőtől pontosan az, ami kiváltja annak a vezérlőnek az OnExit eseményét, szinkronban, mielőtt a tulajdonság-hozzárendelés, amely kiváltotta, egyáltalán visszatérne. A CommitInplaceEditor, a metódus, amit a PDF Library for Delphi a helybeni szerkesztő bezárásához és értékének a formmezőbe való visszaírásához használ, pontosan ezt a két dolgot kell tegye kifelé menet: állítsa az Editor.Visible-t hamisra, és az Editor.Parent-et nilre, így a vezérlő abbahagyja az oldal tetejére rajzolást, és abbahagyja a bemenet fogadását. Tedd meg bármelyiket, amíg a szerkesztő még mindig fókuszban van, ami szinte mindig igaz, mivel a felhasználó épp elhagyta, és az OnExit ismét kiváltódik annak a hívásnak a közepén, amelynek az utolsó dolognak kellett volna lennie, amit az a szerkesztő OnExit-je valaha kivált
Mi romlik el, amikor a CommitInplaceEditor újrabelép önmagába?
Egy naiv elkötelezési metódus két módon fizet meg ezért. Vagy kétszer írja meg a mező értékét, egyszer az eredeti hívásból, és egyszer az újrabelépő hívásból, amely besurrant, mielőtt az első befejezte volna saját állapotának érintését, vagy megpróbálja felszabadítani a szerkesztővezérlőt, miközben egy hívási vermen lejjebb lévő keret még mindig ugyanannak a vezérlőnek saját eseménykezelőjén belül van, ami definiálatlan terület a VCL-ben, és hozzáférési hibaként mutatkozik meg, amely szinte bármely sorra mutathat, nem feltétlenül arra, amelyik ténylegesen okozta. Egyik hibának sincs szüksége egy nagy formra a kiváltáshoz; egy kétmezős dokumentum elég, feltéve hogy a felhasználó elég gyorsan elhagyja a második mezőt ahhoz, hogy az OS még mindig letekerje az első fókuszüzeneteit
// Naiv verzió: review közben jónak tűnik, csak valódi gépelési sebesség mellett bukik el
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
CommitEditor; // még mindig FEditor saját OnExit-jén belül fut
end;
procedure TMyPdfViewer.CommitEditor;
begin
if not Assigned(FEditor) then
Exit;
SaveFieldValue(FEditor.Text);
FEditor.Parent := nil; // a fókuszált vezérlő itt kap új szülőt: az OnExit
// ismét kiváltódik, újrabelépve ugyanebbe a metódusba
FEditor.Free; // felszabadítva, miközben egy lejjebbi hívó a
FEditor := nil; // stacken még mindig a saját OnExit-kezelőjén belül van
end;
Nullázd a hivatkozást, mielőtt hozzányúlnál a vezérlőhöz
A javítás, amit a PDF Library for Delphi szállít, egyetlen átrendezés: rögzítsd a szerkesztőt egy helyi változóban, töröld a mezőt, amely rá mutat, és csak azután kezdd el megváltoztatni a vezérlő tulajdonságait. A CommitInplaceEditor beolvassa a FInplaceEditor-t egy helyi Editor változóba, azonnal nilre állítja a FInplaceEditor-t, és csak azután rendeli hozzá az Editor.Visible-t és Editor.Parent-et. Egy újrabelépő hívás, amit e két hozzárendelés bármelyike kivált, saját maga olvassa be a FInplaceEditor-t, azt már nilnek találja, és kilép a saját legelső során, mielőtt hozzáérhetne az Editor-hoz, vagy másodszor is megírhatná a mező értékét
procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
Editor: TWinControl;
begin
Editor := FInplaceEditor;
if not Assigned(Editor) then
Exit; // egy újrabelépő hívás itt landol és leáll
FInplaceEditor := nil; // leválasztás, mielőtt a vezérlőhöz egyáltalán hozzáérnénk
if Save then
SaveEditorValue(Editor); // biztonságos: a FInplaceEditor már nil
Editor.Visible := False;
Editor.Parent := nil; // esetleg ismét kiváltja az OnExit-et; a fenti őr
// ezt az újrabelépő hívást no-op-pá teszi
ReapDeadEditor; // felszabadítja, ami az előző ciklusból ott parkolt
FDeadEditor := Editor; // ezt parkoljuk ahelyett, hogy itt felszabadítanánk
end;
A SaveEditorValue abban a listázásban a valódi ágat helyettesíti, amely ellenőrzi, hogy az Editor TComboBox-e, TMemo-e, vagy TEdit-e, és ennek megfelelően olvassa be az értékét, mivel a PDF Library for Delphi eltérő vezérlőt hoz létre attól függően, hogy a mező szövegmező-e vagy választómező. Az őrt nem érdekli, melyik ág fut, csak az, hogy a FInplaceEditor nil legyen, mielőtt bármi, ami képes kiváltani az OnExit-et, végrehajtódna, ami az az egyetlen sorrendi megkötés, amely biztonságossá teszi a metódus többi részének megírását bármilyen egyébként természetes stílusban
Soha ne szabadíts fel egy vezérlőt saját eseményén belülről
A TPDFlibViewer.CommitInplaceEditor soha nem hívja meg közvetlenül az Editor.Free-t, és ez szándékos: egy vezérlő felszabadítása nem biztonságos, amíg egy, ugyanahhoz a vezérlőhöz tartozó saját eseménydiszpécselés stack-kerete még mindig letekeredik a hívás fölött, amely felszabadítja, újrabelépő OnExit vagy sem. A PDF Library for Delphi ehelyett átadja a leválasztott szerkesztőt egy egyhelyes parkolóhelynek, a FDeadEditor-nak, felszabadítva bármit, ami az előző szerkesztési ciklusból ott ült, egy kis segédfüggvényen keresztül, a ReapDeadEditor-on, amelyet a következő BeginEditFormField elején hívnak meg, és még egyszer a CloseDocument-ből; minden szerkesztőt, amit a megjelenítő létrehoz, maga a megjelenítő is birtokol, TEdit.Create(Self), nem TEdit.Create(nil), így még egy vezérlő is, amely a FDeadEditor-ban parkol, amikor a megjelenítő megsemmisül, a szokásos VCL-komponens-tulajdonlás söpri fel, ahelyett hogy szivárogna
procedure TPDFlibViewer.ReapDeadEditor;
begin
if Assigned(FDeadEditor) then
begin
FDeadEditor.Free; // most már biztonságos: e vezérlő saját OnExit-je
FDeadEditor := nil; // legalább egy szerkesztési ciklussal ezelőtt befejeződött
end;
end;
function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
Edit: TEdit;
begin
Result := 0;
CommitInplaceEditor(True); // kiüríti, ami éppen nyitva maradt szerkesztőként
// ... mezőkeresés és téglalap-konverzió kihagyva ...
ReapDeadEditor; // most már biztonságos felszabadítani az előző ciklus parkolt szerkesztőjét
Edit := TEdit.Create(Self);
Edit.Parent := Self;
Edit.OnExit := InplaceEditorExit;
FInplaceEditor := Edit;
FInplaceEditor.SetFocus;
Result := 1;
end;
Miért mutatkozik ez meg leginkább gyors Tab-navigáció közben?
A FocusNextFormField, a metódus, amit a PDF Library for Delphi v3.226.0-ban adott hozzá a Tab és Shift+Tab navigáció vezérléséhez egy formon át, meghívja a BeginEditFormField-et a következő jogosult mezőhöz minden egyes lépésnél, és a BeginEditFormField a CommitInplaceEditor(True) meghívásával kezd, hogy kiürítse bármit, amit az előző mező nyitva hagyott. Ez azt jelenti, hogy minden Tab-lenyomás, amit egy felhasználó egy többmezős form kitöltése közben tesz, egyszer lefuttatja a fent leírt pontos leválasztás-majd-érintés sorozatot, ami pontosan az a kódútvonal, amely a leginkább hordozhat egy ténylegesen fókuszban lévő vezérlőt abban a pillanatban, amikor a Visible és a Parent megváltozik, mert a Tab az az egy interakció, amely szinte garantáltan a kimenő szerkesztőnél tartja a fókuszt egészen addig, amíg az új nem kéri
Ebből semmi nem teszi a hibát megbízhatóan bemutathatóvá, és ezt érdemes egyszerűen kimondani, nem elkenni. Az, hogy egy adott Visible- vagy Parent-hozzárendelés ténylegesen szinkron OnExit-et kényszerít-e ki, olyan fókusz- és ablak-handle-állapottól függ, amit egy debugger már azzal megváltoztat, hogy csatolva van, amit egy nem kapcsolódó újrarajzolás vagy időzítő megzavarhat, és amely eltérően viselkedik attól függően, melyik van játékban a TEdit, a TMemo, vagy a TComboBox közül. Egy őr, amelyet csak néha gyakorolnak, az oka annak, hogy ez a fajta hiba túléli mind a kódáttekintést, mind a kézi tesztelést, és ez az oka annak is, hogy a javításnak felépítésénél fogva helyesnek kell lennie, a hivatkozás nullázásával, mielőtt bármi más történne, nem pedig annak alapján helyesnek, amit néhány kézi tesztátfutás véletlenül megfigyelt
Ennek a javításnak az általános alakja messze túlmutat egyetlen megjelenítő-vezérlőn. Bármely egyéni szerkesztési felület, amelyet egy élő VCL-vezérlő renderelt tartalomra rétegezésével építenek, nem csak egy PDF formmező, ugyanazt a veszélyt örökli abban a pillanatban, amikor a bezárás-és-elkötelezés logikáját mind egy explicit felhasználói művelet, mind egy implicit fókuszváltás kiválthatja, és ugyanaz a kétrészes válasz alkalmazandó: töröld azt a hivatkozást, amely az aktív vezérlőt azonosítja, mielőtt bármit tennél, ami kiválthatná saját exit-eseményét, és soha ne hívj Free-t egy olyan kódútvonalról, amely még mindig futhat annak a vezérlőnek saját eseménydiszpécselése alatt. A TPDFlibViewer szélesebb form-kitöltési és renderelési felülete, beleértve azt, hogyan dönti el, melyik vezérlőtípust mutassa melyik mezőhöz, a Delphi VCL-ben interaktív PDF-megjelenítő vezérlő építéséről szóló áttekintésben szerepel a PDFlibPasszal, és a lapbitkép-gyorsítótár, amit a SetFormFieldValueAndRefresh-nek minden elkötelezett szerkesztésnél érvénytelenítenie kell, külön a a megjelenítő monitoronkénti DPI lemez-oldalgyorsítótáráról szóló cikkben szerepel
A helybeni formmező-szerkesztés, a Tab-vezérelt mezőnavigáció, és a mindkettő mögötti újrabelépés-biztos elkötelezési útvonal a Delphihez és C++Builderhez készült PDF Library for Delphi PDF-könyvtárral szállított interaktív megjelenítő vezérlő része, oldal-renderelési, annotáció-, és formmező-API felületének többi részével együtt