I have a simple ASP.Net Core MVC application that is a basic form of inputs with an HTML Canvas on it (for a signature). When the form is filled out I need to convert it to a PDF and attach it to an email. I found SelectPDF which has a free, community edition that supports .Net Core and figured I'd give it a go.
I've got my application in a spot where I can submit the form and see the completed form in a separate view (complete with an image to represent that what the user entered into the canvas). Emails are sending just fine but I can't for the life of me generate a PDF out of my rendered view.
What I didn't know until I spent several days on attempting to resolve was that this solution with SelectPdf performs a GET on the URL in a new session - meaning I'd need to provide a massive request since my form has ~20 fields including a converted image that more than exceeds the request size limit.
I'm trying to do this without the use of a database or service, but the image is proving this to be a much more challenging endeavor that I expected.
I've seen and tried many of the proposed solutions on SO and other sites. They are either several years (in some cases a decade or more) old and outdated or try to make the problem much more complicated than it needs to be using several other tools or extensions (most of which are paid or out of date).
Is there a way for me to:
Any advice or suggestions on how to accomplish what I'm trying to do would be great.
Edit: More code & what I've tried so far (Extended)
Model:
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; }
....
}
Controller:
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");
}
View:
@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>
Shown above are essentially what my model, controller & view look like.
The commented code in my model & controller represents my most recent attempt at solving the problem from this answer. Clearly I've still got some work to do if I want to try and get this method to work since despite marking my model as Serializable, I get the following error.
I attempted this because if I just did a normal RedirectToAction("DisplayRepairAgreement", Model);
the request would be too long (since I convert an HTML canvas to a URL string via Javascript) as shown.
Another thing I tried was just using the same view and having the POST action be the one that's used to send to the PDF conversion (which is why I have the if condition near the signature input at the bottom) but this would only ever grab the GET
when I passed the URL to the method and would have the form in a PDF, but with no values filled in.
Below are more actions I had in my controller before my latest attempt (shown above):
[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);
}
In my desperation I also tried to hardcode the HTML my view directly in my model since one of the methods of the SelectPDF object can take in an HTML string rather than a URL. I filled out the form and was taken to the Display view, where I used the inspector to just grab the entire blob of HTML and pasted it in. It almost worked. Essentially in my action I'd just call the following method and the Html being passed in was stored in the model as explained earlier in this paragraph.
public PdfDocument CreatePdfFromHTML(string Html)
{
HtmlToPdf converter = new HtmlToPdf();
PdfDocument pdfDoc = converter.ConvertHtmlString(Html);
return pdfDoc;
}
Here is how the form looks in the browser, and how I'd like the PDF to look as well
and here is how it looks when I tried the stringbuilder approach and wrote my own HTML string based off of the inspector in Chrome.
Alright it makes a bit more sense now. It seems like you are not getting your view rendered correctly. I have had tried something similar earlier and you can use this method to render a view to a string:
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();
}
}
}
Here you go. I had to fill in some blanks, but I hope it makes sense. I added some code that is able to render a Razor view. This way it should use the Razor engine to render it the exact same way as your browser did. Another benefit of the above is that you are not making any other http requests. You are just using the rendering engine directly and generating the desired http page in the controller.
The code has been taken from this answer stackoverflow answer
I have never tried SelectPdf myself, but if it still comes out without any styling, you might have to look into some kind of rendering engine for example Chromium. I hope this brings you a step closer to achieving what you want.