【问题标题】:How to convert a razor view with a form & model values (including an image URL) into a PDF?如何将带有表单和模型值(包括图像 URL)的剃刀视图转换为 PDF?
【发布时间】:2019-07-09 17:35:38
【问题描述】:

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

我将我的应用程序放在一个可以提交表单并在单独的视图中查看已完成的表单的位置(完整的图像代表用户在画布中输入的内容)。电子邮件发送得很好,但我终生无法从渲染视图中生成 PDF。

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

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

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

我有什么办法:

  • 以某种方式修剪图像 URL?或者
  • 允许 GET 请求接受 这么长的请求?
  • 以某种方式将填充了值的渲染视图转换为 HTML 字符串并发送它而不是 URL?
  • 使用 Tempdata 在“显示”视图中重建我的模型以绕过请求长度限制?

任何关于如何完成我正在尝试做的事情的建议或建议都会很棒。

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

型号:

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 将 HTML 画布转换为 URL 字符串),如图所示。

我尝试的另一件事是使用相同的视图并让 POST 操作成为用于发送到 PDF 转换的操作(这就是为什么我在底部的签名输入附近有 if 条件)但这只会当我将 URL 传递给方法并将表单保存在 PDF 中,但没有填写任何值时,请务必抓住 GET

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

    [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 块并将其粘贴进去。它几乎可以工作了。本质上,在我的操作中,我只需调用以下方法,传入的 Html 就存储在模型中,如本段前面所述。

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

            return pdfDoc;
        }

这是表单在浏览器中的外观,以及我希望 PDF 的外观

这是我尝试使用 stringbuilder 方法并根据 Chrome 中的检查器编写自己的 HTML 字符串时的样子。

【问题讨论】:

  • 你能试着分享一些你到目前为止写的代码吗?这是一个非常开放的问题,您在这里提出的问题很难给出反馈,没有看到有问题的代码。
  • 我将尝试将除签名之外的所有内容存储在模型中,并将签名存储在 TempData 中,并尝试在页面加载时仅检索该签名,这样我的请求 URL 的长度是合理的,我之后可以获取图片网址。
  • @KristianBarrett 我添加了更多信息。希望它有所帮助。

标签: asp.net-core asp.net-core-mvc pdf-generation selectpdf


【解决方案1】:

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

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 页面。

代码取自这个答案stackoverflow answer

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

【讨论】:

  • @Mkalafut 好吧,我也有类似的想法。我使用了两个不同的库,它们需要花钱 AsposePDF 和 AbcPDF。这两个库都使用了上面的代码。所以我希望它也适用于你。它可能需要一些调整。告诉我进展如何。
  • 看来我也可以使用 ConvertHtmlString 方法的重载记录 here 来提供基本 URL。我会调查的。
  • @Mkalafut 是的,这有时是个问题。您可以直接在 SelectPDF 中指定样式表。或者您必须在渲染之前将您的 CSS(或将其注入)到 html 页面中。这样它就不会从外部源加载,而是 html 文档的一部分。
  • &lt;head&gt; &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" /&gt; &lt;link rel="stylesheet" href="~/css/site.css" /&gt; &lt;/head&gt; 添加到视图的标题中,并且可以正常工作。再次感谢您的帮助!
  • @Mkalafut 太好了。很高兴能帮到你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-11
  • 2019-03-20
  • 2018-09-05
  • 1970-01-01
  • 2019-12-08
相关资源
最近更新 更多