It is fairly easy to display a PDF file that is local to your phone. For example, see this article from Xamarin.com.
A bit trickier is to get a PDF from a server and then to display it. Here’s how I did it (note, this blog post offers snippets of code, rather than a complete solution… I will reduce my larger program to something manageable asap)
The first thing I did was create a model object named ReportItem. Its primary goal is to help group all the items in my list of pdf’s, but it contains the all-important URL to the pdf itself.
Next, I created a page named ShowPDFPage. In the XAML part of this page I just have a CustomWebView:
<local:CustomWebView Uri="foo.pdf"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
The CustomWebView was taken from an article I found on the web.
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create (propertyName:"Uri",
returnType:typeof(string),
declaringType:typeof(CustomWebView),
defaultValue:default(string));
public string Uri
{
get { return ( string ) GetValue( UriProperty ); }
set { SetValue( UriProperty, value ); }
}
}
This performs the magic of showing the PDF file, but how do we get it?
Gettig the PDF From The Server
In my ViewModel page I declare the FileName as a string property. Every pdf will have the same name, and will overwrite the one obtained and displayed previously. That is, I don’t persist the PDF, I just get it, show it, and then get another.
To do so, I pass the URL (and associated data) to my GetPDF method. I then fill the headers that the server requires. I start by setting up the Http client:
client = new HttpClient();
client.MaxResponseContentBufferSize = 256000;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( "application/json" ) );
I then use James Montemagno’s terrific connectivity plugin to make sure I’m connected or to be notified when I am.
Next, I send the server-specific headers, e.g.,
client.DefaultRequestHeaders.Add( "version", "1.0" );
I’m ready now to ask for the PDF file. If I get a success code, I know that I’ve been sent a byte array representing the PDF, so I have to convert it. Once converted, I save it:
var response = await client.GetAsync(configuration.Url);
if ( response.IsSuccessStatusCode )
{
byte[] contentAsByteArray = await response.Content.ReadAsByteArrayAsync();
await FileSaver.SaveFile( contentAsByteArray, "MyFile.pdf" );
}
It is that file that I feed to the custom viewer (shown above).
FileSaver is also “borrowed” from an article on the web, but I can’t find the URL, so if this is your code, let me know and I’ll add a link:
public static class FileSaver { public static async Task<string> SaveFile( byte[] fileBytes, string filename ) { try { string filepath = null; if ( Device.RuntimePlatform == Device.Android ) { var pdfWriter = DependencyService.Get<IAndroidPDFWriter>(); filepath = pdfWriter.CreateFile( filename, fileBytes.ToArray() ); } else if ( Device.RuntimePlatform == Device.iOS ) { var rootFolder = FileSystem.Current.GetFolderFromPathAsync( DependencyService.Get<IFileHelper>().GetLocalFilePath() ).Result; using ( var pdfBytes = new MemoryStream( fileBytes ) ) { var file = await rootFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting); var newFile = await file.OpenAsync(FileAccess.ReadAndWrite); using ( var outputStream = newFile ) { pdfBytes.CopyTo( outputStream ); } } } if ( string.IsNullOrEmpty( filepath ) ) { Log.LogThis( "file path empty", "DataFileManager", DateTime.Now ); return "Error: Unable to save file"; } else { return filepath; } } catch ( Exception ex ) { Log.LogThis( "Exception", ex.Message, DateTime.Now ); return "Error: " + ex.Message; } } }
Notice that if you are on Android phone there is a call to AndroidPDFWriter. This too came from the web, and looks like this (in the Android project):
[assembly: Dependency( typeof( AndroidPDFWriter ) )]
namespace myProject.Droid
{
public class AndroidPDFWriter : IAndroidPDFWriter
{
public string CreateFile( string filename, byte[] bytes )
{
if (! Directory.Exists( Path.Combine( global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "myFileName" ) ) )
{
Directory.CreateDirectory( Path.Combine( global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "myFileName" ) );
}
var path = Path.Combine(global::Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, "myFileName", filename);
File.WriteAllBytes( path, bytes );
return path;
}
}
}
This of course relies on dependency injection. The interface we’re using looks like this:
public interface IAndroidPDFWriter
{
string CreateFile( string filename, byte[] bytes );
}
Note that you’ll also need interfaces and implementations that know how to write to files on the individual platforms. If this is of interest, I can follow up with a blog post on that as well. Here’s the snippet that matters:
[assembly: Dependency( typeof( FileImplementation ) )]
namespace myProject.Droid
{
public class FileImplementation : IFile
{
public void SaveText( string filename, string text )
{
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
var filePath = Path.Combine(documentsPath, filename);
File.Delete( filePath );
File.WriteAllText( filePath, text );
}
Hope all that helps.