如何将具有表单和模型值(包括图像 URL)的剃刀视图转换为 PDF?

How to convert a razor view with a form & model values (including an image URL) into a PDF?

我有一个简单的 ASP.Net 核心 MVC 应用程序,它是输入的基本形式,上面带有 HTML Canvas(用于签名)。填写表格后,我需要将其转换为 PDF 并将其附加到电子邮件中。我发现 SelectPDF 有一个支持 .Net Core 的免费社区版,我想我会试一试。

我已将我的申请放在一个地方,我可以在其中提交表格并在单独的视图中查看完成的表格(附有图像以表示用户在 canvas 中输入的内容)。电子邮件发送正常,但我终生无法从呈现的视图中生成 PDF。

在我花了几天时间尝试解决之前我不知道的是,这个带有 SelectPdf 的解决方案在新会话中对 URL 执行 GET - 这意味着我需要提供大量请求,因为我的表单有 ~20 个字段,包括超过请求大小限制的转换图像。

我尝试在不使用数据库或服务的情况下执行此操作,但图像证明这是我预期的更具挑战性的工作。

我已经在 SO 和其他站点上看到并尝试了许多建议的解决方案。它们要么已有数年(在某些情况下为十年或更长时间)陈旧和过时,要么试图使问题变得比使用其他几种工具或扩展(其中大部分已付费或已过时)所需的复杂得多。

我有什么办法可以:

任何关于如何完成我想做的事情的意见或建议都会很棒。

编辑:更多代码和我目前尝试的内容(扩展)

型号:

namespace Website.Models
{
    //[Serializable]
    public class ComputerRepairModel
    {
        [Required(AllowEmptyStrings = false)]
        [Display(Name="Customer Name")]
        public string CustomerName { get; set; }

        [Display(Name = "Email")]
        public string CustomerEmail { get; set; }

        [Display(Name = "Home")]
        public string ContactHomeNumber { get; set; }

        [Display(Name = "Work")]
        public string ContactWorkNumber { get; set; }

        [Required(AllowEmptyStrings = false)]
        [Display(Name = "Cell")]
        public string ContactCellNumber { get; set; }

        [Display(Name = "Signed")]
        public string Signature { get; set; }
        ....
    }

控制器:

namespace Website.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public IActionResult RepairAgreement()
        {                          
            ComputerRepairModel model = new ComputerRepairModel();
            return View(model);
        }

        [HttpPost]
        public IActionResult RepairAgreement(ComputerRepairModel Model)
        {
            if (!ModelState.IsValid)
            {
                Model.Signature = "";
                return View("RepairAgreement", Model);
            }

            return View(Model);
        }

        [HttpGet]
        public IActionResult DisplayRepairAgreement()
        {
            //ComputerRepairModel model = (ComputerRepairModel)TempData["model"];
            return View();
        }

        [HttpPost]
        public IActionResult SubmitRepairAgreement(ComputerRepairModel Model)
        {
            if (!ModelState.IsValid)
            {
                Model.Signature = null;
                return View("RepairAgreement", Model);
            }


            //TempData["model"] = Model;
            return RedirectToAction("DisplayRepairAgreement");
        }

查看:

@model ComputerRepairModel

@section Scripts{
    <script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>

    <script>
        $(function () {
            var canvas = document.querySelector('#signatureCanvas');
            var pad = new SignaturePad(canvas);
        });
    </script>

    <script>
        $("#submit").click(function () {
            //alert("button"); // Remove this line if it worked
            var dataURL = document.getElementById('signatureCanvas').toDataURL();
            document.getElementById('signature').value = dataURL;
            $("#submitbutton").hide();
        });
    </script>
}

<head>

</head>

<body>
    <h2 style="margin-top:20px;">Computer Repair Form</h2>

    <hr />

    <form method="post" asp-action="SubmitRepairAgreement">
        <div class="form-group">
            <div class="form-row">
                <div class="form-group col-sm-3">
                    <label asp-for="CustomerName"></label>
                    <input type="text" asp-for="CustomerName" class="form-control" />
                    <span asp-validation-for="CustomerName" class="text-danger"></span>
                </div>

                <div class="form-group col-sm-3">
                    <label asp-for="CustomerEmail"></label>
                    <input type="text" asp-for="CustomerEmail" class="form-control" placeholder="example@domain.com" />
                    <span asp-validation-for="CustomerEmail" class="text-danger"></span>
                </div>
            </div>
        </div>

        <div class="form-group">
            <label><b>Contact Number(s)</b></label>
            <div class="form-row">
                <div class="form-group col-sm-3">
                    <label asp-for="ContactHomeNumber"></label>
                    @*<input type="text" asp-for="ContactHomeNumber" class="phone form-control" maxlength="14" />*@
                    <input id="homePhone" class="form-control" type="text" asp-for="ContactHomeNumber" />
                    <span asp-validation-for="ContactHomeNumber" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-3">
                    <label asp-for="ContactWorkNumber"></label>
                    <input id="workPhone" class="form-control" type="text" asp-for="ContactWorkNumber" />
                    <span asp-validation-for="ContactWorkNumber" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-3">
                    <label asp-for="ContactCellNumber"></label>
                    <input id="cellPhone" class="form-control" type="text" asp-for="ContactCellNumber" />
                    <span asp-validation-for="ContactCellNumber" class="text-danger"></span>
                </div>
            </div>
        </div>

        <div class="form-group">
            <label><b>Billing Address</b></label>
            <div class="form-row">
                <div class="form-group col-sm-5">
                    <label asp-for="BillingStreetAddress"></label>
                    <input class="form-control" type="text" asp-for="BillingStreetAddress" />
                    <span asp-validation-for="BillingStreetAddress" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-2">
                    <label asp-for="BillingCity"></label>
                    <input class="form-control" type="text" asp-for="BillingCity" />
                    <span asp-validation-for="BillingCity" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-2">
                    <label asp-for="BillingState"></label>
                    <input class="form-control" type="text" asp-for="BillingState" />
                    <span asp-validation-for="BillingState" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-2">
                    <label asp-for="BillingZip"></label>
                    <input class="form-control" type="text" asp-for="BillingZip" />
                    <span asp-validation-for="BillingZip" class="text-danger"></span>
                </div>
            </div>
        </div>

        <div class="form-group">
            <label><b>Computer Access</b></label>
            <div class="form-row">
                <div class="form-group col-sm-3">
                    <label asp-for="CustomerComputerUsername"></label>
                    <input class="form-control" type="text" asp-for="CustomerComputerUsername" />
                    <span asp-validation-for="CustomerComputerUsername" class="text-danger"></span>
                </div>
                <div class="form-group col-sm-3">
                    <label asp-for="CustomerComputerPassword"></label>
                    <input class="form-control" type="text" asp-for="CustomerComputerPassword" />
                    <span asp-validation-for="CustomerComputerPassword" class="text-danger"></span>
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="form-row">
                <div class="form-group col-sm-12">
                    <label asp-for="DescriptionOfProblem"></label>
                    <textarea class="form-control" asp-for="DescriptionOfProblem"></textarea>
                    <span asp-validation-for="DescriptionOfProblem" class="text-danger"></span>
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="form-row">
                <div class="form-group col-sm-12">
                    <label asp-for="ItemsReceived"></label>
                    <textarea class="form-control" asp-for="ItemsReceived"></textarea>
                    <span asp-validation-for="ItemsReceived" class="text-danger"></span>
                </div>
            </div>
        </div>

        <hr />    

        <div class="form-group">
            <div class="form-row">
                <div class="form-group col-sm-12">
                    <label asp-for="Comments"></label>
                    <textarea class="form-control" asp-for="Comments"></textarea>
                    <span asp-validation-for="Comments" class="text-danger"></span>
                </div>
            </div>
        </div>
        <div>
            I hereby agree to the above terms and authorize AMTI to perform services/repairs as stated in the service order.<br />
            I also agree to the terms and conditions within this Agreement.
        </div>

        <div class="form-group" style="margin-top:20px;">
            <div class="form-row justify-content-between">
                <div class="col-sm-6">
                    <label asp-for="Signature"></label>
                    @if (String.IsNullOrEmpty(Model.Signature))
                    {
                        <input type="hidden" id="signature" asp-for="Signature" />
                        <canvas width="500" height="100" id="signatureCanvas" style="border:1px solid black"></canvas>
                    }
                    else
                    {
                        <img src="@Url.Content(Model.Signature)" alt="Image" />
                    }
                </div>
                <div class="form-group col-sm-3">
                    <label asp-for="DateSigned"></label>
                    <input class="form-control" type="date" asp-for="DateSigned"/>
                </div>
            </div>
        </div>

        <div>
            <hr />
            <center><b>For Office Use Only</b></center>
            <div class="form-group">
                <div class="form-row">
                    <div class="form-group col-sm-4">
                        <label asp-for="ComputerMfg"></label>
                        <input class="form-control" readonly asp-for="ComputerMfg" />
                    </div>
                    <div class="form-group col-sm-4">
                        <label asp-for="ComputerModel"></label>
                        <input class="form-control" readonly asp-for="ComputerModel" />
                    </div>
                    <div class="form-group col-sm-4">
                        <label asp-for="ComputerOS"></label>
                        <input class="form-control" readonly asp-for="ComputerOS" />
                    </div>
                </div>
            </div>
        </div>

        <div id="submitbutton">
            <input id="submit" class="form-control button" style="background-color: #4CAF50; color:white;" type="submit"/>
        </div>
    </form>

</body>

上面显示的基本上是我的模型、控制器和视图的样子。

我的模型和控制器中的注释代码代表了我最近尝试解决 this answer 中的问题。显然,如果我想尝试并使此方法起作用,我还有一些工作要做,因为尽管将我的模型标记为可序列化,但我收到以下错误。

我尝试这样做是因为如果我只是做一个普通的 RedirectToAction("DisplayRepairAgreement", Model); 请求会太长(因为我通过Javascript) 如图所示。

我尝试的另一件事是使用相同的视图并使 POST 操作成为用于发送到 PDF 转换的操作(这就是为什么我在签名输入附近有 if 条件的原因底部),但这只会在我将 URL 传递给该方法时获取 GET,并且将以 PDF 形式提供表单,但没有填写任何值。

以下是我最近一次尝试之前在控制器中执行的更多操作(如上所示):

    [HttpPost]
    public IActionResult RepairAgreement(ComputerRepairModel Model)
    {
        if (!ModelState.IsValid)
        {
            Model.Signature = "";
            return View("RepairAgreement", Model);
        }
        string url = Url.Action(nameof(DisplayRepairAgreement), 
            new { Model.CustomerName, Model.CustomerEmail, Model.ContactHomeNumber, Model.ContactWorkNumber, Model.ContactCellNumber,
                Model.BillingStreetAddress, Model.BillingCity, Model.BillingState, Model.BillingZip, Model.CustomerComputerUsername, Model.CustomerComputerPassword, Model.DescriptionOfProblem,
                Model.ItemsReceived, Model.Comments, Model.Signature, Model.DateSigned});

        // instantiate a html to pdf converter object
        HtmlToPdf converter = new HtmlToPdf();

        // create a new pdf document converting an url
        PdfDocument doc = converter.ConvertUrl(url);

        // save pdf document
        doc.Save("Sample.pdf");

        // close pdf document
        doc.Close();
        return View(Model);
    }

无奈之下,我还尝试直接在我的模型中硬编码 HTML 我的视图,因为 SelectPDF 对象的方法之一可以接受 HTML 字符串而不是 URL.我填写了表格并被带到了显示视图,在那里我使用检查器只抓取 HTML 的整个 blob 并将其粘贴进去。它几乎成功了。本质上,在我的操作中,我只是调用以下方法,并且传入的 Html 存储在模型中,如本段前面所述。

        public PdfDocument CreatePdfFromHTML(string Html)
        {
            HtmlToPdf converter = new HtmlToPdf();
            PdfDocument pdfDoc = converter.ConvertHtmlString(Html);

            return pdfDoc;
        }

这是表单在浏览器中的样子,我也希望 PDF 看起来像这样

这是我尝试使用 stringbuilder 方法并根据 Chrome.

中的检查器编写自己的 HTML 字符串时的样子

好吧,现在更有意义了。似乎您没有正确呈现您的视图。我之前曾尝试过类似的方法,您可以使用此方法将视图呈现为字符串:

public class MyController : Controller
{
    private readonly ICompositeViewEngine _viewEngine;

    public MyController(ICompositeViewEngine viewEngine)
    {
        _viewEngine = viewEngine;
    }

    [HttpPost]
    public async Task<IActionResult> RepairAgreement(ComputerRepairModel Model)
    {
        if (!ModelState.IsValid)
        {
            Model.Signature = "";
            return View("RepairAgreement", Model);
        }

        string url = await RenderPartialViewToString("DisplayRepairAgreement", new { Model.CustomerName, Model.CustomerEmail, Model.ContactHomeNumber, Model.ContactWorkNumber, Model.ContactCellNumber,
            Model.BillingStreetAddress, Model.BillingCity, Model.BillingState, Model.BillingZip, Model.CustomerComputerUsername, Model.CustomerComputerPassword, Model.DescriptionOfProblem,
            Model.ItemsReceived, Model.Comments, Model.Signature, Model.DateSigned});

        // instantiate a html to pdf converter object
        HtmlToPdf converter = new HtmlToPdf();

        // create a new pdf document converting an url
        PdfDocument doc = converter.ConvertHtmlString(url);

        // save pdf document
        doc.Save("Sample.pdf");

        // close pdf document
        doc.Close();
        return View(Model);
    }

    [HttpPost]
    public IActionResult DisplayRepairAgreement()
    {
        return Ok();
    }

    private async Task<string> RenderPartialViewToString(string viewName, object model)
    {
        if (string.IsNullOrEmpty(viewName))
            viewName = ControllerContext.ActionDescriptor.ActionName;

        ViewData.Model = model;

        using (var writer = new StringWriter())
        {
            ViewEngineResult viewResult = 
                _viewEngine.FindView(ControllerContext, viewName, false);

            ViewContext viewContext = new ViewContext(
                ControllerContext, 
                viewResult.View, 
                ViewData, 
                TempData, 
                writer, 
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);

            return writer.GetStringBuilder().ToString();
        }
    }
}

给你。我不得不填补一些空白,但我希望它有意义。我添加了一些能够呈现 Razor 视图的代码。这样,它应该使用 Razor 引擎以与浏览器完全相同的方式呈现它。上述的另一个好处是您没有发出任何其他 http 请求。您只是直接使用渲染引擎并在控制器中生成所需的 http 页面。

代码取自这个答案

我自己从未尝试过 SelectPdf,但如果它仍然没有任何样式,您可能需要研究某种渲染引擎,例如 Chromium。我希望这能让你更接近实现你想要的目标。