Nicht offensichtliche Funktionen der Rotativa-Anwendung zum Generieren von PDF in der ASP.NET MVC-Anwendung

Viele Entwickler stehen vor der Aufgabe, PDF-Berichte für Webanwendungen zu erstellen, eine ganz natürliche Anforderung. Ich möchte Sie auf meine Erfahrungen mit einer solchen Aufgabe bei der Verwendung der Rotativa-Bibliothek zur Erstellung von Berichten aufmerksam machen. Dies ist meiner Meinung nach eine der praktischsten Bibliotheken für einen solchen Zweck in ihrem Segment, aber als ich sie verwendete, stieß ich auf einige nicht offensichtliche Punkte, über die ich sprechen möchte.


Um ganz ehrlich zu sein, möchte ich die Reihe von Rechen teilen, auf die ich bei der Integration dieser Bibliothek gestoßen bin, ohne Zweifel schnell und sehr praktisch.


In diesem Artikel werde ich nicht auf das Problem der Auswahl einer Bibliothek eingehen. Jeder kann seine eigenen Gründe haben, dies oder das zu verwenden. Ich habe mich für Rotativa entschieden, weil es mit minimalen Einrichtungskosten alles Notwendige hat, um die Kundenanforderungen zu erfüllen. Zusätzlich zu ihr habe ich drei oder vier weitere Optionen ausprobiert.


Erklärung des Problems


Webanwendung unter ASP.NET MVC, .NET Version 4.6. Andere Funktionen sind in diesem Zusammenhang mit Ausnahme der Bereitstellung nicht relevant. Die Bereitstellung wird voraussichtlich in Azure erfolgen. Dies ist wichtig, da einige andere Bibliotheken (z. B. HiQPdf) in bestimmten Azure-Konfigurationen keine Installationen übertragen. Dies ist dokumentiert.


Ich muss einen bestimmten statischen HTML-Bericht mit einem Link und eine PDF-Version desselben Berichts mit dem zweiten Link öffnen. Der Bericht selbst besteht lediglich aus einigen Tabellen, Feldern und Grafiken, die dem Benutzer demonstriert werden sollen. Beide Versionen erfordern ein Menü mit Navigation durch Abschnitte des Berichts, das Vorhandensein von Tabellen und einige Grafiken (Farben, Textgröße, Rahmen).


Verwenden der Rotativa-Bibliothek


Rotativa ist meiner Meinung nach so einfach wie möglich anzuwenden.


  1. Sie haben bereits einen vorgefertigten HTML-Bericht in Form einer ASP.NET MVC-Vorlage und eines Controllers, z. B.:

[HttpGet] public async Task<ActionResult> Index(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return View(model); } 

  1. Installieren Sie das Nuget Rotativa-Paket


  2. Neuen Controller für PDF-Bericht hinzufügen



 [HttpGet] public async Task<ActionResult> Pdf(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return new ViewAsPdf("Index", model); } 

Im Wesentlichen wird von nun an eine PDF-Datei als Datei zurückgegeben, die alle Daten aus dem ursprünglichen HTML-Bericht enthält.


Ich habe das Routing hier nicht beschrieben, aber es versteht sich, dass Sie Routen so konfiguriert haben, dass beide Controller ordnungsgemäß aufgerufen werden


Interessanterweise ist diese Bibliothek selbst im Wesentlichen ein Wrapper über das bekannte Konsolendienstprogramm wkhtmltopdf . Die Geschwindigkeit der Arbeit in der Höhe, auf die Sie wetten können, wird funktionieren. Es gibt jedoch Funktionen, über die wir sprechen werden.


Seitenzahl


Es ist logisch anzunehmen, dass der Kunde das PDF druckt und die Seitenzahl sehen möchte. Dank der Macher von Rotativa ist hier alles sehr einfach.


Gemäß der Rotativa-Dokumentation können Sie über den Parameter CustomSwitches die Argumente angeben, die an das Dienstprogramm wkhtmltopdf . Nun, Online-Tipps sind großzügig mit Beispielen. Der folgende Aufruf fügt am Ende jeder Seite eine Nummer hinzu:


 return new ViewAsPdf("Index", model) { PageMargins = new Rotativa.Options.Margins(10, 10, 10, 10), PageSize = Rotativa.Options.Size.A4, PageOrientation = Rotativa.Options.Orientation.Portrait, CustomSwitches = "--page-offset 0 --footer-center [page] --footer-font-size 8 }; 

Es funktioniert großartig. Die Seitenzahl selbst wird mit dem Parameter [page] . Diese Art von Parametern wird durch bestimmte Werte ersetzt.


Neben [page] gibt es noch andere:
  • [Seite] Ersetzt durch die Anzahl der aktuell gedruckten Seiten
  • [frompage] Ersetzt durch die Nummer der ersten zu druckenden Seite
  • [topage] Ersetzt durch die Nummer der zuletzt zu druckenden Seite
  • [Webseite] Ersetzt durch die URL der zu druckenden Seite
  • [Abschnitt] Ersetzt durch den Namen des aktuellen Abschnitts
  • [Unterabschnitt] Ersetzt durch den Namen des aktuellen Unterabschnitts
  • [Datum] Ersetzt durch das aktuelle Datum im lokalen Systemformat
  • [isodate] Ersetzt durch das aktuelle Datum im erweiterten ISO 8601-Format
  • [Zeit] Ersetzt durch die aktuelle Zeit im lokalen Systemformat
  • [title] Ersetzt durch den Titel des aktuellen Seitenobjekts
  • [doctitle] Ersetzt durch den Titel des Ausgabedokuments
  • [sitepage] Ersetzt durch die Nummer der Seite auf der aktuellen Site, die konvertiert wird
  • [sitepages] Ersetzt durch die Anzahl der Seiten auf der aktuellen Site, die konvertiert werden


Inhaltsverzeichnis


Große mehrseitige Berichte erfordern PDF-Inhalt und Seitennavigation. Dies ist sehr praktisch und einfach wichtig, wenn die Anzahl der Seiten in einem Bericht einhundert überschreitet.


Das wkhtmltopdf-Handbuch enthält eine vollständige Liste aller Optionen, darunter --toc . Wenn dieser Parameter angezeigt wird, sammelt das Dienstprogramm im Wesentlichen alle Tags <h1>, <h2>, ... <h6> im Dokument und generiert darauf basierend ein Inhaltsverzeichnis. Dementsprechend ist es erforderlich, die korrekte Verwendung dieser Header-Tags in Ihrer HTML-Vorlage sicherzustellen.


In Wirklichkeit hat das Hinzufügen von --toc jedoch keine Konsequenzen. Als ob es keinen Parameter gäbe. Andere Optionen funktionieren jedoch. Dank eines Beitrags in einem Forum habe ich festgestellt, dass dieser Parameter ohne Bindestriche übergeben werden sollte: toc . In diesem Fall wird der Inhalt als allererste Seite hinzugefügt. Wenn Sie auf eine Zeile im Inhalt klicken, gelangen Sie zur gewünschten Seite im Dokument. Die Seitenzahlen sind korrekt.


Es ist noch nicht ganz klar, wie die Stile eingerichtet werden sollen, aber ich habe es noch nicht getan.


Javascript-Ausführung


Der nächste Punkt, auf den ich stieß, war die Notwendigkeit, dem Bericht Diagramme hinzuzufügen. Meine HTML-Seite enthält JS-Code, der mithilfe der Bibliothek dc.js Grafiken dc.js . Hier ist ein Beispiel:


 function initChart() { renderChart(@Html.Raw(Json.Encode(Model.Chart_1_Data)), 'chartDiv_1'); } function renderChart(data, chartElementId) { var colors = ['#03a9f4', '#67daff', '#8bc34a']; var barHeight = 45; var clientHeight = data.length * barHeight + 50; var clientWidth = document.getElementById(chartId).offsetWidth; var chart = dc.rowChart('#' + chartElementId); var ndx = crossfilter(dataToRender); var dimension = ndx.dimension(d => d.name); var group = dimension.group().reduceSum(d => d.value); chart .width(clientWidth) .height(clientHeight) .margins({ top: 16, right: 16, bottom: 16, left: 16 }) .ordinalColors(colors) .dimension(dimension) .group(group) .xAxis() .scale(d3.scaleLinear().domain([0, 2]).range([1, 3]).nice()); chart.render(); } 

Gleichzeitig habe ich in HTML ein entsprechendes Element:


 <div id="chart_C2" class="dc-chart"></div> 

Damit dieser Code funktioniert, müssen Sie die entsprechenden Bibliotheken importieren: dc.js , d3.js , crossfilter.js . Ein Aufruf der initChart Funktion erstellt ein Diagramm und fügt das Ergebnis ein
svg zum angegebenen Element im Baum.


Das PDF enthält jedoch keine Spur von Grafiken. Sowie alle anderen Spuren der Ausführung von JavaScript-Code vor dem PDF-Rendering. Dies zu überprüfen ist ganz einfach - fügen Sie einfach den Elementarcode hinzu, um ein einfaches <div> -Element mit Text zu erstellen, und testen Sie die Tatsache des JavaScript-Aufrufs.


Empirisch stellte sich heraus, dass der Speicherort des JS-Codes für wkhtmltopdf eine wichtige Rolle spielt. Der Code am Ende von <html> oder beispielsweise am Ende von <body> JS wird nicht ausgeführt. Es scheint, dass der Versorger ihn einfach nicht bemerkt oder nicht erwartet, ihn dort zu treffen.


Der Code im <head> wird jedoch ausgeführt. So kam ich zu dem Schema, wenn sich JavaScript-Code nach der Deklaration von Stilen innerhalb des <head> befindet und mit der üblichen Konstruktion aufgerufen wird:


 <body onload="initCharts()"> 

In diesem Fall wird der Code wie erwartet ausgeführt.


JavaScript-Einschränkungen


Das Ausgabe-PDF enthielt jedoch ohnehin keine Grafiken. Dann begann ich zu vermuten, dass die PDF-Rendering- und Ausführungs-Engine, da sie kein vollständiger Browser ist, höchstwahrscheinlich nicht perfekt ist und die letzten Regeln nicht versteht. Wiederum habe ich experimentell herausgefunden, dass Pfeilfunktionen nicht wahrgenommen werden. Wenn der Dolmetscher etwas Unbekanntes für ihn findet, funktioniert es einfach nicht mehr.


Ersetzen von x => x.value der Form x => x.value durch klassischere function(x) { return x.value; } function(x) { return x.value; } geholfen und der gesamte Code wurde ausgeführt. Das resultierende Diagramm wurde in eine PDF-Datei übertragen.


Diagrammbreite


Empirisch stellte sich heraus, dass die Breite des übergeordneten Elements des Diagramms klar angegeben werden musste. Dafür habe ich den dc-chart Stil angegeben. Es enthält die Breite des Diagramms in Pixel. Andernfalls ist das Diagramm in der PDF-Datei sehr klein, obwohl es in der HTML-Version die gesamte Breite einnimmt. Die Angabe der prozentualen Breite funktioniert nur für HTML.


Inline-JavaScript / CSS


Abschließend möchte ich darauf hinweisen, dass viele Bibliotheken, die HTML in PDF konvertieren, eine bestimmte baseUrl als Parameter akzeptieren. Dies ist die URL, auf deren Grundlage der Konverter die relativen Pfade für den Empfang importierter CSS-Stile, JavaScirpt-Dateien oder Schriftarten vervollständigt. Ich kann nicht sicher sagen, wie das in Rotativa funktioniert, aber ich habe mir einen anderen Ansatz ausgedacht.


Um das anfängliche Laden des Berichts zu beschleunigen und die Ursache für die Probleme beim Einbetten von Skript- oder Stildateien während der Konvertierung zu beseitigen, binde ich das erforderliche JS und CSS direkt in den Hauptteil der HTML-Vorlage ein.


Erstellen Sie dazu die entsprechenden Bundles:


 public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new StyleBundle("~/Styles/report-html") .Include("~/Styles/report-common.css") .Include("~/Styles/report-html.css") ); bundles.Add(new StyleBundle("~/Styles/report-pdf") .Include("~/Styles/report-common.css") .Include("~/Styles/report-pdf.css") ); bundles.Add(new ScriptBundle("~/Scripts/charts") .Include("~/Scripts/d3/d3.js") .Include("~/Scripts/crossfilter/crossfilter.js") .Include("~/Scripts/dc/dc.js") ); } } 

Fügen Sie Global.asax.cs einen Konfigurationsaufruf für diese Bundles Global.asax.cs


 protected void Application_Start() { ... BundleConfig.RegisterBundles(BundleTable.Bundles); } 

Fügen Sie die entsprechende Methode hinzu, um den Code in die Seite einzubetten. Es muss im selben Namespace wie Global.asax.cs werden, damit die Methode über die HTML-Vorlage aufgerufen werden kann:


 public static class HtmlHelperExtensions { public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<style rel=\"stylesheet\" type=\"text/css\">{bundleContent}</style>"; return new HtmlString(htmlTag); } public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<script type=\"text/javascript\">{bundleContent}</script>"; return new HtmlString(htmlTag); } private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath) { var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath); var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath); var bundleResponse = bundle.GenerateBundleResponse(bundleContext); return bundleResponse.Content; } } 

Nun, der letzte Schliff ist ein Aufruf aus der Vorlage:


 @Html.InlineStyles("~/Styles/report-pdf"); @Html.InlineScripts("~/Scripts/charts"); 

Infolgedessen befinden sich alle erforderlichen CSS- und JavaScript-Dateien direkt in HTML, obwohl Sie während der Entwicklung mit einzelnen Dateien arbeiten können.


Höchstwahrscheinlich werden viele sofort über die Ineffizienz dieses Ansatzes beim Zwischenspeichern von Anforderungen durch den Browser nachdenken. Aber ich hatte zwei spezifische Ziele:


  • damit der PDF-Konverter nicht irgendwo nach Stilen oder Code fragen muss und der Benutzer darauf warten muss;
  • Der erste Download von PDF- und HTML-Berichten dauert nur minimal, ohne auf einige zusätzliche Anfragen warten zu müssen. Im Kontext meines Projekts ist dies wichtig;

Seitenumbrüche


Die Strukturierung des Berichts in Abschnitte kann mit Anforderungen verbunden sein, um einen neuen Abschnitt von einer neuen Seite aus zu beginnen. In diesem Fall können Sie den einfachen CSS-Ansatz erfolgreich verwenden:


 .page-break-before { page-break-before: always; } .no-page-break-inside { page-break-before: auto; page-break-inside: avoid; } 

Das Dienstprogramm wkhtmltopdf liest diese Klassen erfolgreich und versteht, dass eine neue Seite gestartet werden muss. Die erste Klasse - page-break-before - weist das Dienstprogramm an, immer eine neue Seite mit diesem Element zu starten. Die zweite Klasse - no-page-break-inside - sollte auf diejenigen Elemente angewendet werden, die auf der Seite so vollständig wie möglich gehalten werden sollen. Sie haben beispielsweise aufeinanderfolgende Blöcke strukturierter Informationen oder sagen Tabellen. Wenn zwei Blöcke auf die Seite passen, werden sie gefunden. Wenn der dritte nicht in die Seite passt, ist es nicht der nächste. Wenn es größer als eine Seite ist, ist seine Übertragung bereits unvermeidlich. All dies funktioniert angemessen und bequem.


Flex-Verhalten in wkhtmltopdf


Nun, die letzte Funktion, die mir aufgefallen ist, bezieht sich auf die Verwendung von Flexbox-Markup-Stilen. Wir haben uns alle an sie gewöhnt und fast das gesamte Markup wird durch Flexes gemacht. Wkhtmltopdf liegt in dieser Hinsicht jedoch etwas zurück. Horizontale Flex-Optionen funktionieren nicht (zumindest in meinem Fall hat dies nicht funktioniert. Ich habe im Netzwerk erwähnt, dass es sich lohnt, Flex-Stile wie folgt zu duplizieren:


 display: -webkit-flex; display: flex; flex-direction: row; -webkit-flex-direction: row; -webkit-box-pack: justify; /* wkhtmltopdf uses this one */ -webkit-justify-content: space-between; justify-content: space-between; 

Dies führte jedoch leider nicht zum erwarteten Markup im PDF. Ich musste das Layout einiger Elemente wiederholen, damit die horizontale Platzierung der Blöcke den Anforderungen entsprach. Wenn jemand erfolgreiche Erfahrung mit der Integration von Flexes für wkhtmltopdf hat, teilen Sie diese bitte mit. Das wäre sehr hilfreich.


Einige Links:


Source: https://habr.com/ru/post/de425511/


All Articles