مقاله فنی

تولید فایل‌های اکسل در دلفی بدون اتوماسیون آفیس

اگر تنها کار یک سرور انتشار فایل‌های اکسل باشد، دلیلی برای اجرای اکسل ندارد. نصب آفیس روی یک عامل بیلد (build agent) یا سرویس گزارش‌گیری برای هدایت آن از طریق اتوماسیون COM یک طراحی اشتباه است، و از زمانی که این رویه وجود داشته است، طراحی اشتباهی بوده است. مایکروسافت خودش این را می‌گوید، در راهنمایی که در بیست سال گذشته نرم نشده است: آفیس نه ساخته شده و نه مجوز (license) دارد که از طریق یک فرآیند سمت سرور و بدون نظارت، خودکار شود. پاسخ درست نوشتن مستقیم بایت‌های BIFF و OOXML است، بدون اینکه اصلاً اکسل در تصویر باشد. این تمام فرض HotXLS است، یک کتابخانه بومی آبجکت پاسکال که فرمت‌های صفحه گسترده را خودش می‌خواند و می‌نویسد، بنابراین هیچ برنامه دسکتاپی برای هنگ کردن، نشت حافظه (leak)، یا پرداخت هزینه به ازای هر کاربر وجود ندارد

چرا هدایت EXCEL.EXE از طریق یک سرویس با شکست مواجه می‌شود

اتوماسیون COM یک برنامه دسکتاپ را از راه دور کنترل می‌کند، و یک برنامه دسکتاپ بی‌صدا سه چیزی را فرض می‌کند که یک سرویس ویندوز نمی‌تواند به آن بدهد: یک پروفایل کاربری بارگذاری شده، یک ایستگاه پنجره تعاملی، و انسانی که به صفحه نمایش نگاه می‌کند. این‌ها را حذف کنید و شکست‌ها به شکلی از راه می‌رسند که هیچ ماشین توسعه‌دهنده‌ای هرگز آن را بازتولید نمی‌کند. یک درخواست بازیابی فایل، یک خطای افزونه (add-in)، یا یک دیالوگ فعال‌سازی مجوز (license) روی دسکتاپی که هیچ کس نمی‌تواند ببیند باز می‌شود، و تماس اتوماسیونی که باعث آن شده هرگز باز نمی‌گردد. تماس‌گیرنده در نهایت دچار پایان مهلت زمانی (timeout) شده و متوقف می‌شود؛ نمونه (instance) اکسل اغلب اینگونه نیست، و به عنوان یتیمی باقی می‌ماند که قفل‌های فایل را نگه می‌دارد و اجرای بعدی را مسموم می‌کند. هر کسی که شاهد روی هم انباشته شدن یازده فرآیند سرگردان EXCEL.EXE در زیر یک حساب سرویس بوده باشد، بقیه آن داستان را می‌داند

داستان مقیاس‌پذیری حتی زمانی که هیچ چیزی خراب نمی‌شود بهتر نیست. یک نمونه اکسل یک خط لوله تک‌کارپوشه است، دسترسی به هر ویژگی هزینه مارشالینگ COM بین فرآیندی (cross-process) را می‌پردازد، و سیستمی که کد را اجرا می‌کند دارای یک مجوز آفیس است که شرایط آن این کاربرد دقیق را مستثنی می‌سازد. بیشتر تیم‌ها یک به یک با این محدودیت‌ها در قطعی‌های سیستم مواجه می‌شوند، که تقریباً به همین ترتیب است که "بازنشستگی لایه COM" به نقشه راه ختم می‌شود

قبل از شروع بازنویسی، یک سؤال محدوده (scope) را حل و فصل کنید، زیرا این موضوع تعیین می‌کند چه مقدار از کار واقعی است. کد COM تقریباً هرگز فقط مقادیر سلول را تنظیم نمی‌کند. با ثابت‌های فرمت، Workbook.SaveAs را فراخوانی می‌نماید، محاسبه مجدد را تحمیل می‌کند، تنظیمات چاپ را هل می‌دهد، گاهی به سراغ کلیپ‌بورد می‌رود. در کد قدیمی بگردید و یادداشت کنید که کدام یک از این رفتارها در خروجی ارسال می‌شوند، زیرا هر یک از این‌ها در گوشه متفاوتی از یک کتابخانه بومی قرار می‌گیرد، و چند تا از آن‌ها (تعامل با کلیپ‌بورد واضح‌ترین آن‌هاست) هیچ معنای سمت سروری ندارند و باید به جای پورت شدن (porting)، حذف شوند

دو موتور بومی، دو مدل مالکیت

HotXLS فرآیند اکسل را با دو پیاده‌سازی مستقیم فرمت مبادله می‌کند. یک موتور جریان رکورد BIFF8 (شامل TXLSWorkbook، یونیت lxHandle) به فایل‌های xls. رسیدگی می‌کند. یک نویسنده بسته OOXML (شامل TXLSXWorkbook، یونیت lxHandleX) فایل‌های xlsx. تولید می‌کند که با ECMA-376 / ISO/IEC 29500 مطابقت دارند. هیچ چیزی برای ثبت نام (register) و هیچ چیزی برای نصب روی سرور وجود ندارد، و می‌توانید تا جایی که حافظه اجازه می‌دهد، هر تعداد کارپوشه را به طور همزمان باز نگه دارید

چیزی که افراد را در اوایل کار با مشکل مواجه می‌کند این است که این دو نما (facade) حافظه خود را به طور متفاوتی در اختیار دارند، و این تفاوت تا زمانی که خراب شود بی‌صدا است:

var
  Book: IXLSWorkbook;          // interface reference: released automatically
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // plain object: you free it
  SheetX: TXLSXWorksheet;
begin
  // BIFF8 .xls output - no Free; the interface refcount owns it
  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 - explicit lifetime
  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 از طریق رابط IXLSWorkbook شمارش-مرجع (reference-counted) می‌شود. متغیر را به عنوان نوع رابط اعلام کنید و هرگز روی آن با Free تماس نگیرید؛ همان شی را در یک متغیر شی ساده نگه دارید و خودتان آن را آزاد کنید، و شمارشگر-مرجع آن را برای بار دوم آزاد می‌کند. نمای XLSX یک شی معمولی است که نیازمند try..finally معمولی است. آدرس‌دهی سلول در هر دو طرف مبتنی بر 1 است، که این تنها جایی است که این دو توافق دارند. مجموعه‌های برگه (sheet) توافق ندارند: نمایه Entries در سمت XLS مبتنی بر 1 است، نمایه Items در سمت XLSX مبتنی بر 0 است، و این خطای خارج از محدوده (off-by-one) به هر روشی که آن را اشتباه انجام دهید به طور تمیز کامپایل می‌شود و تنها در زمان اجرا خود را نشان می‌دهد

نوشتن یک کارپوشه مستقیماً به یک پاسخ HTTP

یک خروجی سمت سرور معمولاً دلیلی برای لمس دیسک ندارد. فایل‌های موقت نیازمند یک سیاست پاکسازی (cleanup policy) هستند، تحت درخواست‌های همزمان با هم برخورد می‌کنند و داده‌های مشتری را روی حجم‌هایی (volumes) باقی می‌گذارند که هیچ‌کس به فکر ممیزی آن‌ها نبود. هر دو نما یک TStream را از طریق سربارگذاری‌های (overloads) SaveAs خود دریافت می‌کنند، بنابراین کارپوشه می‌تواند مستقیماً وارد پاسخ (response) شود:

Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // writes from the CURRENT stream position
  Mem.Position := 0;         // rewind before handing the stream over
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // the framework now owns Mem
finally
  Book.Free;
end;

به عقب برگرداندن (rewind) خطی است که مستحق توضیح است. SaveAs(Stream) از موقعیت فعلی جریان می‌نویسد و پس از آن هرگز به صفر باز نمی‌گردد. Mem.Position := 0 را فراموش کنید و کلاینت یک دانلود صفر-بایتی دریافت می‌کند، یا اکسل فایل را خراب می‌خواند. این رایج‌ترین باگ در کد کارپوشه وب-محور و بی‌رحم‌ترین آن‌هاست، زیرا از هر آزمون واحدی که تنها تأیید می‌کند جریان دارای طول غیر صفر است، به سادگی عبور می‌کند

یک روال ساخت کارپوشه بدون تغییر ساختار به هر فرمت تحویل دیگر می‌رسد. SaveAsCSV به درخواست "فقط داده‌های خام را به من بده" پاسخ می‌دهد، SaveAsHTML درخواست "آن را در یک صفحه پورتال رها کن" را اداره می‌کند، SaveAsRTF خطوط لوله سند را تغذیه می‌نماید، و SaveAsODS یک اجبار OpenDocument را پوشش می‌دهد، همه این‌ها دارای سربارگذاری‌های فایل و جریان هستند. یک روال خروجی منفرد به اضافه یک پارامتر فرمت، جایگزین چیزی می‌شود که معمولاً چهار ماکرو مجزای COM بودند. TXLSXHtmlExportOptions صادرکننده HTML شامل عنوان، کلاس CSS و یک سوئیچ قطعه-یا-سند-کامل (fragment-or-full-document) است، که مورد پورتال را از کسب و کار ویرایش منظم نشانه‌گذاری صادر شده خارج نگه می‌دارد

مقادیر فرمول بدون یک فرآیند اکسل برای محاسبه آنها

تحت اتوماسیون COM، اکسل همه چیز را به صورت رایگان دوباره محاسبه می‌کرد، و حذف COM بی‌صدا آن را لغو می‌کند. SaveAs فرمول‌ها را به عنوان متن بدون ارزیابی آنها ذخیره می‌کند؛ اعداد تنها زمانی ظاهر می‌شوند که اکسل فایل را باز کرده و دوباره محاسبه کند، رفتاری که نمای XLS به شما امکان می‌دهد از طریق RecalcOnSave و CalculationMode تنظیم نمایید. برای فایلی که به دست یک شخص می‌رسد، این دقیقاً درست است. برای سرویسی که باید پیش از ارسال مجموع را تأیید کند، و برای صدور CSV، که متن فرمول را به جای نتیجه آن می‌نویسد، اشتباه است. هر دو مورد باید در سرور با موتور داخلی ارزیابی شوند:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // XLSX facade: no '=' prefix
Total := BookX.Calculate('SUM(A1:A2)');       // evaluate on the server, now
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

قرارداد نما دوباره در اینجا گاز می‌گیرد. سمت XLSX عبارات را از طریق Cell.Formula بدون علامت مساوی تخصیص می‌دهد؛ سمت XLS آنها را از طریق Cell.Value با یک '=' پیشتاز می‌نویسد. کد را بدون تغییر از یکی به دیگری منتقل کنید و قرارداد اشتباه یک رشته متنی را ذخیره می‌کند که صرفاً شبیه یک فرمول است، بدون هیچ خطایی که آن را نشانه‌گذاری کند. هنگامی که فرمول‌های یک کارپوشه نیاز به رسیدن به منطق تجاری شما دارند، تماس بازگشتی OnUserFunction به موتور اجازه می‌دهد نام توابع ناشناخته را در زمان ارزیابی به کد دلفی تحویل دهد. این جایگزین بومی برای افزونه‌های UDF است که تمایل دارند در همان صفحات گسترده‌ای پنهان شوند که یک سیستم اتوماسیون COM در اطراف آن رشد کرده است

لبه‌های استقرار که تنها روی سرور ظاهر می‌شوند

چند جزئیات تعیین می‌کنند که آیا استقرار تمیز است یا گیج‌کننده، و اولی نمودار واحد (unit graph) است. صادرکننده مجموعه داده با کشیدن و رها کردن TDataToXLS، Forms، Controls، و Dialogs VCL را وارد می‌کند. در یک ابزار دسکتاپ بی‌ضرر است؛ در یک سرویس کنسول کل VCL را به دنبال خود می‌کشد. واحدهای اصلی lxHandle و lxHandleX فقط برای Windows، Classes، SysUtils، و Variants دست دراز می‌کنند، بنابراین یک سرویس خالص بهتر است حلقه مجموعه داده خود را در برابر API اصلی بنویسد تا اینکه کامپوننت را برای راحتی وارد نماید

سپس موضوع ریسمان (threading) مطرح است. نمونه‌های کارپوشه ایمن-از-ریسمان (thread-safe) نیستند، اما هیچ وضعیت جهانی (global state) را هم به اشتراک نمی‌گذارند، بنابراین الگویی که مقیاس‌پذیر است ساده‌ترین آن‌هاست: یک شی کارپوشه در هر کار، یا در هر ریسمان کارگر (worker thread). این امر تولید گزارش موازی را به ارمغان می‌آورد، چیزی که یک نمونه اکسل به اشتراک گذاشته شده هرگز نمی‌تواند انجام دهد. یک کنترل‌کننده درخواست (request handler) که کارپوشه خاص خود را ایجاد، پر، ذخیره و آزاد می‌کند، به هیچ قفلی نیاز ندارد، و شعاع تخریب یک خرابی از "نمونه به اشتراک گذاشته شده اکسل برای همه گیر کرده است" به "تنها این یک درخواست یک استثنا ایجاد کرد" کاهش می‌یابد، که مدیریت خطای موجود شما از قبل می‌داند با آن چه کند

هدف‌گیری فرمت آخرین مورد از آن‌هاست. TXLSWorkbook.SaveAs به طور پیش‌فرض BIFF (xlExcel97) را می‌نویسد، و فشار دادن محتوای XLS به درون xlsx. از طریق پل SaveXLSWorkbookAsXLSX با وفاداری کاهش یافته انجام می‌شود. نما را بر اساس فرمتی که قصد ارسال آن را دارید، در زمان طراحی انتخاب کنید، به جای اینکه در یکی بسازید و در انتهای خط لوله تبدیل کنید

برای نیمی از بارگذاری-داده در یک پروژه جایگزین معمولی، الگوهای صدور از-پایگاه-داده-به-کارپوشه هم کامپوننت و هم حلقه دست‌نویس را پوشش می‌دهند، و هنگامی که شمارش ردیف‌ها به شش رقم رسید، تکنیک‌های عملکرد کارپوشه-بزرگ تفاوت بین دقیقه‌ها و ثانیه‌ها می‌شوند. گزارش‌های ساخته شده از چیدمان‌های نگهداری شده توسط طراح در راهنمای تولید گزارش قالبی پوشش داده شده‌اند

HotXLS به عنوان منبع آبجکت پاسکال برای دلفی و C++Builder عرضه می‌شود؛ نسخه‌ها، مجوزها، و مرجع کامل API در صفحه محصول کامپوننت HotXLS قرار دارند