Kontrola TPDFlibViewer u PDFlibPas-a potvrđuje uređivač polja obrasca u mestu kroz sopstveni događaj OnExit, a takav dizajn krije klasičnu zamku Delphi VCL-a: skrivanje, promena roditelja ili uništavanje fokusirane kontrole iz njenog OnExit rukovaoca može drugi put da pokrene OnExit pre nego što se prvi poziv vrati, pa se logika potvrde vraća sama u sebe
Greška koju ovo proizvodi teško se uredno ponavlja. Korisnik brzo pritiska Tab kroz niz tekstualnih polja na skeniranom obrascu, a čitač povremeno prijavi narušavanje pristupa ili, još gore, nastavi da radi i potajno upiše pogrešnu vrednost u polje dva koraka ranije. Kada se ponovi po želji, greška naknadno deluje očigledno; kada se prati iz jednog izveštaja o padu, izgleda kao duh, jer zavisi od vremena rada prozorskog rukohvata i fokusa, koje se menja sa vrstom polja, brzinom kucanja i svim ostalim što red poruka upravo radi
Kako TPDFlibViewer postavlja pravi editor preko renderovane stranice
TPDFlibViewer svaku PDF stranicu prikazuje kao bitmapu i podrazumevano ne pretvara polja obrasca u aktivne VCL kontrole, pa je BeginEditFormField most između ta dva sveta: kada dobije indeks polja, pronalazi pravougaonik polja i pretvara ga u koordinate klijenta, zatim preko njega postavlja pravi TEdit ili TMemo za tekstualno polje, odnosno TComboBox u stilu csDropDownList za polje izbora, sa već učitanom trenutnom vrednošću. ISO 32000-2 §12.7 definiše šta su tekstualno polje i polje izbora unutar PDF-a, ali ta specifikacija ne govori kako Windows aplikacija treba da omogući unos, i upravo tu prazninu popunjava BeginEditFormField. OnKeyDown i OnExit su povezani sa ista dva metoda čitača, InplaceEditorKeyDown i InplaceEditorExit, na svakom uređivaču koji TPDFlibViewer napravi, u paru koji se isporučuje nepromenjen od uvođenja interaktivnog popunjavanja obrazaca u v3.220.0, a problem počinje u OnExit-u
Zašto skrivanje editora drugi put aktivira OnExit
TWinControl u VCL-u promenu svojstva Visible ili Parent na fokusiranoj kontroli tretira kao razlog da joj odmah oduzme fokus, a upravo oduzimanje fokusa sinhrono pokreće OnExit te kontrole, pre nego što se vrati dodela svojstva koja je sve izazvala. CommitInplaceEditor, metod koji PDFlibPas koristi za zatvaranje uređivača u mestu i upis njegove vrednosti nazad u polje obrasca, pri izlasku mora da uradi upravo te dve stvari: postavi Editor.Visible na False i Editor.Parent na nil, da kontrola prestane da se iscrtava preko stranice i da prestane da prima unos. Ako bilo šta od toga uradi dok uređivač još ima fokus, što se gotovo uvek dešava jer ga je korisnik upravo napustio, OnExit se ponovo pokreće usred poziva koji je trebalo da bude poslednja radnja koju je OnExit tog uređivača izazvao
Šta se kvari kada CommitInplaceEditor ponovo uđe u sebe
Naivan metod potvrde to plaća na jedan od dva načina. Ili dvaput upiše vrednost polja, jednom iz prvobitnog poziva i jednom iz ponovnog ulaska koji se uvukao pre nego što je prvi poziv završio promenu sopstvenog stanja, ili pokuša da oslobodi uređivač dok je okvir dublje na steku još uvek u rukovaocu događaja iste kontrole, što je nedefinisano područje u VCL-u i pojavljuje se kao narušavanje pristupa koje može pokazati gotovo bilo koju liniju, ne nužno onu koja ga je stvarno izazvala. Za nijednu grešku nije potreban veliki obrazac; dovoljan je dokument sa dva polja, ako korisnik dovoljno brzo napusti drugo polje dok operativni sistem još razmotava poruke fokusa prvog
// 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;
Postavite referencu na nil pre rada sa kontrolom
Ispravka koju PDFlibPas isporučuje sastoji se od jednog preuređenja: sačuvati uređivač u lokalnoj promenljivoj, očistiti polje koje pokazuje na njega i tek onda početi menjati svojstva kontrole. CommitInplaceEditor čita FInplaceEditor u lokalnu promenljivu Editor, odmah postavlja FInplaceEditor na nil, a tek posle toga dodeljuje Editor.Visible i Editor.Parent. Ponovni ulazak izazvan bilo kojom od te dve dodele čita sam FInplaceEditor, nalazi ga već postavljenog na nil i izlazi već u prvom redu, pre nego što može da dodirne Editor ili drugi put upiše vrednost polja
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 u tom prikazu predstavlja stvarnu granu koja proverava da li je Editor TComboBox, TMemo ili TEdit i u skladu s tim čita njegovu vrednost, jer PDFlibPas pravi različitu kontrolu u zavisnosti od toga da li je polje tekstualno ili izborno. Zaštita ne zavisi od toga koja se grana izvršava, već samo od toga da FInplaceEditor bude nil pre svega što može da pokrene OnExit, što je jedino ograničenje redosleda koje ostatak metoda čini bezbednim za pisanje u inače prirodnom stilu
Nikada ne oslobađajte kontrolu iz njenog sopstvenog događaja
TPDFlibViewer.CommitInplaceEditor nikada direktno ne poziva Editor.Free, i to je namerno: oslobađanje kontrole nije bezbedno dok se okvir steka koji pripada otpremanju događaja te iste kontrole možda još odmotava iznad poziva koji je oslobađa, bez obzira na ponovni OnExit. PDFlibPas umesto toga odvaja uređivač na jedno mesto za privremeno čuvanje, FDeadEditor, a ono što je tamo ostalo iz prethodnog ciklusa uređivanja oslobađa malim pomoćnim metodom, ReapDeadEditor, koji se poziva na početku sledećeg BeginEditFormField i još jednom iz CloseDocument-a; svaki uređivač koji čitač napravi pripada i samom čitaču, kroz TEdit.Create(Self), a ne TEdit.Create(nil), pa čak i kontrolu koja je u trenutku uništavanja čitača još parkirana u FDeadEditor-u preuzima uobičajeno vlasništvo VCL komponenti umesto da procuri
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;
Zašto se ovo najviše vidi tokom brzog kretanja tasterom Tab
FocusNextFormField, metod koji je PDFlibPas dodao u v3.226.0 za navigaciju kroz obrazac pomoću Tab i Shift+Tab, poziva BeginEditFormField za sledeće podobno polje pri svakom koraku, a BeginEditFormField počinje pozivom CommitInplaceEditor(True) da isprazni uređivač koji je prethodno polje ostavilo otvorenim. Zato svaki pritisak na Tab tokom popunjavanja obrasca sa više polja jednom izvršava opisani redosled odvajanja pa dodirivanja, baš onu putanju u kojoj je najverovatnije da kontrola zaista još ima fokus u trenutku promene Visible i Parent, jer Tab gotovo sigurno ostavlja fokus na uređivaču iz kog se izlazi sve dok ga novi uređivač ne zatraži
Ništa od ovoga ne čini grešku pouzdanom za demonstraciju, i to vredi reći otvoreno. Da li određena dodela Visible ili Parent zaista prisiljava sinhroni OnExit zavisi od stanja fokusa i prozorskog rukohvata koje debugger menja samim povezivanjem, koje može poremetiti nepovezano ponovno iscrtavanje ili tajmer i koje se razlikuje u zavisnosti od toga da li je aktivna kontrola TEdit, TMemo ili TComboBox. Zaštita koja se izvršava samo ponekad razlog je što ovakav nedostatak preživi i pregled koda i ručno testiranje, a zato ispravka mora po konstrukciji da bude tačna, tako što referencu postavlja na nil pre svega ostalog, a ne da zavisi od ponašanja koje je slučajno pokazalo nekoliko ručnih testova
Opšti oblik ove ispravke primenljiv je daleko izvan jedne kontrole čitača. Svaka prilagođena površina za uređivanje napravljena postavljanjem aktivne VCL kontrole preko prikazanog sadržaja, ne samo polje PDF obrasca, nasleđuje istu opasnost čim logiku zatvaranja i potvrde mogu da pokrenu i izričita radnja korisnika i implicitna promena fokusa, pa važi isti odgovor u dva dela: obrišite referencu koja identifikuje aktivnu kontrolu pre svega što bi moglo da pokrene njen događaj izlaska i nikada ne pozivajte Free iz putanje koja još može da se izvršava ispod otpreme događaja te kontrole. Šira površina TPDFlibViewer-a za popunjavanje obrazaca i prikazivanje, uključujući način odlučivanja koju vrstu kontrole prikazati za koje polje, obrađena je u pregledu izgradnje interaktivne PDF kontrole čitača u Delphi VCL-u sa PDFlibPas-om, dok je keš bitmapa stranica koji SetFormFieldValueAndRefresh mora da poništi posle svake potvrđene izmene zasebno opisan u tekstu o kešu stranica na disku po monitoru i DPI vrednosti čitača
Uređivanje polja obrasca u mestu, navigacija poljima pomoću tastera Tab i putanja potvrde bez ponovnog ulaska koja stoji iza oba ponašanja deo su interaktivne kontrole čitača koja se isporučuje uz PDFlibPas, PDF biblioteku za Delphi i C++Builder, zajedno sa ostatkom njenog API-ja za prikaz stranica, anotacije i polja obrazaca