Show loading screen before dynamically created PDF - c#

I have a view that is, instead of returning a View(), is returning a dynamically created PDF and then showing the PDF in a new tab. I'm not saving the PDF anywhere, or storing it anywhere. What I would like to do is have a loading screen show up while the PDF is being created. Can this be done?
public ActionResult SolicitorActionReport_Load(SolicitorActionParamsViewModel viewModel) {
var cultivationModel = new CultivationModel(viewModel, ConstituentRepository, CampaignRepository);
var cultivationData = cultivationModel.GetCultivationActivityData();
var reportParamModel = new List<ReportParamModel>
{new ReportParamModel {AgencyName = SelectedUserAgency.AgencyName, StartDate = viewModel.StartDate, EndDate = viewModel.EndDate}};
var reportToRun = "ActionDateCultivationReport";
if (viewModel.SortActionBy == SolicitorActionReportSortType.Constituent) {
reportToRun = "ConstituentCultivationReport";
} else if (viewModel.SortActionBy == SolicitorActionReportSortType.Solicitor) {
reportToRun = "SolicitorCultivationReport";
}
return FileContentPdf("Constituent", reportToRun, cultivationData, reportParamModel, new List<FundraisingAppealMassSummary>(), new List<FundraisingAppealPortfolioSummary>());
}
public FileContentResult FileContentPdf(string folder, string reportName, object dataSet,object reportParamModel,object appealMassDataSet, object appealPortfolioDataSet) {
var localReport = new LocalReport();
localReport.ReportPath = Server.MapPath("~/bin/Reports/" + folder + "/rpt" + reportName + ".rdlc");
var reportDataSource = new ReportDataSource(reportName + "DataSet", dataSet);
var reportParamsDataSource = new ReportDataSource("ReportParamModelDataSet", reportParamModel);
var reportParamsDataSourceMass = new ReportDataSource("FundraisingAppealMassSummaryDataSet", appealMassDataSet);
var reportParamsDataSourcePortfolio = new ReportDataSource("FundraisingAppealPortfolioSummaryDataSet", appealPortfolioDataSet);
#region Setting ReportViewControl
localReport.DataSources.Add(reportDataSource);
localReport.DataSources.Add(reportParamsDataSource);
localReport.DataSources.Add(reportParamsDataSourceMass);
localReport.DataSources.Add(reportParamsDataSourcePortfolio);
localReport.SubreportProcessing += (s, e) => { e.DataSources.Add(reportDataSource); };
string reportType = "pdf";
string mimeType;
string encoding;
string fileNameExtension;
//The DeviceInfo settings should be changed based on the reportType
//http://msdn2.microsoft.com/en-us/library/ms155397.aspx
string deviceInfo = "<DeviceInfo><OutputFormat>PDF</OutputFormat></DeviceInfo>";
Warning[] warnings;
string[] streams;
byte[] renderedBytes;
//Render the report
renderedBytes = localReport.Render(reportType, deviceInfo, out mimeType, out encoding, out fileNameExtension, out streams, out warnings);
#endregion
return File(renderedBytes, mimeType);
}

I'm not saving the PDF anywhere, or storing it anywhere. What I would like to do is have a loading screen show up while the PDF is being created. Can this be done?
Short Answer
No, not in a new tab.
The main problem with what you're trying to do is the lack of power you have when it comes to controlling the browser. Specifically, when you tell an anchor to open its hyperlink in a new tab (ie target="_blank"). There are hacky ways around this that generally are just going to frustrate your user because you're changing behavior that they might be dependent/relying on.
Workaround
You can get very close to your desired outcome by using this jQuery File Download plugin (view a demo). Basically, it manipulates an iframe to queue a download. This makes it possible to show a loading div while also keeping the user on the active page (not directing them to another tab). Then, the user can click the downloaded PDF which will most-likely open in a new tab (view compatible browsers here).
If you decide to use this plugin, here are the steps to applying it:
Download the plugin js source and include it in your Scripts.
Include the FileDownloadAttribute class provided in the plugin MVC Demo:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class FileDownloadAttribute: ActionFilterAttribute
{
public FileDownloadAttribute(string cookieName = "fileDownload", string cookiePath = "/")
{
CookieName = cookieName;
CookiePath = cookiePath;
}
public string CookieName { get; set; }
public string CookiePath { get; set; }
/// <summary>
/// If the current response is a FileResult (an MVC base class for files) then write a
/// cookie to inform jquery.fileDownload that a successful file download has occured
/// </summary>
/// <param name="filterContext"></param>
private void CheckAndHandleFileResult(ActionExecutedContext filterContext)
{
var httpContext = filterContext.HttpContext;
var response = httpContext.Response;
if (filterContext.Result is FileResult)
//jquery.fileDownload uses this cookie to determine that a file download has completed successfully
response.AppendCookie(new HttpCookie(CookieName, "true") { Path = CookiePath });
else
//ensure that the cookie is removed in case someone did a file download without using jquery.fileDownload
if (httpContext.Request.Cookies[CookieName] != null)
{
response.AppendCookie(new HttpCookie(CookieName, "true") { Expires = DateTime.Now.AddYears(-1), Path = CookiePath });
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
CheckAndHandleFileResult(filterContext);
base.OnActionExecuted(filterContext);
}
}
github source
Apply the FileDownload attribute to your ActionResult method:
[FileDownload]
public ActionResult SolicitorActionReport_Load(SolicitorActionParamsViewModel viewModel) {
...
return FileContentPdf("Constituent", reportToRun, cultivationData, reportParamModel, new List<FundraisingAppealMassSummary>(), new List<FundraisingAppealPortfolioSummary>());
}
Include the necessary markup in the View to which you'll be linking to the report:
<a class="report-download" href="/Route/To/SolicitorActionReport">Download PDF</a>
Attach an event handler to the report-download anchor:
$(document).on("click", "a.report-download", function () {
$.fileDownload($(this).prop('href'), {
preparingMessageHtml: "We are preparing your report, please wait...",
failMessageHtml: "There was a problem generating your report, please try again."
});
return false; //this is critical to stop the click event which will trigger a normal file download!
});
You can view working demos at http://jqueryfiledownload.apphb.com/. There is also a demo that uses pre-styled jQuery UI modals to "prettify" the user experience.
You can also download the demo ASP.NET MVC solution from johnculviner / jquery.fileDownload github to see all of this working.

I think you have two choices:
Redirect to a "loading" page with fancy GIF spinners, then direct the request to the PDF (this would work if the PDF take a little server time to generate - the visitor would be looking at a loading page while waiting for next page to load)
or
Use an iFrame: load a page that has an iframe. This page can overlay a spinning GIF and loading message while the iFrame loads the PDF itself. Note: you could make the iframe 100% width and height

Related

Xamarin iOS post image to Instagram

Goal: Using an iOS native method, push a user made picture onto their Instagram feed in C#.
public bool ShareImage(byte[] imageByte)
{
bool result = false;
//string fileName = "xxx.png";
//string path = Path.Combine(FileSystem.CacheDirectory, fileName);
//File.WriteAllBytes(path, imageByte);
NSData data = NSData.FromArray(imageByte);
UIImage image = UIImage.LoadFromData(data);
//NSUrl url = NSUrl.FromString($"instagram://library?AssetPath={path}"); // Only opens
NSUrl url = NSUrl.FromString($"instagram://library?LocalIdentifier={1}");
if (UIApplication.SharedApplication.CanOpenUrl(url))
{
UIApplication.SharedApplication.OpenUrl(url);
}
return result;
}
As far as I'm aware the way I have to do this is to save my image to the device and get a Local Identifier for this.
I have constructed this from the snippets of objective-c code I have seen. Sharing photos only works with the native app installed, as I have learnt from my trials to get the facebook module working.
Edit: Using PHAssetChangeRequest from the iOS Photos namespace does not work.
A collegue has pointed out to me about the possibility of saving then using a photo picker to get the PHAsset for the Local Identifier. But this is an extra step I do not want the users of the App to go through. Better to just remove Instagram support as I just can go through the generic share method as shown below. The disadvantage of this method is that the user has then to pick the medium to share over.
public async void ShareImage(byte[] imageByte)
{
string fileName = "xxx.png";
string path = Path.Combine(FileSystem.CacheDirectory, fileName);
File.WriteAllBytes(path, imageByte);
await Share.RequestAsync(
new ShareFileRequest()
{
File = new ShareFile(path),
Title = "xxx"
}
);
}
Edit 2
Tried a different way using UIDocumentInteractionController but it is showing nothing and not posting, it is not throwing any exceptions to give me any clues as to what I'm doing wrong.
public bool ShareImage(byte[] imageByte)
{
bool result = false;
string fileName = "xxx.igo";
string path = Path.Combine(FileSystem.CacheDirectory, fileName);
File.WriteAllBytes(path, imageByte);
//string caption = "xxx";
string uti = "com.instagram.exclusivegram";
UIImageView view = new UIImageView();
//UIDocumentInteractionController controller = UIDocumentInteractionController.FromUrl(NSUrl.FromString(path));
UIDocumentInteractionController controller = new UIDocumentInteractionController
{
//controller.Url = NSUrl.FromString(path);
Url = NSUrl.FromFilename(path),
Uti = uti
};
//CoreGraphics.CGRect viewDimensions = new CoreGraphics.CGRect(0, 0, 200, 100);
_ = controller.PresentOpenInMenu(CoreGraphics.CGRect.Empty, view, true);
//_ = controller.PresentOpenInMenu(viewDimensions, view, true);
return result;
}
Edit 3
Using
UIView view = new UIImageView();
if (UIApplication.SharedApplication.KeyWindow.RootViewController is UIViewController uiController && uiController.View != null)
{
view = uiController.View;
}
I was able to get the possible share options to show. I was logged in to Instagram using the native App, but the post did not show.
Change
Url = NSUrl.FromFilename(path)
to
Url = new NSUrl(path,false)
The second way is pointing to the correct file path not the file name .
Change
controller.PresentOpenInMenu(CoreGraphics.CGRect.Empty, view, true);
to
controller.PresentOptionsMenu(UIScreen.MainScreen.Bounds, (UIApplication.SharedApplication.Delegate as AppDelegate).Window, true);
Show UIDocumentInteractionController inside current Window and set the proper frame.

.NET MVC: perform heavy task on server then email user when complete so that user can leave site instead of wait?

The scenario: A button allows a user to merge a large number of PDF documents to download as a single PDF. Currently the action of getting all the PDF's and merging can take upwards of a minute or more while the user has to wait for the download to start.
My goal is to allow the user to leave if they want. The solution I thought of would be to merge the documents in the background on the server and then email a link to the user when it's completed but I'm open to other solutions.
The thing that I don't understand is how to perform the merging asynchronously in the background. Using .NET, MVC 5, DevExpress.
The code looks kinda like:
$.ajax({
type: "POST",
url: '#Url.Action("ExportMergedDocuments_PersonnelId", "Personnel", new { personnelId = Model.Id })',
}).done(function(data) {
window.location.href = '#Url.RouteUrl(new { Controller = "Personnel", Action = "Download"})/?file=' + data.fileName; }
});
[HttpPost]
public JsonResult ExportMergedDocuments_PersonnelId(int PersonnelId)
{
var allDocuments = new DataSet();
allDocuments.Merge(GetDocuments((int)PersonnelId, ".....1").Tables[0]);
allDocuments.Merge(GetDocuments((int)PersonnelId, ".....2").Tables[0]);
string fileName = $"merged__{DateTime.Now.ToString("yyyyMMddHHmm")}.pdf";
if (MergePdfSet(fileName, allDocuments))
return Json(new { fileName });
// else error msg
}
Download the file:
[HttpGet]
public ActionResult Download(string file)
{
return File(..fullpath.., "application/pdf", file);
}
Merging Pdfs:
public bool MergePdfSet(string fileName, DataSet allDocuments)
{
bool merged = false;
string fullPath = Path.Combine(Server.MapPath("~/App_Data/temp/"), fileName);
using (var pdfDocumentProcessor = new PdfDocumentProcessor())
{
pdfDocumentProcessor.CreateEmptyDocument(fullPath);
foreach (DataRow row in allDocuments.Tables[0].Rows)
{
var documentId = (int)row["DocumentID"];
var fetchedDocument = GetFile(documentId);
pdfDocumentProcessor.AppendDocument(fetchedDocument);
merged = true;
}
}
return merged;
}
Two option comes to mind:
Create a new thread and run the code there but don't await it.
Use Hangfire (https://www.hangfire.io/), you can easy enqueue a job.

Returning file from ASP.NET Controller [duplicate]

I'm encountering a problem sending files stored in a database back to the user in ASP.NET MVC. What I want is a view listing two links, one to view the file and let the mimetype sent to the browser determine how it should be handled, and the other to force a download.
If I choose to view a file called SomeRandomFile.bak and the browser doesn't have an associated program to open files of this type, then I have no problem with it defaulting to the download behavior. However, if I choose to view a file called SomeRandomFile.pdf or SomeRandomFile.jpg I want the file to simply open. But I also want to keep a download link off to the side so that I can force a download prompt regardless of the file type. Does this make sense?
I have tried FileStreamResult and it works for most files, its constructor doesn't accept a filename by default, so unknown files are assigned a file name based on the URL (which does not know the extension to give based on content type). If I force the file name by specifying it, I lose the ability for the browser to open the file directly and I get a download prompt. Has anyone else encountered this?
These are the examples of what I've tried so far.
//Gives me a download prompt.
return File(document.Data, document.ContentType, document.Name);
//Opens if it is a known extension type, downloads otherwise (download has bogus name and missing extension)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType);
//Gives me a download prompt (lose the ability to open by default if known type)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType) {FileDownloadName = document.Name};
Any suggestions?
UPDATE:
This questions seems to strike a chord with a lot of people, so I thought I'd post an update. The warning on the accepted answer below that was added by Oskar regarding international characters is completely valid, and I've hit it a few times due to using the ContentDisposition class. I've since updated my implementation to fix this. While the code below is from my most recent incarnation of this problem in an ASP.NET Core (Full Framework) app, it should work with minimal changes in an older MVC application as well since I'm using the System.Net.Http.Headers.ContentDispositionHeaderValue class.
using System.Net.Http.Headers;
public IActionResult Download()
{
Document document = ... //Obtain document from database context
//"attachment" means always prompt the user to download
//"inline" means let the browser try and handle it
var cd = new ContentDispositionHeaderValue("attachment")
{
FileNameStar = document.FileName
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
return File(document.Data, document.ContentType);
}
// an entity class for the document in my database
public class Document
{
public string FileName { get; set; }
public string ContentType { get; set; }
public byte[] Data { get; set; }
//Other properties left out for brevity
}
public ActionResult Download()
{
var document = ...
var cd = new System.Net.Mime.ContentDisposition
{
// for example foo.bak
FileName = document.FileName,
// always prompt the user for downloading, set to true if you want
// the browser to try to show the file inline
Inline = false,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(document.Data, document.ContentType);
}
NOTE: This example code above fails to properly account for international characters in the filename. See RFC6266 for the relevant standardization. I believe recent versions of ASP.Net MVC's File() method and the ContentDispositionHeaderValue class properly accounts for this. - Oskar 2016-02-25
I had trouble with the accepted answer due to no type hinting on the "document" variable: var document = ... So I'm posting what worked for me as an alternative in case anybody else is having trouble.
public ActionResult DownloadFile()
{
string filename = "File.pdf";
string filepath = AppDomain.CurrentDomain.BaseDirectory + "/Path/To/File/" + filename;
byte[] filedata = System.IO.File.ReadAllBytes(filepath);
string contentType = MimeMapping.GetMimeMapping(filepath);
var cd = new System.Net.Mime.ContentDisposition
{
FileName = filename,
Inline = true,
};
Response.AppendHeader("Content-Disposition", cd.ToString());
return File(filedata, contentType);
}
To view file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain);
To download file (txt for example):
return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain, "TextFile.txt");
note: to download file we should pass fileDownloadName argument
Darin Dimitrov's answer is correct. Just an addition:
Response.AppendHeader("Content-Disposition", cd.ToString()); may cause the browser to fail rendering the file if your response already contains a "Content-Disposition" header. In that case, you may want to use:
Response.Headers.Add("Content-Disposition", cd.ToString());
I believe this answer is cleaner, (based on
https://stackoverflow.com/a/3007668/550975)
public ActionResult GetAttachment(long id)
{
FileAttachment attachment;
using (var db = new TheContext())
{
attachment = db.FileAttachments.FirstOrDefault(x => x.Id == id);
}
return File(attachment.FileData, "application/force-download", Path.GetFileName(attachment.FileName));
}
Below code worked for me for getting a pdf file from an API service and response it out to the browser - hope it helps;
public async Task<FileResult> PrintPdfStatements(string fileName)
{
var fileContent = await GetFileStreamAsync(fileName);
var fileContentBytes = ((MemoryStream)fileContent).ToArray();
return File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf);
}
FileVirtualPath --> Research\Global Office Review.pdf
public virtual ActionResult GetFile()
{
return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Action method needs to return FileResult with either a stream, byte[], or virtual path of the file. You will also need to know the content-type of the file being downloaded. Here is a sample (quick/dirty) utility method. Sample video link
How to download files using asp.net core
[Route("api/[controller]")]
public class DownloadController : Controller
{
[HttpGet]
public async Task<IActionResult> Download()
{
var path = #"C:\Vetrivel\winforms.png";
var memory = new MemoryStream();
using (var stream = new FileStream(path, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var ext = Path.GetExtension(path).ToLowerInvariant();
return File(memory, GetMimeTypes()[ext], Path.GetFileName(path));
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".txt", "text/plain"},
{".pdf", "application/pdf"},
{".doc", "application/vnd.ms-word"},
{".docx", "application/vnd.ms-word"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
...
};
}
}
If, like me, you've come to this topic via Razor components as you're learning Blazor, then you'll find you need to think a little more outside of the box to solve this problem. It's a bit of a minefield if (also like me) Blazor is your first forray into the MVC-type world, as the documentation isn't as helpful for such 'menial' tasks.
So, at the time of writing, you cannot achieve this using vanilla Blazor/Razor without embedding an MVC controller to handle the file download part an example of which is as below:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
[Route("api/[controller]")]
[ApiController]
public class FileHandlingController : ControllerBase
{
[HttpGet]
public FileContentResult Download(int attachmentId)
{
TaskAttachment taskFile = null;
if (attachmentId > 0)
{
// taskFile = <your code to get the file>
// which assumes it's an object with relevant properties as required below
if (taskFile != null)
{
var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileNameStar = taskFile.Filename
};
Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
}
}
return new FileContentResult(taskFile?.FileData, taskFile?.FileContentType);
}
}
Next, make sure your application startup (Startup.cs) is configured to correctly use MVC and has the following line present (add it if not):
services.AddMvc();
.. and then finally modify your component to link to the controller, for example (iterative based example using a custom class):
<tbody>
#foreach (var attachment in yourAttachments)
{
<tr>
<td>#attachment.Filename </td>
<td>#attachment.CreatedUser</td>
<td>#attachment.Created?.ToString("dd MMM yyyy")</td>
<td><ul><li class="oi oi-circle-x delete-attachment"></li></ul></td>
</tr>
}
</tbody>
Hopefully this helps anyone who struggled (like me!) to get an appropriate answer to this seemingly simple question in the realms of Blazor…!

Xamarin.forms how to hold image on same page after navigating another page

I have uploaded image on my profile page and I want to hold that image until I logout in xamarin forms.
My image will be lost if I select another page so I want to hold it until I log out.
var profile = new Image { };
profile.Source = "profile.png";
profile.HorizontalOptions = LayoutOptions.StartAndExpand;
profile.VerticalOptions = LayoutOptions.StartAndExpand;
var profiletap = new TapGestureRecognizer();
profiletap.Tapped += async (s, e) =>
{
var file = await CrossMedia.Current.PickPhotoAsync();
if (file == null)
return;
await DisplayAlert("File Location", file.Path, "OK");
im = ImageSource.FromStream(() =>
{
var stream = file.GetStream();
//file.Dispose();
return stream;
});
profile.Source = im;
// await Navigation.PushModalAsync(new PhotoPage(im));
};
profile.GestureRecognizers.Add(profiletap);
Pages do not get destroyed when navigating to another page and coming back, which is why a page's constructor only gets executed the first time it is shown. So I am not sure what you mean when you say you want to hold that image.
Having said that, you could always assign the entire profile variable to a static global variable in your App class like below so that it stays the same no matter what. Then you would have to assign/initialize the global variable at the correct time.
But again, I am not sure if that is necessary, so you might try to explain more what the issue actually is:
In the App class:
public class App : Application {
public static Image ProfileImage = new Image {
Source = "profile.png",
HorizontalOptions = LayoutOptions.StartAndExpand,
VerticalOptions = LayoutOptions.StartAndExpand
};
....
}
Then in your page:
public class ProfilePage : ContentPage {
public ProfilePage() {
....
App.ProfileImage.GestureRecognizers.Add(profiletap);
}
}
Edit: See my answer here for an example of using a plugin to allow the user to choose a photo from their device's camera roll. Once you have the photo path, you can simply use HttpClient to send the image and a base64 string. There are plenty of other example online about how to do that.
Edit #2: After this line of your code:
var file = await CrossMedia.Current.PickPhotoAsync();
You now have the file and the path in file variable. So currently all you are doing is showing the image using ImageSource.FromStream but in order to keep showing the image when you return to the page, you need to also save the image to the device. In order to do that, you will need to write platform specific code in each project and reference that in your shared code. Something like this:
In your iOS and Android project, create a new file (FileHelper_Android.cs and FileHelper_iOS.cs for example) and add the following (the same code can be added to both iOS and Android files, just change the name of the class and file:
using ....;
[assembly: Dependency(typeof(FileHelper_Android))]
namespace YourNamespace.Droid{
/// <summary>
/// Responsible for working with files on an Android device.
/// </summary>
internal class FileHelper_Android : IFileHelper {
#region Constructor
public FileHelper_Android() { }
#endregion
public string CopyFile(string sourceFile, string destinationFilename, bool overwrite = true) {
if(!File.Exists(sourceFile)) { return string.Empty; }
string fullFileLocation = Path.Combine(Environment.GetFolderPath (Environment.SpecialFolder.Personal), destinationFilename);
File.Copy(sourceFile, fullFileLocation, overwrite);
return fullFileLocation;
}
}
}
Do the same on iOS and just change the file name. Now in your shared project you need to create IFileHelper.cs like so:
public interface IFileHelper {
string CopyFile(string sourceFile, string destinationFilename, bool overwrite = true);
}
Finally, in your page you would write the following:
_fileHelper = _fileHelper ?? DependencyService.Get<IFileHelper>();
profiletap.Tapped += async (s, e) =>
{
var file = await CrossMedia.Current.PickPhotoAsync();
if (file == null)
return;
await DisplayAlert("File Location", file.Path, "OK");
profile.Source = im;
imageName = "SomeUniqueFileName" + DateTime.Now.ToString("yyyy-MM-dd_hh-mm-ss-tt");
filePath = _fileHelper.CopyFile(file.Path, imageName);
im = ImageSource.FromFile(filePath)
// await Navigation.PushModalAsync(new PhotoPage(im));
};
Above, once the user chooses the file, we copy that file locally and the we also set your im variable to the new local file path, which gets returned from the IFileHelper.CopyFile method.
You still need to handle the case when the user comes back to the page or turns the app off and on again. In that situation, you need to load the saved image path. I would suggest either saving the image path into the DB, unless the user will only ever have a single profile image, then you could always just load that same path and filename. Let me know if you still have issues.

Blank PDF being generated from ActiveReports/WebAPI

On a project I am working on, I'm building a feature that lets users generate a report - in my case, it will go on an envelope - on-demand from information stored in our database. The problem I'm trying to solve, is that a blank PDF is being generated.
I've tried some sanity checks. First I set a breakpoint in Visual Studio and ensured that the models being passed to the report had fixed data; the reports were blank. Next, I tried including a static label that's not tied to any data, to determine if it's a report data-binding issue - the static label is not appearing in the generated report either.
More stymying, is that I've used similar code in the past without issue. I have no idea why a blank PDF file would be generated in this case.
I've read the 'Similar Questions' provided by StackOverflow, specifically this question from one year ago, but it had no answers, and thus nothing to learn from it. I've also tried the requisite Google searches, but found nothing relevant.
The only thing I cannot provide is the actual ActiveReport itself. I've checked this for Silly Programmer Errors™ like having everything hidden, or transparent labels, or similar silly things. Unfortunately, I've made no such errors.
Report Code:
public partial class EnvelopeReport : SectionReport
{
public EnvelopeReport()
{
InitializeComponent();
}
internal void RunReport(IEnumerable<PrintedAddress> model)
{
if (model != null)
{
DataSource = model;
}
Run();
}
private void OnReportStart(object sender, EventArgs e)
{
Document.Printer.PrinterName = string.Empty;
PageSettings.PaperKind = PaperKind.Number10Envelope;
PageSettings.Margins.Top = 0.25f;
PageSettings.Margins.Left = 0.5f;
PageSettings.Margins.Right = 0.5f;
PageSettings.Margins.Bottom = 0.25f;
}
}
Web API Controller Code:
[HttpGet]
public HttpResponseMessage EnvelopeReport(int addressId, string attentionTo, bool isConfidential)
{
Address address = AddressRepository.GetAddress(addressId, true);
List<PrintedAddress> models = new List<PrintedAddress>
{
new PrintedAddress(address, attentionTo, isConfidential)
};
var report = new EnvelopeReport();
report.RunReport(models);
var pdfExporter = new ActiveReportsPdfExporter();
var reportBytes = pdfExporter.ExportPdf(report);
var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new ByteArrayContent(reportBytes, 0, reportBytes.Length);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = "Envelope Report.pdf"
};
return response;
}
PDF Exporter:
public class ActiveReportsPdfExporter
{
private readonly PdfExport _pdfExport;
public ActiveReportsPdfExporter()
{
_pdfExport = new PdfExport();
}
public byte[] ExportPdf(SectionReport report)
{
using (var stream = new MemoryStream())
{
_pdfExport.Export(report.Document, stream);
return stream.ToArray();
}
}
public Stream ExportPdfToStream(SectionReport report)
{
var stream = new MemoryStream();
_pdfExport.Export(report.Document, stream);
return stream;
}
}
Client Service (Angular):
(function () {
angular.module('app').factory('addressSvc', [
'$http', addressSvc
]);
function addressSvc($http) {
var service = {
printAddress: function(addressId, attentionTo, someFlag) {
var args = {
'addressId': thingId,
'attentionTo': attentionTo,
'isConfidential': isConfidential
};
return $http.get('/api/common/EnvelopeReport', { 'params': args });
}
};
return service;
}
})();
Client Controller (Angular):
(function() {
angular.module('app').controller('someCtrl', [
'$window', 'addressSvc', controller
]);
function controller($window, addressSvc) {
var vm = this;
vm.attentionTo = ''; // Bound to UI.
vm.isConfidential = ''; // Also bound to UI.
vm.address = {}; // Unimportant how we get this.
vm.printAddress = printAddress;
function printAddress() {
addressSvc.printAddress(vm.address.id, vm.attentionTo, vm.isConfidential)
.then(function(result) {
var file = new Blob([result], {type: 'application/pdf'});
var fileURL = URL.createObjectURL(file);
if(window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(file, 'Envelope.pdf');
} else {
$window.open(fileURL);
}
});
}
}
)();
Question: Why is this code generating an empty PDF? I've used the Report/API Controller structure successfully in the past to generate PDFs, but usually in the context of MVC, not Web API. Another potential point of failure is the client code - I've not previously passed reports between server and client this way.
So, it turns out my server-side code was completely sane. The Client code was off.
Instead of Blobbing the data returned from the server and all of that work, what I instead needed to do was build a URL...and call $window.open(url); This is because my server code as it stands will return the PDF file as-is.

Categories