Asp.net Core 3.1 Razor視圖模版動態渲染PDF
- 2020 年 5 月 19 日
- 筆記
- Asp.Net Core
Asp.net Core 3.1 Razor視圖模版動態渲染PDF
-
前言
最近的線上項目受理回執接入了電子簽章,老項目一直是html列印,但是接入的電子簽章是僅僅對PDF電子簽章,目前還沒有Html電子簽章或者其他格式文件的電子簽章。首先我想到的是用一個js把前端的html轉換PDF,再傳回去伺服器電子簽章。但是這個樣子就有一個bug,用戶可以在瀏覽器刪改html,這樣電子簽章的防刪改功能就用不到,那麼電子簽章還有啥意義?所以PDF簽章前還是不能給用戶有接觸的機會,不然用戶就要偷偷幹壞事了。於是這種背景下,本插件應運而生。我想到直接把Razor渲染成html,html再渲染成PDF。
該項目的優點在於,可以很輕鬆的把老舊項目的Razor轉換成PDF文件,無需後台組裝PDF,如果需要排版PDF,我們只需要修改CSS樣式和Html程式碼即可做到。而且我們可以直接先寫好Razor視圖,做到動態半可視化設計,最後切換一下ActionResult。不必像以前需要在腦海裡面設計PDF板式,並一次一次的重啟啟動調試去修改樣式。
2.依賴項目
本插件 支援net45,net46,core的各個版本,(我目前僅僅使用net45和core 3.1.對於其他版本我還沒實際應用,但是稍微調整都是支援的,那麼簡單來說就是支援net 45以上,現在演示的是使用Core3.1)。
依賴插件
Haukcode.DinkToPdf
RazorEngine.NetCore
第一個插件是Html轉換PDF的核心插件,具體使用方法自行去了解,這裡不多說。
第二個是根據數據模版渲染Razor.
3.核心程式碼
Razor轉Html程式碼
protected string RunCompileRazorTemplate(object model,string razorTemplateStr) { if(string.IsNullOrWhiteSpace(razorTemplateStr)) throw new ArgumentException("Razor模版不能為空"); var htmlString= Engine.Razor.RunCompile(razorTemplateStr, razorTemplateStr.GetHashCode().ToString(), null, model); return htmlString; }
Html模版轉PDF核心程式碼
private static readonly SynchronizedConverter PdfConverter = new SynchronizedConverter(new PdfTools());
private byte[] ExportPdf(string htmlString, PdfExportAttribute pdfExportAttribute ) { var objSetting = new ObjectSettings { HtmlContent = htmlString, PagesCount = pdfExportAttribute.IsEnablePagesCount ? true : (bool?)null, WebSettings = { DefaultEncoding = Encoding.UTF8.BodyName }, HeaderSettings= pdfExportAttribute?.HeaderSettings, FooterSettings= pdfExportAttribute?.FooterSettings, }; var htmlToPdfDocument = new HtmlToPdfDocument { GlobalSettings = { PaperSize = pdfExportAttribute?.PaperKind, Orientation = pdfExportAttribute?.Orientation, ColorMode = ColorMode.Color, DocumentTitle = pdfExportAttribute?.Name }, Objects = { objSetting } }; var result = PdfConverter.Convert(htmlToPdfDocument); return result; }
Razor 渲染PDF ActionResult核心程式碼
using JESAI.HtmlTemplate.Pdf; #if !NET45 using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; #else using System.Web.Mvc; using System.Web; #endif using RazorEngine.Compilation.ImpromptuInterface.Optimization; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using JESAI.HtmlTemplate.Pdf.Utils; namespace Microsoft.AspNetCore.Mvc { public class PDFResult<T> : ActionResult where T:class { private const string ActionNameKey = "action"; public T Value { get; private set; } public PDFResult(T value) { Value = value; } //public override async Task ExecuteResultAsync(ActionContext context) // { // var services = context.HttpContext.RequestServices; // // var executor = services.GetRequiredService<IActionResultExecutor<PDFResult>>(); // //await executor.ExecuteAsync(context, new PDFResult(this)); // } #if !NET45 private static string GetActionName(ActionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!context.RouteData.Values.TryGetValue(ActionNameKey, out var routeValue)) { return null; } var actionDescriptor = context.ActionDescriptor; string normalizedValue = null; if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out var value) && !string.IsNullOrEmpty(value)) { normalizedValue = value; } var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture); if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase)) { return normalizedValue; } return stringRouteValue; } #endif #if !NET45 public override async Task ExecuteResultAsync(ActionContext context) { var viewName = GetActionName(context); var services = context.HttpContext.RequestServices; var exportPdfByHtmlTemplate=services.GetService<IExportPdfByHtmlTemplate>(); var viewEngine=services.GetService<ICompositeViewEngine>(); var tempDataProvider = services.GetService<ITempDataProvider>(); var result = viewEngine.FindView(context, viewName, isMainPage: true); #else public override void ExecuteResult(ControllerContext context) { var viewName = context.RouteData.Values["action"].ToString(); var result = ViewEngines.Engines.FindView(context, viewName, null); IExportPdfByHtmlTemplate exportPdfByHtmlTemplate = new PdfByHtmlTemplateExporter (); #endif if (result.View == null) throw new ArgumentException($"名稱為:{viewName}的視圖不存在,請檢查!"); context.HttpContext.Response.ContentType = "application/pdf"; //context.HttpContext.Response.Headers.Add("Content-Disposition", "attachment; filename=test.pdf"); var html = ""; using (var stringWriter = new StringWriter()) { #if !NET45 var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = Value }; var viewContext = new ViewContext(context, result.View, viewDictionary, new TempDataDictionary(context.HttpContext, tempDataProvider), stringWriter, new HtmlHelperOptions()); await result.View.RenderAsync(viewContext); #else var viewDictionary = new ViewDataDictionary(new ModelStateDictionary()) { Model = Value }; var viewContext = new ViewContext(context, result.View, viewDictionary, context.Controller.TempData, stringWriter); result.View.Render(viewContext, stringWriter); result.ViewEngine.ReleaseView(context, result.View); #endif html = stringWriter.ToString(); } //var tpl=File.ReadAllText(result.View.Path); #if !NET45 byte[] buff=await exportPdfByHtmlTemplate.ExportByHtmlPersistAsync<T>(Value,html); #else byte[] buff = AsyncHelper.RunSync(() => exportPdfByHtmlTemplate.ExportByHtmlPersistAsync<T>(Value, html)); context.HttpContext.Response.BinaryWrite(buff); context.HttpContext.Response.Flush(); context.HttpContext.Response.Close(); context.HttpContext.Response.End(); #endif #if !NET45 using (MemoryStream ms = new MemoryStream(buff)) { byte[] buffer = new byte[0x1000]; while (true) { int count = ms.Read(buffer, 0, 0x1000); if (count == 0) { return; } await context.HttpContext.Response.Body.WriteAsync(buffer, 0, count); } } #endif } } }
PDF屬性設置特性核心程式碼
#if NET461 ||NET45 using TuesPechkin; using System.Drawing.Printing; using static TuesPechkin.GlobalSettings; #else using DinkToPdf; #endif using System; using System.Collections.Generic; using System.Text; namespace JESAI.HtmlTemplate.Pdf { public class PdfExportAttribute:Attribute { #if !NET461 &&!NET45 /// <summary> /// 方向 /// </summary> public Orientation Orientation { get; set; } = Orientation.Landscape; #else /// <summary> /// 方向 /// </summary> public PaperOrientation Orientation { get; set; } = PaperOrientation.Portrait; #endif /// <summary> /// 紙張類型(默認A4,必須) /// </summary> public PaperKind PaperKind { get; set; } = PaperKind.A4; /// <summary> /// 是否啟用分頁數 /// </summary> public bool IsEnablePagesCount { get; set; } /// <summary> /// 頭部設置 /// </summary> public HeaderSettings HeaderSettings { get; set; } /// <summary> /// 底部設置 /// </summary> public FooterSettings FooterSettings { get; set; } /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 伺服器是否保存一份 /// </summary> public bool IsEnableSaveFile { get; set; } = false; /// <summary> /// 保存路徑 /// </summary> public string SaveFileRootPath { get; set; } = "D:\\PdfFile"; /// <summary> /// 是否快取 /// </summary> public bool IsEnableCache { get; set; } = false; /// <summary> /// 快取有效時間 /// </summary> public TimeSpan CacheTimeSpan { get; set; } = TimeSpan.FromMinutes(30); } }
4.使用方式
建立一個BaseController,在需要使用PDF渲染的地方繼承BaseController
public abstract class BaseComtroller:Controller { public virtual PDFResult<T> PDFResult<T>(T data) where T:class { return new PDFResult<T>(data); } }
建一個model實體,可以使用PdfExport特性設置PDF的一些屬性。
[PdfExport(PaperKind = PaperKind.A4)] public class Student { public string Name { get; set; } public string Class { get; set; } public int Age { get; set; } public string Address { get; set; } public string Tel { get; set; } public string Sex { get; set; } public string Des { get; set; } }
新建一個控制器和視圖
public class HomeController : BaseComtroller { private readonly ILogger<HomeController> _logger; private readonly ICacheService _cache; public HomeController(ILogger<HomeController> logger, ICacheService cache) { _logger = logger; _cache = cache; } public IActionResult GetPDF() { var m = new Student() { Name = "111111", Address = "3333333", Age = 22, Sex = "男", Tel = "19927352816", Des = "2222222222222222222" }; return PDFResult<Student>(m); } }
@{ Layout = null; } <!DOCTYPE html> <html lang="en" xmlns="//www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title></title> </head> <body> <table border="1" style="background-color:red;width:800px;height:500px;"> <tr> <td>姓名</td> <td>@Model.Name</td> <td>性別</td> <td>@Model.Sex</td> </tr> <tr> <td>年齡</td> <td>@Model.Age</td> <td>班級</td> <td>@Model.Class</td> </tr> <tr> <td>住址</td> <td>@Model.Address</td> <td>電話</td> <td>@Model.Tel</td> </tr> <tr> <td clospan="2">住址</td> <td>@Model.Des</td> </tr> </table> </body> </html>
啟用本項目插件,strup裡面設置
public void ConfigureServices(IServiceCollection services) { services.AddHtmlTemplateExportPdf(); services.AddControllersWithViews(); }
5.運行效果:
6.項目程式碼:
程式碼託管://gitee.com/Jesai/JESAI.HtmlTemplate.Pdf
希望看到的點個星星點個贊,寫文章不容易,開源更不容易。同時希望本插件對你有所幫助。