Teknisk artikel

Generering af Excel-filer i Delphi uden Office-automatisering

Hvis en servers eneste job er at udsende Excel-filer, har den ingen forretning i at køre Excel. At installere Office på en build-agent eller en rapporttjeneste for at fjernstyre det via COM-automatisering er det forkerte design, og det har været det forkerte design, så længe praksissen har eksisteret. Microsoft siger det selv, i vejledning, der ikke er blevet blødere i tyve år: Office er hverken bygget eller licenseret til at blive automatiseret fra en uovervåget, server-side proces. Det rigtige svar er at skrive BIFF- og OOXML-bytene direkte, uden Excel i billedet overhovedet. Det er hele præmissen for HotXLS, et nativt Object Pascal-bibliotek, der selv læser og skriver regnearksformaterne, så der er ingen desktopapplikation at hænge, lække eller betale pr. sæde for

Hvorfor det fejler at fjernstyre EXCEL.EXE fra en tjeneste

COM-automatisering fjernstyrer et desktopprogram, og et desktopprogram antager i det stille tre ting, en Windows-tjeneste ikke kan give det: en indlæst brugerprofil, en interaktiv window station og et menneske, der ser på skærmen. Fjern dem, og fejlene ankommer i en form, ingen udviklermaskine nogensinde genskaber. En fil-gendannelsesprompt, en add-in-fejl eller en licens-aktiveringsdialog åbner på en desktop, ingen kan se, og automatiseringskaldet, der udløste den, vender aldrig tilbage. Kalderen får til sidst timeout og dør; Excel-instansen gør ofte ikke, og overlever som en forældreløs, der holder fillåse og forgifter næste kørsel. Enhver, der har set elleve strejfende EXCEL.EXE-processer hobe sig op under en tjenestekonto, kender resten af den historie

Diagram, der kontrasterer en Delphi-service, der driver EXCEL.EXE gennem COM-automatisering, hvor skjulte dialoger og forældreløse processer blokerer kald, med HotXLS, der skriver BIFF8- og OOXML projektmappebytes direkte in-process
COM-automatisering arver et desktopprograms manglende antagelser, mens HotXLS skriver BIFF8- og OOXML-bytes direkte uden noget at installere på serveren

Skaleringshistorien er ikke bedre, selv når intet går ned. En Excel-instans er en pipeline til én projektmappe ad gangen, hver egenskabsadgang betaler prisen for cross-process COM-marshalling, og boksen, der kører koden, bærer en Office-licens, hvis vilkår udelukker netop denne brug. De fleste teams møder disse grænser ét udfald ad gangen, hvilket nogenlunde er, hvordan "udfas COM-laget" ender på en roadmap

Før den omskrivning starter, skal man afklare ét scope-spørgsmål, fordi det afgør, hvor meget af arbejdet der er reelt. COM-kode sætter næsten aldrig kun celleværdier. Den kalder Workbook.SaveAs med formatkonstanter, tvinger genberegning, skubber udskriftsopsætning, griber nogle gange til udklipsholderen. Gennemgå den gamle kode, og skriv ned, hvilke af de adfærdsmønstre der reelt lander i output, da hver især lander i et forskelligt hjørne af et nativt bibliotek, og et par af dem (udklipsholder-interop er det oplagte) har ingen server-side betydning og bør droppes frem for porteres

To native motorer, to ejerskabsmodeller

HotXLS bytter Excel-processen ud med to direkte formatimplementeringer. En BIFF8-poststrøm-motor (TXLSWorkbook, unitten lxHandle) håndterer .xls. En OOXML-pakke-skriver (TXLSXWorkbook, unitten lxHandleX) producerer .xlsx, der overholder ECMA-376 / ISO/IEC 29500. Der er intet at registrere og intet at installere på serveren, og man kan holde lige så mange projektmapper åbne på én gang, som hukommelsen tillader

Det, der får folk til at snuble tidligt, er, at de to facader ejer deres hukommelse forskelligt, og forskellen er tavs, indtil den styrter:

var
  Book: IXLSWorkbook;          // grænsefladereference: frigives automatisk
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // almindeligt objekt: du frigør det
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls-output - intet Free; grænseflade-refcount ejer det
  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-output - eksplicit levetid
  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;

XLS-facaden er referencetalt gennem IXLSWorkbook-grænsefladen. Erklær variablen som grænsefladetypen, og kald aldrig Free på den; hold samme objekt i en almindelig objektvariabel og frigør det selv, og refcounten frigør det en anden gang. XLSX-facaden er et almindeligt objekt, der ønsker en almindelig try..finally. Celleadressering er 1-baseret på begge sider, hvilket er det ene sted, de to er enige. Arksamlingerne er det ikke: Entries på XLS-siden er 1-baseret, XLSX-Items-indekseren er 0-baseret, og den off-by-one kompilerer rent, uanset hvilken vej man tager fejl på, og viser sig først ved kørselstid

At skrive en projektmappe direkte ind i et HTTP-svar

En server-side-eksport har som regel ingen grund til at røre disken. Temp-filer kræver en oprydningspolitik, kolliderer under samtidige forespørgsler og efterlader kundedata liggende på volumener, ingen tænkte på at revidere. Begge facader tager en TStream via deres SaveAs-overloads, så projektmappen kan gå direkte ind i svaret:

Diagram, der sammenligner de to HotXLS Delphi-facader: TXLSWorkbook frigivet automatisk gennem IXLSWorkbook interface refcounting, og TXLSXWorkbook som et simpelt objekt, der behøver en eksplicit Free i en try..finally-blok
XLS-facaden frigøres af interface-refcounting, mens XLSX-facaden behøver et eksplicit Free, og ark-samlingerne adskiller sig mellem 1-based Entries og 0-based Items
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // skriver fra den AKTUELLE stream-position
  Mem.Position := 0;         // spol tilbage, før strømmen gives videre
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // frameworket ejer nu Mem
finally
  Book.Free;
end;

Tilbagespolingen er linjen, der fortjener sin kommentar. SaveAs(Stream) skriver fra strømmens nuværende position og søger aldrig tilbage til nul bagefter. Glem Mem.Position := 0, og klienten får et download på nul byte, eller Excel kalder filen ødelagt. Det er den mest almindelige fejl i webvendt projektmappe-kode, og den grusomste, fordi den sejler forbi enhver unit test, der kun konstaterer, at strømmen har en længde forskellig fra nul

Én projektmappe-byggende rutine rækker ud til alle andre leveringsformater uden omstrukturering. SaveAsCSV besvarer "giv mig bare rådataene"-anmodningen, SaveAsHTML håndterer "smid det ind i en portalside", SaveAsRTF fodrer dokumentpipelines, og SaveAsODS dækker et OpenDocument-krav, alt sammen med både fil- og stream-overloads. Én enkelt eksportrutine plus en formatparameter erstatter, hvad der plejede at være fire separate COM-makroer. HTML-eksportørens TXLSXHtmlExportOptions bærer titel, CSS-klasse og en fragment-eller-fuldt-dokument-kontakt, hvilket holder portal-tilfældet ude af forretningen med at regex-redigere eksporteret markup

Diagram over en Delphi request handler, der gemmer en HotXLS projektmappe i en TMemoryStream, spoler Mem.Position tilbage til nul og giver strømmen til HTTP-responsen, med CSV-, HTML-, RTF- og ODS-eksportørerne ved siden af
At gemme i en TMemoryStream og spole den tilbage før overdragelsen sender workbook-bytes direkte til klienten, og én eksportrutine dækker CSV-, HTML-, RTF- og ODS-skriverne

Formelværdier uden en Excel-proces til at beregne dem

Under COM-automatisering genberegnede Excel alt gratis, og at droppe COM tilbagekalder det i det stille. SaveAs gemmer formler som tekst uden at evaluere dem; tallene dukker først op, når Excel åbner filen og genberegner, en adfærd XLS-facaden lader dig tune via RecalcOnSave og CalculationMode. For en fil bestemt til et menneske er det præcis rigtigt. Det er forkert for en tjeneste, der skal bekræfte en total, før den leverer, og forkert for CSV-eksport, som skriver formelteksten frem for dens resultat. I begge tilfælde skal man evaluere på serveren med den indbyggede motor:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX-facade: intet '='-præfiks
Total := BookX.Calculate('SUM(A1:A2)');       // evaluér på serveren, nu
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

Facade-konventionen bider igen her. XLSX-siden tildeler udtryk via Cell.Formula uden lighedstegn; XLS-siden skriver dem via Cell.Value med et indledende '='. Bær kode fra den ene til den anden uændret, og den forkerte konvention gemmer en tekststreng, der blot ligner en formel, uden nogen fejl til at flage det. Når en projektmappes formler skal nå ind i din egen forretningslogik, lader OnUserFunction-callbacken motoren give ukendte funktionsnavne videre til Delphi-kode ved evalueringstidspunktet. Det er den native erstatning for de UDF-add-ins, der har en tendens til at gemme sig inde i netop de regneark, et COM-automatiseringssystem voksede op omkring

Udrulningskanter, der kun viser sig på serveren

Nogle få detaljer afgør, om udrulningen er ren eller forvirrende, og den første er unit-grafen. Den træk-og-slip dataset-eksportør TDataToXLS trækker VCL Forms, Controls og Dialogs med sig. Harmløs i et desktopværktøj; i en konsoltjeneste slæber den hele VCL'en med sig bagud. Kerneunittene lxHandle og lxHandleX rækker kun ud efter Windows, Classes, SysUtils og Variants, så en ren tjeneste er bedre tjent med at skrive sin egen dataset-løkke mod kerne-API'et end at importere komponenten for bekvemmelighedens skyld

Så er der trådning. Projektmappeinstanser er ikke trådsikre, men de deler heller ingen global tilstand, så det mønster, der skalerer, er det simpleste: ét projektmappeobjekt pr. job, eller pr. worker-tråd. Det køber parallel rapportgenerering, hvilket en enkelt delt Excel-instans aldrig kan. En request-handler, der opretter, udfylder, gemmer og frigør sin egen projektmappe, har slet ikke brug for låse, og en fejls sprængradius kollapser fra "den delte Excel-instans er fastlåst for alle" til "denne ene forespørgsel kastede en undtagelse", hvilket din eksisterende fejlhåndtering allerede ved, hvad den skal gøre med

Formatmålretning er den sidste af dem. TXLSWorkbook.SaveAs skriver BIFF (xlExcel97) som standard, og at skubbe XLS-indhold ind i .xlsx kører gennem SaveXLSWorkbookAsXLSX-broen med reduceret troskab. Vælg facaden ud fra det format, du har tænkt dig at levere, ved designtidspunktet, frem for at bygge i den ene og konvertere ved enden af pipelinen

For dataindlæsnings-halvdelen af et typisk erstatningsprojekt dækker mønstrene for eksport fra database til projektmappe både komponenten og den håndskrevne løkke, og når rækketal når seks cifre, bliver teknikkerne til ydeevne for store projektmapper forskellen mellem minutter og sekunder. Rapporter bygget af designer-vedligeholdte layouts er dækket i gennemgangen af skabelonbaseret rapportgenerering

HotXLS leveres som Object Pascal-kildekode til Delphi og C++Builder; udgaver, licensiering og den fulde API-reference findes på HotXLS-Delphi-komponentens produktside