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="http://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.项目代码:
代码托管:https://gitee.com/Jesai/JESAI.HtmlTemplate.Pdf
希望看到的点个星星点个赞,写文章不容易,开源更不容易。同时希望本插件对你有所帮助。
补充:后面陆陆续续有人私下问我有没有电子签章的源码开源。在这里我只能告诉你们,电子签章这个东西是非常复杂的一个东西。暂时没有开源。我们也是用了第三方的服务。这里仅仅给大家看一下效果已经如果接入使用。项目里面有一个PDFCallResult 的ActionResult。