Issue
Until recently I have been stuck on how to achieve the goal of “exporting” a report from a StackLayout into a PDF in a project I somehow pulled out of Dev Limbo.
–BackStory–
Previously I have tried to continue the use of the already placed (in the project) PDFSharp package to convert the data presented in the XAML to a PDF for a client. Long story short, I was unable to get PDFSharp to do what I needed it to do and turned to Syncfusion. They seemed to have the features I needed to make this happen. Going based off the code samples they had, I was able to get close to my goal, but not quite. They have the capture portion and they have the pagination portion, but not a combination of the two. I essentially needed to paginate the screenshot that CaptureAsync() saves to make a pdf of the entire report.
Solution
–How was this resolved?–
After doing some digging, I came across an answer in this article (I am forever grateful) and forged a solution using it.
Here’s a sample of my XAML content page for context:
<?xml version"1.0" encoding"utf-8"?>
<ContentPage xmlns"http://xamarin.com/schemas/2014/forms"
xmlns:x"http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls"clr-namespace:ReportTool.Controls"
x:Class"ReportTool.ReportViewer">
<ContentPage.Content>
<StackLayout Style"{StaticResource TopLevelStackLayout}">
<!-- Body Block -->
<Grid x:Name"MainGrid" Style"{StaticResource MainContainingGrid}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width"1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height"1*" />
</Grid.RowDefinitions>
<ScrollView x:Name"MainScrollLayout" VerticalOptions"Fill" HorizontalOptions"Fill" Grid.Row"0" Grid.Column"0" BackgroundColor"#FFFFFF" MinimumWidthRequest"700">
<StackLayout x:Name"MainStackLayout" Style"{StaticResource MainBkg}">
<Button x:Name"DownloadPdfBtn" Text"Export to PDF" Clicked"DownloadPdfBtn_OnClicked" TextColor"White" BackgroundColor"DodgerBlue" VerticalOptions"Start" HorizontalOptions"Start" />
<Image Source"~\..\Assets\Logos\CompanyLogo.png" Margin"0,60,0,10" HorizontalOptions"Center" />
<Label x:Name"TitlePageTitleText" Style"{StaticResource ReportViewerTitleTextMain}" Text"{StaticResource CompanyAnalysisReport}" />
<Label x:Name"TitlePagePreparedFor" Style"{StaticResource ReportViewerTitleTextMiddle}" Text"{StaticResource PreparedFor}" />
<Label x:Name"TitlePageOrganizationName" Style"{StaticResource ReportViewerTitleTextMiddle}" />
<Label x:Name"TitlePageOrganizationAddress1" Style"{StaticResource ReportViewerTitleTextMiddle}" />
<Label x:Name"TitlePageOrganizationAddress2" Style"{StaticResource ReportViewerTitleTextMiddle}" />
<Label x:Name"TitlePageOrganizationCityStateZip" Style"{StaticResource ReportViewerTitleTextLast}" />
<Grid x:Name"ReportGrid" Style"{StaticResource ReportGridBody}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width"90"/>
<ColumnDefinition Width"1*"/>
<ColumnDefinition Width"125"/>
<ColumnDefinition Width"Auto"/>
<ColumnDefinition Width"Auto"/>
<ColumnDefinition Width"1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height"Auto"/>
</Grid.RowDefinitions>
</Grid>
</StackLayout>
</ScrollView>
</Grid>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Here is the code for the star of the show, the ExportToPdf button:
using Syncfusion.Drawing;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
private async void DownloadPdfBtn_OnClicked(object sender, EventArgs e)
{
try
{
var filename "SurveyReport_" + ((App)Application.Current).CurrentUser.UserName + "_" + DateTime.UtcNow.ToString("MMddyy") + ".pdf";
// Init Memory Stream.
var stream new MemoryStream();
//Create a new PDF document
using (var document new PdfDocument())
{
// Add page to the PDF document.
var page document.Pages.Add();
// Get the scroll view height.
var xamlPageHeight MainScrollLayout.ContentSize.Height;
// Get the page dimensions.
var pageWidth page.GetClientSize().Width;
var pageHeight page.GetClientSize().Height;
// Capture the number of pages.
var numberOfPages (int)Math.Ceiling(xamlPageHeight / pageHeight);
for (var i 0; i < numberOfPages; i++)
{
// Find beginning of page.
await MainScrollLayout.ScrollToAsync(0, i * pageHeight, false).ConfigureAwait(false);
// Capture the XAML page as an image and returns the image in memory stream.
var byteData await DependencyService.Get<IExportPdf>().CaptureAsync();
var imageStream new MemoryStream(byteData);
// Load the image in PdfBitmap.
var pdfBitmapImage new PdfBitmap(imageStream);
// Set the pdf page settings.
document.PageSettings.Margins.All 0;
document.PageSettings.Orientation PdfPageOrientation.Portrait;
document.PageSettings.Size new SizeF(pageWidth, pageHeight);
// Add new page for graphics (otherwise graphics won't know where to draw the rest of the image)
page document.Pages.Add();
// Graphics for drawing image to pdf.
var graphics page.Graphics;
// Draw the image to the page.
graphics.DrawImage(pdfBitmapImage,0,0, pageWidth, pageHeight);
// Insert page at i position.
document.Pages.Insert(i, page);
// Save the document into memory stream.
document.Save(stream);
}
}
stream.Position 0;
// Save the stream as a file in the device and invoke it for viewing.
await Xamarin.Forms.DependencyService.Get<IExportPdf>().Save(filename, "application/pdf", stream);
}
catch (Exception ex)
{
DisplayErrorAlert("DownloadPdfBtn_OnClicked", ex.StackTrace);
}
}
It is important to note that you will need a dependency in order to save anywhere other than local memory. Thankfully, Syncfusion provides a snippet for you to use. For the sake of your time, I will share the snippets. You will need to add two .cs files, one class file with the capture/save functionality and one interface file for your app.
Capture/Save class:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Display;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Xamarin.Forms;
public class ExportPdf : IExportPdf
{
public async Task<byte[]> CaptureAsync()
{
var renderTargetBitmap new RenderTargetBitmap();
await renderTargetBitmap.RenderAsync(Window.Current.Content);
var pixelBuffer await renderTargetBitmap.GetPixelsAsync();
var pixels pixelBuffer.ToArray();
var displayInformation DisplayInformation.GetForCurrentView().LogicalDpi;
var stream new InMemoryRandomAccessStream();
var encoder await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);
encoder.SetPixelData(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Ignore,
(uint)renderTargetBitmap.PixelWidth,
(uint)renderTargetBitmap.PixelHeight,
displayInformation,
displayInformation,
pixels);
await encoder.FlushAsync();
stream.Seek(0);
var readStream stream.AsStreamForRead();
var bytes new byte[readStream.Length];
await readStream.ReadAsync(bytes, 0, bytes.Length);
return bytes;
}
public async Task Save(string filename, string contentType, MemoryStream stream)
{
if (Device.Idiom ! TargetIdiom.Desktop)
{
var local ApplicationData.Current.LocalFolder;
var outFile await local.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using (var outStream await outFile.OpenStreamForWriteAsync()) { await outStream.WriteAsync(stream.ToArray(), 0, (int)stream.Length); }
if (contentType ! "application/html") await Windows.System.Launcher.LaunchFileAsync(outFile);
}
else
{
StorageFile storageFile null;
var savePicker new FileSavePicker
{
SuggestedStartLocation PickerLocationId.Desktop,
SuggestedFileName filename
};
switch (contentType)
{
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
savePicker.FileTypeChoices.Add("PowerPoint Presentation", new List<string> { ".pptx" });
break;
case "application/msexcel":
savePicker.FileTypeChoices.Add("Excel Files", new List<string> { ".xlsx" });
break;
case "application/msword":
savePicker.FileTypeChoices.Add("Word Document", new List<string> { ".docx" });
break;
case "application/pdf":
savePicker.FileTypeChoices.Add("Adobe PDF Document", new List<string> { ".pdf" });
break;
case "application/html":
savePicker.FileTypeChoices.Add("HTML Files", new List<string> { ".html" });
break;
}
storageFile await savePicker.PickSaveFileAsync();
using (var outStream await storageFile.OpenStreamForWriteAsync())
{
await outStream.WriteAsync(stream.ToArray(), 0, (int)stream.Length);
await outStream.FlushAsync();
outStream.Dispose();
}
stream.Flush();
stream.Dispose();
await Windows.System.Launcher.LaunchFileAsync(storageFile);
}
}
}
Interface:
using System.IO;
using System.Threading.Tasks;
public interface IExportPdf
{
Task Save(string filename, string contentType, MemoryStream stream);
Task<byte[]> CaptureAsync();
}
And that should do it! I hope this helps anyone that has been tasked with something similar!
Answered By – Wolfie_Mk6