Technisch artikel

HotXLS Delphi Component: Office-free workbook automation in Delphi

Als de enige taak van een server is om Excel-bestanden uit te zenden, hoort hij geen Excel te draaien. Office installeren op een build-agent of een rapportservice om het via COM-automatisering aan te sturen, is het verkeerde ontwerp, en dat is het al zo lang als de praktijk bestaat. Microsoft zegt het zelf, in richtlijnen die twintig jaar lang niet zijn verzacht: Office is niet gebouwd noch gelicentieerd om te worden geautomatiseerd vanuit een onbewaakt, server-side proces. Het juiste antwoord is de BIFF- en OOXML-bytes rechtstreeks te schrijven, zonder Excel ergens in beeld. Dat is de hele premisse van HotXLS, een native Object Pascal-bibliotheek die de spreadsheetformaten zelf leest en schrijft, zodat er geen desktoptoepassing is om te blijven hangen, te lekken of per zitplaats voor te betalen

Waarom EXCEL.EXE aansturen vanuit een service faalt

COM-automatisering bedient op afstand een desktopprogramma, en een desktopprogramma neemt stilzwijgend drie dingen aan die een Windows-service het niet kan geven: een geladen gebruikersprofiel, een interactief window station, en een mens die naar het scherm kijkt. Strip die weg en de fouten arriveren in een vorm die geen ontwikkelaarsmachine ooit reproduceert. Een bestandsherstelmelding, een add-infout of een licentieactiveringsdialoog opent op een desktop die niemand kan zien, en de automatiseringsaanroep die het veroorzaakte, keert nooit terug. De aanroeper krijgt uiteindelijk een timeout en sterft; de Excel-instantie vaak niet, en overleeft als een wees die bestandsvergrendelingen vasthoudt en de volgende run vergiftigt. Iedereen die elf verdwaalde EXCEL.EXE-processen onder een serviceaccount heeft zien opstapelen, kent de rest van dat verhaal

Diagram dat een Delphi-service die EXCEL.EXE bestuurt via COM-automatisering, waar verborgen dialogen en weesprocessen aanroepen blokkeren, afzet tegen HotXLS dat BIFF8- en OOXML-werkboekbytes direct in-process schrijft
COM-automatisering erft de ontbrekende veronderstellingen van een desktopprogramma, terwijl HotXLS BIFF8- en OOXML-bytes rechtstreeks schrijft met niets te installeren op de server

Het schaalverhaal is niet beter, zelfs als er niets crasht. Een Excel-instantie is een pipeline voor één werkmap tegelijk, elke eigenschapstoegang betaalt de kosten van cross-process COM-marshaling, en de machine die de code draait, draagt een Office-licentie waarvan de voorwaarden precies dit gebruik uitsluiten. De meeste teams stuiten op deze grenzen één storing per keer, en dat is ongeveer hoe "de COM-laag afschaffen" op een roadmap terechtkomt

Voordat die herschrijving begint, moet je één scopevraag beslechten, want die bepaalt hoeveel van het werk echt is. COM-code stelt bijna nooit alleen celwaarden in. Het roept Workbook.SaveAs aan met formaatconstanten, dwingt herberekening af, stuurt afdrukinstellingen, en grijpt soms naar het klembord. Loop de oude code door en noteer welke van die gedragingen daadwerkelijk in de uitvoer terechtkomen, want elk landt in een ander hoekje van een native bibliotheek, en een paar ervan (klembordinterop is het voor de hand liggende voorbeeld) hebben geen server-side betekenis en moeten worden weggelaten in plaats van overgezet

Twee native engines, twee eigendomsmodellen

HotXLS vervangt het Excel-proces door twee directe formaatimplementaties. Een BIFF8-recordstroomengine (TXLSWorkbook, unit lxHandle) behandelt .xls. Een OOXML-pakketschrijver (TXLSXWorkbook, unit lxHandleX) produceert .xlsx dat voldoet aan ECMA-376 / ISO/IEC 29500. Er is niets te registreren en niets te installeren op de server, en je kunt zoveel werkmappen tegelijk open houden als het geheugen toelaat

Wat mensen vroeg laat struikelen is dat de twee facades hun geheugen anders bezitten, en het verschil is stil totdat het crasht:

var
  Book: IXLSWorkbook;          // interfaceverwijzing: automatisch vrijgegeven
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // gewoon object: jij geeft het vrij
  SheetX: TXLSXWorksheet;
begin
  // BIFF8-.xls-uitvoer - geen Free; de interface-refcount bezit het
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // OOXML-.xlsx-uitvoer - expliciete levensduur
  BookX := TXLSXWorkbook.Create;
  try
    SheetX := BookX.Sheets.Add('Report');
    SheetX.Cells[1, 1].Value := 'Generated without Excel';
    BookX.SaveAs('report.xlsx');
  finally
    BookX.Free;
  end;
end;

De XLS-facade is referentiegeteld via de IXLSWorkbook-interface. Declareer de variabele als het interfacetype en roep er nooit Free op aan; houd hetzelfde object vast in een gewone objectvariabele en geef het zelf vrij, en de refcount geeft het een tweede keer vrij. De XLSX-facade is een gewoon object dat een gewone try..finally wil. Celadressering is aan beide kanten 1-gebaseerd, wat de enige plek is waar ze het eens zijn. De bladverzamelingen niet: Entries aan de XLS-kant is 1-gebaseerd, de XLSX-Items-indexer is 0-gebaseerd, en die off-by-one compileert netjes hoe je het ook fout doet en toont zich pas tijdens runtime

Een werkmap rechtstreeks naar een HTTP-respons schrijven

Een server-side export heeft meestal geen reden om de schijf aan te raken. Tijdelijke bestanden vragen om een opruimbeleid, botsen bij gelijktijdige verzoeken, en laten klantdata achter op volumes waar niemand aan dacht ze te auditen. Beide facades nemen een TStream via hun SaveAs-overloads, zodat de werkmap rechtstreeks in de respons terecht kan komen:

Diagram dat de twee HotXLS Delphi-facades vergelijkt: TXLSWorkbook automatisch vrijgegeven via IXLSWorkbook-interface-referentietelling, en TXLSXWorkbook als plat object dat een expliciete Free in een try..finally-blok nodig heeft
De XLS-façade wordt vrijgegeven door interface-referentietelling terwijl de XLSX-façade een expliciete Free nodig heeft, en de sheet-collecties verschillen tussen 1-gebaseerde Entries en 0-gebaseerde Items
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // schrijft vanaf de HUIDIGE streampositie
  Mem.Position := 0;         // spoel terug voordat je de stream overhandigt
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // het framework bezit Mem nu
finally
  Book.Free;
end;

De terugspoeling is de regel die zijn commentaar verdient. SaveAs(Stream) schrijft vanaf de huidige positie van de stream en zoekt daarna nooit terug naar nul. Vergeet Mem.Position := 0 en de client krijgt een download van nul bytes, of Excel meldt het bestand corrupt. Dit is de meest voorkomende bug in web-gerichte workbook-code, en de wreedste, omdat hij ongemerkt langs elke unit test glipt die alleen bevestigt dat de stream een lengte groter dan nul heeft

Eén workbook-bouwroutine bereikt elk ander afleverformaat zonder herstructurering. SaveAsCSV beantwoordt het verzoek "geef me gewoon de ruwe data", SaveAsHTML behandelt "zet het in een portaalpagina", SaveAsRTF voedt documentpipelines, en SaveAsODS dekt een OpenDocument-mandaat, allemaal met zowel bestands- als stream-overloads. Eén exportroutine plus een formaatparameter vervangt wat vroeger vier aparte COM-macro's plachten te zijn. De TXLSXHtmlExportOptions van de HTML-exporter draagt titel, CSS-klasse en een fragment-of-volledig-document-schakelaar, wat het portaalgeval buiten het regex-bewerken van geëxporteerde markup houdt

Diagram van een Delphi-verzoekhandler die een HotXLS-werkboek in een TMemoryStream opslaat, Mem.Position terugspoelt naar nul, en de stream aan het HTTP-antwoord overhandigt, met de CSV-, HTML-, RTF- en ODS-exporters erbij
Opslaan in een TMemoryStream en die terugspoelen vóór de overdracht stuurt workbook-bytes rechtstreeks naar de client, en één exportroutine dekt de CSV-, HTML-, RTF- en ODS-writers

Formulewaarden zonder een Excel-proces om ze te berekenen

Onder COM-automatisering herberekende Excel alles gratis, en COM loslaten trekt dat stilletjes in. SaveAs slaat formules op als tekst zonder ze te evalueren; de getallen verschijnen pas zodra Excel het bestand opent en herberekent, gedrag dat de XLS-facade je laat afstemmen via RecalcOnSave en CalculationMode. Voor een bestand dat naar een mens gaat is dat precies goed. Het is fout voor een service die een totaal moet bevestigen voordat het wordt uitgeleverd, en fout voor CSV-export, die de formuletekst schrijft in plaats van het resultaat. Beide gevallen moeten op de server evalueren met de ingebouwde engine:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX-facade: geen '='-prefix
Total := BookX.Calculate('SUM(A1:A2)');       // evalueer nu op de server
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

De facadeconventie bijt hier opnieuw. De XLSX-kant wijst expressies toe via Cell.Formula zonder isgelijkteken; de XLS-kant schrijft ze via Cell.Value met een voorafgaand '='. Draag code ongewijzigd van de ene naar de andere over en de verkeerde conventie slaat een tekststring op die slechts op een formule lijkt, zonder fout om het te markeren. Wanneer de formules van een werkmap in je eigen bedrijfslogica moeten reiken, laat de callback OnUserFunction de engine onbekende functienamen tijdens evaluatie doorgeven aan Delphi-code. Dat is de native vervanging voor de UDF-add-ins die de neiging hebben zich te verstoppen in precies de spreadsheets waaromheen een COM-automatiseringssysteem is opgegroeid

Deploymentrandjes die pas op de server naar boven komen

Een paar details bepalen of de uitrol schoon verloopt of verwarrend, en het eerste is de unitgraaf. De drag-and-drop-dataset-exporter TDataToXLS trekt VCL Forms, Controls en Dialogs binnen. Onschuldig in een desktoptool; in een consoleservice sleept het de hele VCL erachteraan mee. De kernunits lxHandle en lxHandleX grijpen alleen naar Windows, Classes, SysUtils en Variants, dus een pure service kan beter zijn eigen dataset-lus tegen de kern-API schrijven dan de component voor het gemak te importeren

Dan is er threading. Workbook-instanties zijn niet thread-safe, maar ze delen ook geen globale status, dus het patroon dat schaalt is het simpelste: één workbook-object per taak, of per workerthread. Dat koopt parallelle rapportgeneratie, wat een enkele gedeelde Excel-instantie nooit kan. Een requesthandler die zijn eigen werkmap aanmaakt, vult, opslaat en vrijgeeft, heeft helemaal geen locks nodig, en de impactstraal van een fout krimpt van "de gedeelde Excel-instantie zit voor iedereen vast" naar "dit ene verzoek gooide een uitzondering," waar je bestaande foutafhandeling al mee weet om te gaan

Formaatkeuze is de laatste van het stel. TXLSWorkbook.SaveAs schrijft standaard BIFF (xlExcel97), en XLS-inhoud naar .xlsx duwen loopt via de SaveXLSWorkbookAsXLSX-brug met verminderde getrouwheid. Kies de facade op basis van het formaat dat je van plan bent uit te leveren, op ontwerptijd, in plaats van in het ene te bouwen en aan het einde van de pipeline te converteren

Voor de datalaadhelft van een typisch vervangingsproject dekken de database-naar-workbook-exportpatronen zowel de component als de handgeschreven lus, en zodra rijaantallen zescijferig worden, maken de technieken voor prestaties van grote werkmappen het verschil tussen minuten en seconden. Rapporten opgebouwd uit door ontwerpers onderhouden lay-outs worden behandeld in de walkthrough voor sjabloon-rapportgeneratie

HotXLS wordt geleverd als Object Pascal-broncode voor Delphi en C++Builder; edities, licenties en de volledige API-referentie staan op de productpagina van HotXLS Delphi Component