Wiele aplikacji wymaga generowanie dokumentów PDF – i dobrze, bo są fajne. Gdy jednak programista rzucony zostaje w całe wzburzone morze możliwych do zastosowania rozwiązań, bo nie jest już tak wesoło.
Kiedyś było prosto: brało się iTextSharp i wsio. Jednak od paru lat (?) licencja tego komponentu się zmieniła, i ja w sumie sam nie wiem kiedy można a kiedy nie można go używać oraz ile to kosztuje. Z niego korzystają inne dostępne rozwiązania, takie chociażby jak ciekawie wyglądający RazorPDF for MVC. A przecież nie chcemy licencji łamać, nie? Można rozejrzeć się także za płatnymi produktami, jak TallComponents.
Ja podczas researchu niechcący na blogu mojego brata, jeszcze z czasów konkursu Daj Się Poznać, natknąłem się na komentarz Gutka. Ot, przypadek! Gutek podpowiedział, że do generowania PDF można wykorzystać Reporting Services, nawet nie mając żadnego webserwisu czy sqla. Spróbowałem. Wyszło. Ba, okazało się to banalnie proste! Jest to o tyle fajne, że nie muszę niczego “rysować” w kodzie. Co jak co, ale do projektowania wyglądu PDFa designer to rzecz zdecydowanie pożądana (o czym można się zresztą przekonać zerkając na podlinkowany post).
A jak to osiągnąć?
Ja robiłem aplikację konsolową, ale pewnie podobnie proste jest to i w innych typach programów. Po pierwsze, dodałem do projektu New Item i z zakładki “Reporting” wybrałem Report Wizard:
Sam wizard nie jest banalny jeśli nie miało się z tym wcześniej do czynienia, ale na pewno każdy go bez większych problemów ogarnie w paręnaście minut. Wystarczy podać źródło danych (u mnie to był obiekt), porozrzucać kontrolki po powierzchni i tyle.
Dopiero później zaczęła się zabawa – dość ciężko było w necie znaleźć jakiś sensowny tutorial pokazujący jak, bez pośrednictwa zdalnej usługi renderującej raporty, stworzyć sobie PDFa. Oto co mi wyszło trochę metodą prób i błędów:
- plik definicji raportu (*.rdlc) oznaczamy we właściwościach jako “embedded resource” (to jest chyba nawet domyślna opcja); zamiast tego można wykorzystać fizyczny plik na dysku, ale nie lubię takich śmieci i w miarę możliwości staram się wrzucać do assembly wszystko co mi potrzebne; wada: zmiana w wyglądzie PDFa będzie wymagała rekomplikacji projektu
- szykujemy metodę, która nam ten plik wyłuska ze skompilowanego assembly (ja robię tak jak poniżej żeby nie babrać się w strukturze katalogów i przestrzeni nazw, IYKWIM):
- dodajemy referencje do Microsoft.ReportViewer.WinForms, System.Windows.Forms i System.XML
- implementujemy logikę generującą… PDFa, a cóżby innego:
Stream get_report_definition() { Assembly assembly = Assembly.GetExecutingAssembly(); var definitionName = assembly.GetManifestResourceNames() .Single(x => x.EndsWith("PdfDefinition.rdlc")); return assembly.GetManifestResourceStream(definitionName); }
public byte[] GeneratePdf(PdfSource data) { using (var localReport = new LocalReport()) { using (Stream reportDefinition = get_report_definition()) { localReport.LoadReportDefinition(reportDefinition); } string reportDataSourceName = localReport.GetDataSourceNames()[0]; var dataSource = new ReportDataSource(reportDataSourceName, new[] { data }); localReport.DataSources.Add(dataSource); byte[] pdfBytes = localReport.Render("PDF"); return pdfBytes; } }
I już.
PdfSource to klasa podana jako źródło danych w wizardzie odpalanym przy dodawaniu raportu do projektu.
Ta konstrukcja:
localReport.GetDataSourceNames()[0]
pozwala mi uniezależnić się od nazwy wpisanej w wizardzie, bo od razu po jej wpisaniu zapomniałem jaka ona była.
Obiekt z danymi podaję jako tablicę – bo tak trzeba (a pewnie w wielu przypadkach jest to nawet przydatne).
Na koniec renderuję raport przekazując “PDF” jako oczekiwany format.
Wsio. Działa. Teraz zostaje tylko uczynić ten plik “pięknym”, co jest zdecydowanie najtrudniejszą częścią całego zadania.