Tekninen artikkeli

OnExit-uudelleenajautuvuus Delphi-paikallisessa lomake-editorissa

PDF Library for Delphi:n TPDFlibViewer-ohjausobjekti sitoo paikallisen lomakekenttäeditorin editorin oman OnExit-tapahtuman kautta, ja tuo suunnitteluvalinta piilottaa klassisen Delphi VCL -ansan: fokusoidun ohjausobjektin piilottaminen, uudelleenvanhemmoiminen tai tuhoaminen sen oman OnExit-käsittelijän sisältä voi laukaista OnExit:n toisen kerran ennen kuin ensimmäinen kutsu palautuu, lähettäen sitomislogiikan takaisin itseensä

Tämän tuottama epäonnistuminen vastustaa puhdasta toistamista. Käyttäjä tabuloi nopeasti läpi rivin tekstikenttiä skannatussa hakemuslomakkeessa, ja silloin tällöin katseluohjelma nostaa access violationin, tai pahempaa, jatkaa toimintaansa hiljaa kirjoittaen väärän arvon kenttään kaksi tabia taaksepäin. Toista se pyynnöstä, ja bugi näyttää jälkikäteen ilmeiseltä; jäljitä se yhden asiakkaan kaatumisraportista, ja se näyttää haamulta, koska laukeaako toinen OnExit todella, riippuu ikkunakahva- ja fokusajoituksesta, joka vaihtelee kenttätyypin, kirjoitusnopeuden ja minkä tahansa muun mukaan, mitä viestijono tekee tuolla hetkellä

Miten TPDFlibViewer asettaa todellisen editorin renderöidyn sivun päälle

TPDFlibViewer renderöi jokaisen PDF-sivun bittikartaksi eikä muuta lomakekenttiä eläviksi VCL-ohjausobjekteiksi oletuksena, joten BeginEditFormField on metodi, joka siltaa nämä kaksi maailmaa: kutsuttuna kenttäindeksillä se hakee kentän suorakulmion ja muuntaa sen asiakaskoordinaateiksi, ja pudottaa sitten todellisen TEdit- tai TMemo-olion tuon suorakulmion päälle tekstikentälle, tai TComboBox-olion csDropDownList-tyylissä valintakentälle, kentän nykyisen arvon jo ladattuna. ISO 32000-2 §12.7 määrittelee, mikä teksti- tai valintalomakekenttä on PDF:n sisällä, mutta mikään tuossa spesifikaatiossa ei sano, miten Windows-sovelluksen pitäisi antaa jonkun kirjoittaa sellaiseen, ja tuo aukko on juuri se, mitä varten BeginEditFormField on olemassa täyttämään. Sekä OnKeyDown että OnExit on kytketty samoihin kahteen katseluohjelmametodiin, InplaceEditorKeyDown ja InplaceEditorExit, jokaisessa editorissa, jonka TPDFlibViewer luo, pariutus, joka on toimitettu muuttumattomana siitä lähtien, kun interaktiivinen lomakkeen täyttö ensimmäisen kerran laskeutui v3.220.0:ssa, ja OnExit on se, mistä hankaluus alkaa

Kaavio TPDFlibVieweristä, joka sijoittaa toimivan VCL-editorin renderoidun PDF-sivun bittikartan päälle ja kytkee OnKeyDown- ja OnExit-tapahtumat katseluohjelman käsittelijöihin
BeginEditFormField asettaa sivun päälle oikean, kohdistuksen saaneen kontrolin, ja jokainen tällainen editori toimitetaan mukanaan OnExit — se ovi, josta reentranssi kävelee sisään

Miksi editorin piilottaminen laukaisee OnExit:n toisen kerran?

VCL:n TWinControl kohtelee Visible- tai Parent-ominaisuuden muutosta fokusoidulla ohjausobjektilla syynä siirtää fokus siitä pois välittömästi, ja fokuksen siirtäminen pois ohjausobjektista on juuri se, mikä laukaisee tuon ohjausobjektin OnExit-tapahtuman, synkronisesti, ennen kuin ominaisuudensijoitus, joka sen laukaisi, edes palautuu. CommitInplaceEditor, metodi, jota PDF Library for Delphi käyttää sulkeakseen paikallisen editorin ja kirjoittaakseen sen arvon takaisin lomakekenttään, tarvitsee tehdä juuri nuo kaksi asiaa poistuessaan: asettaa Editor.Visible false:ksi ja Editor.Parent nilliksi, jotta ohjausobjekti lakkaa piirtymästä sivun päälle ja lakkaa vastaanottamasta syötettä. Tee kumpi tahansa niistä, kun editorilla on yhä fokus, mikä sillä lähes aina on, koska käyttäjä juuri poistui siitä, ja OnExit laukeaa uudelleen keskellä sitä samaa kutsua, jonka piti olla viimeinen asia, jonka tuon editorin OnExit koskaan laukaisi

Mikä menee pieleen, kun CommitInplaceEditor ajautuu itseensä uudelleen?

Naiivi sitomismetodi maksaa tästä yhdellä kahdesta tavasta. Joko se kirjoittaa kentän arvon kahdesti, kerran alkuperäisestä kutsusta ja kerran uudelleenajautuvasta kutsusta, joka livahti sisään ennen kuin ensimmäinen ehti koskea omaa tilaansa, tai se yrittää vapauttaa editoriohjausobjektin, samalla kun kutsupinossa alempana oleva kehys on yhä saman ohjausobjektin oman tapahtumajakelun sisällä, mikä on määrittelemätöntä aluetta VCL:ssä ja näkyy access violationina, joka voi osoittaa lähes mihin tahansa riviin, ei välttämättä siihen, joka todella aiheutti sen. Kumpikaan epäonnistuminen ei tarvitse suurta lomaketta laukaistakseen; kahden kentän asiakirja riittää, kunhan käyttäjä poistuu toisesta kentästä tarpeeksi nopeasti, että käyttöjärjestelmä yhä purkaa fokusviestejä ensimmäisestä

// Naiivi versio: näyttää hyvältä katselmoinnissa, epäonnistuu vain todellisella kirjoitusnopeudella
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
  CommitEditor;               // yhä käynnissä FEditorin omassa OnExit-tapahtumassa
end;

procedure TMyPdfViewer.CommitEditor;
begin
  if not Assigned(FEditor) then
    Exit;
  SaveFieldValue(FEditor.Text);
  FEditor.Parent := nil;      // fokusoitu ohjausobjekti uudelleensijoitetaan tässä: OnExit
                               // laukeaa uudelleen, ajautuen takaisin samaan metodiin
  FEditor.Free;                // vapautetaan, vaikka kutsupinossa alempana oleva kutsuja on
  FEditor := nil;              // yhä sen oman OnExit-käsittelijän sisällä
end;

Nillaa viittaus ennen kuin kosketat ohjausobjektia

Korjaus, jonka PDF Library for Delphi toimittaa, on yksi uudelleenjärjestely: vangitse editori paikalliseen muuttujaan, tyhjennä kenttä, joka osoittaa siihen, ja vasta sitten aloita ohjausobjektin ominaisuuksien muuttaminen. CommitInplaceEditor lukee FInplaceEditor:n paikalliseen Editor-muuttujaan, asettaa FInplaceEditor:n heti nilliksi, ja vasta sen jälkeen asettaa Editor.Visible- ja Editor.Parent-arvot. Uudelleenajautuva kutsu, jonka jompikumpi noista kahdesta sijoituksesta laukaisee, lukee itse FInplaceEditor:n, löytää sen jo nilliksi, ja poistuu ensimmäisellä rivillään, ennen kuin se voi koskea Editor:iin tai kirjoittaa kentän arvon toisen kerran

procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
  Editor: TWinControl;
begin
  Editor := FInplaceEditor;
  if not Assigned(Editor) then
    Exit;                      // uudelleenajautuva kutsu päätyy tähän ja pysähtyy
  FInplaceEditor := nil;       // irrota ennen kuin ohjausobjektia kosketetaan lainkaan
  if Save then
    SaveEditorValue(Editor);   // turvallinen: FInplaceEditor on jo nil
  Editor.Visible := False;
  Editor.Parent := nil;        // saattaa laukaista OnExit:n uudelleen; yllä oleva vartija
                                // muuttaa tuon uudelleenajautuvan kutsun no-opiksi
  ReapDeadEditor;               // vapauta mitä tahansa pysäköitiin edellisellä kierroksella
  FDeadEditor := Editor;        // pysäköi tämä sen sijaan, että vapautettaisiin se tässä
end;

SaveEditorValue tuossa listauksessa edustaa todellista haaraa, joka tarkistaa, onko Editor TComboBox, TMemo vai TEdit, ja lukee sen arvon vastaavasti, koska PDF Library for Delphi luo eri ohjausobjektin riippuen siitä, onko kenttä tekstikenttä vai valintakenttä. Vartija ei välitä, mikä haara ajetaan, vain siitä, että FInplaceEditor on nil ennen kuin mikään, joka voi laukaista OnExit:n, suoritetaan, mikä on se yksi järjestysrajoite, joka tekee metodin lopun turvalliseksi kirjoittaa millä tahansa muuten luonnollisella tyylillä

Älä koskaan vapauta ohjausobjektia sen omasta tapahtumasta

TPDFlibViewer.CommitInplaceEditor ei koskaan kutsu Editor.Free:tä suoraan, ja se on tarkoituksellista: ohjausobjektin vapauttaminen on turvatonta, kun pinokehys, joka kuuluu saman ohjausobjektin omaan tapahtumajakeluun, saattaa yhä purkautua sen kutsun yläpuolella, joka vapauttaa sen, uudelleenajautuva OnExit tai ei. PDF Library for Delphi antaa sen sijaan irrotetun editorin yhden paikan pysäköintipaikkaan, FDeadEditor, vapauttaen mitä tahansa siellä istui edellisestä muokkauskierroksesta pienen apufunktion, ReapDeadEditor, kautta, kutsuttuna seuraavan BeginEditFormField:n alussa ja kerran vielä CloseDocument:sta; jokainen editori, jonka katseluohjelma luo, on myös katseluohjelman itsensä omistama, TEdit.Create(Self) TEdit.Create(nil):n sijaan, joten jopa ohjausobjekti, joka on yhä pysäköitynä FDeadEditor:iin, kun katseluohjelma tuhotaan, lakaistaan tavallisella VCL-komponenttiomistuksella vuotamisen sijaan

procedure TPDFlibViewer.ReapDeadEditor;
begin
  if Assigned(FDeadEditor) then
  begin
    FDeadEditor.Free;          // turvallista nyt: tämän ohjausobjektin oma OnExit
    FDeadEditor := nil;        // päättyi vähintään yhden muokkauskierroksen sitten
  end;
end;

function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
  Edit: TEdit;
begin
  Result := 0;
  CommitInplaceEditor(True);   // huuhtele mikä tahansa editori, joka on vielä auki
  // ... kentän haku ja suorakulmion muunnos jätetty pois ...
  ReapDeadEditor;              // nyt turvallista vapauttaa edellisen kierroksen pysäköity editori
  Edit := TEdit.Create(Self);
  Edit.Parent := Self;
  Edit.OnExit := InplaceEditorExit;
  FInplaceEditor := Edit;
  FInplaceEditor.SetFocus;
  Result := 1;
end;

Miksi tämä näkyy vaikeimmin nopean Tab-navigoinnin aikana?

FocusNextFormField, metodi, jonka PDF Library for Delphi lisäsi v3.226.0:ssa ohjatakseen Tab- ja Shift+Tab-navigointia lomakkeen yli, kutsuu BeginEditFormField:ää seuraavalle kelvolliselle kentälle jokaisella yksittäisellä hypyllä, ja BeginEditFormField alkaa kutsumalla CommitInplaceEditor(True) huuhdellakseen minkä tahansa editorin, jonka edellinen kenttä jätti auki. Se tarkoittaa, että jokainen Tab-painallus, jonka käyttäjä tekee täyttäessään monikenttäistä lomaketta, ajaa täsmälleen yllä kuvatun irrota-sitten-kosketa-sekvenssin kerran, mikä on juuri se koodipolku, jossa on todennäköisimmin ohjausobjekti aidosti fokusoituna sillä hetkellä, kun Visible ja Parent muuttuvat, koska Tab on se yksi vuorovaikutus, joka on lähes taatusti jättävä lähtevän editorin pitämään fokusta aina siihen asti, kunnes uusi pyytää sitä

PDF Library for Delphi -vuokaavio osoittaa, miten Editor.Parentin asettaminen arvoon nil kohdistetussa editorissa saa OnExitin laukeamaan uudelleen ja palaa CommitInplaceEditoriin ennen kuin ensimmäinen kutsu palaa
Yksittäinen uudelleenparentointisijoitus lähettää commitin takaisin itseensä ja avaa sekä kaksoiskirjoituksen että ennenaikaisen Free:n virhemoodit

Mikään tästä ei tee bugista luotettavaa osoitettavaa, ja se kannattaa sanoa suoraan sen sijaan, että se sivuutettaisiin. Se, pakottaako tietty Visible- tai Parent-sijoitus todella synkronisen OnExit:n, riippuu fokus- ja ikkunakahvatilasta, jota debuggeri muuttaa jo pelkästään kiinnittymällä, jota toisiinsa liittymätön uudelleenpiirto tai ajastin voi häiritä, ja joka käyttäytyy eri tavalla riippuen siitä, mikä TEdit-, TMemo- tai TComboBox-oliosta sattuu olemaan kyseessä oleva ohjausobjekti. Vartija, joka vain joskus tulee harjoitetuksi, on syy siihen, miksi tällainen vika selviää sekä koodikatselmuksesta että manuaalisesta testauksesta, ja se on myös syy siihen, miksi korjauksen on oltava oikea rakenteensa ansiosta, nillaten viittauksen ennen kuin mitään muuta tapahtuu, ei oikea sen mukaan, mitä käytöstä kourallinen manuaalisia testikierroksia sattui havaitsemaan

Tämän korjauksen yleinen muoto kulkee paljon yhtä katseluohjelmakomponenttia pidemmälle. Mikä tahansa mukautettu muokkauspinta, joka on rakennettu kerrostamalla elävä VCL-ohjausobjekti renderöidyn sisällön päälle, ei vain PDF-lomakekenttä, perii saman vaaran heti, kun sen sulje-ja-sido-logiikan voi laukaista sekä nimenomainen käyttäjätoiminto että implisiittinen fokuksenmuutos, ja sama kaksiosainen vastaus pätee: tyhjennä viittaus, joka tunnistaa aktiivisen ohjausobjektin, ennen kuin teet mitään, mikä saattaisi laukaista sen oman exit-tapahtuman, äläkä koskaan kutsu Free:tä koodipolusta, joka saattaisi yhä olla käynnissä tuon ohjausobjektin oman tapahtumajakelun alla. TPDFlibViewer:n laajempi lomakkeentäyttö- ja renderöintipinta, mukaan lukien miten se päättää, minkä ohjausobjektityypin näyttää millekin kentälle, käsitellään artikkelissa yleiskatsaus interaktiivisen PDF-katseluohjelmakomponentin rakentamiseen Delphi VCL:ssä PDF Library for Delphi:lla, ja sivun bittikarttavälimuisti, jonka SetFormFieldValueAndRefresh joutuu mitätöimään jokaisella sidotulla muokkauksella, käsitellään erikseen artikkelissa artikkeli katseluohjelman monitorikohtaisesta DPI-levysivuvälimuistista

PDF Library for Delphi: järjestyksessä kulkeva kuusivaiheinen commit-sarja, joka tyhjentää FInplaceEditorin ennen kontrollin koskettamista, käsittelee uudelleensyöttävän OnExitin ei-toimintona ja siirtää Free-kutsun FDeadEditor-paikan kautta myöhemmäksi
Viittauksen nollaus ensin saa jokaisen reentranttisen kutsun poistumaan välittömästi, ja pysäköity-editorin lokero siirtää jokaisen Free:n sellaiseen sykliin, jonka pinot ovat hiljaa

Paikallinen lomakekenttämuokkaus, Tab-ohjattu kentän navigointi ja niiden molempien takana oleva uudelleenajautuvuusturvallinen sitomispolku ovat osa interaktiivista katseluohjelmakomponenttia, joka toimitetaan PDF Library for Delphi:n, PDF-kirjaston Delphille ja C++Builderille, mukana, yhdessä sen muun sivunrenderöinti-, annotaatio- ja lomakekenttä-API-pinnan kanssa