Opening a PDF with Xamarin Forms - c#

I have a pdf i've added as an AndroidAsset and a BundleResource for my Android and IOS projects using xamarin forms.
I just want to be able to open those files from any device, using whatever pdf viewer the device defaults to.
Essentially, i just want to be able to do something like:
Device.OpenUri("file:///android_asset/filename.pdf");
but this doesn't seem to work. Nothing happens and the user is never prompted to open the pdf. I don't want to use any 3rd party libraries that allow the pdf to open in app, i just want it to redirect the user to a pdf viewer or browser.
Any ideas?

First of all you will need an interface class, since you need to call the dependency service in order to pass your document to the native implementation(s) of your app:
so in your shared code add an interface, called "IDocumentView.cs":
public interface IDocumentView
{
void DocumentView(string file, string title);
}
Android
Now in your android project create the corresponding implementation "DocumentView.cs":
assembly: Dependency(typeof(DocumentView))]
namespace MyApp.Droid.Services
{
public class DocumentView: IDocumentView
{
void IDocumentView.DocumentView(string filepath, string title)
{
try
{
File file = new File(filepath);
String mime = FileTypes.GetMimeTypeByExtension(MimeTypeMap.GetFileExtensionFromUrl(filepath));
File extFile = new File (Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDocuments), file.Name);
File extDir = extFile.ParentFile;
// Copy file to external storage to allow other apps to access ist
if (System.IO.File.Exists(extFile.AbsolutePath))
System.IO.File.Delete(extFile.AbsolutePath);
System.IO.File.Copy(file.AbsolutePath, extFile.AbsolutePath);
file.AbsolutePath, extFile.AbsolutePath);
// if copying was successful, start Intent for opening this file
if (System.IO.File.Exists(extFile.AbsolutePath))
{
Intent intent = new Intent();
intent.SetAction(Android.Content.Intent.ActionView);
intent.SetDataAndType(Android.Net.Uri.FromFile(extFile), mime);
MainApplication.FormsContext.StartActivityForResult(intent, 10);
}
}
catch (ActivityNotFoundException anfe)
{
// android could not find a suitable app for this file
var alert = new AlertDialog.Builder(MainApplication.FormsContext);
alert.SetTitle("Error");
alert.SetMessage("No suitable app found to open this file");
alert.SetCancelable(false);
alert.SetPositiveButton("Okay", (object sender, DialogClickEventArgs e) => ((AlertDialog)sender).Hide());
alert.Show();
}
catch (Exception ex)
{
// another exception
var alert = new AlertDialog.Builder(MainApplication.FormsContext);
alert.SetTitle("Error");
alert.SetMessage("Error when opening document");
alert.SetCancelable(false);
alert.SetPositiveButton("Okay", (object sender, DialogClickEventArgs e) => ((AlertDialog)sender).Hide());
alert.Show();
}
}
}
}
Please note that MainApplication.FormsContext is a static variable I added to my MainApplication.cs in order to be able to access the Context of the app quickly.
In your Android manifest, add
In your application resources, add an xml resource (into folder "xml") called file_paths.xml with the following content:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="root" path="/"/>
<external-files-path name="files" path="files" />
</paths>
Also you need to ensure that there are apps installed on the target device, which are able to handle the file in question. (Acrobat Reader, Word, Excel, etc...).
iOS
iOS already comes with a quite nice document preview built in, so you can simply use that (again create a file named "DocumentView.cs" in your iOS Project):
[assembly: Dependency(typeof(DocumentView))]
namespace MyApp.iOS.Services
{
public class DocumentView: IDocumentView
{
void IDocumentView.DocumentView(string file, string title)
{
UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
QLPreviewController previewController = new QLPreviewController();
if (File.Exists(file))
{
previewController.DataSource = new PDFPreviewControllerDataSource(NSUrl.FromFilename(file), title);
UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(previewController, true, null);
}
});
}
}
public class PDFItem : QLPreviewItem
{
public PDFItem(string title, NSUrl uri)
{
this.Title = title;
this.Url = uri;
}
public string Title { get; set; }
public NSUrl Url { get; set; }
public override NSUrl ItemUrl { get { return Url; } }
public override string ItemTitle { get { return Title; } }
}
public class PDFPreviewControllerDataSource : QLPreviewControllerDataSource
{
PDFItem[] sources;
public PDFPreviewControllerDataSource(NSUrl url, string filename)
{
sources = new PDFItem[1];
sources[0] = new PDFItem(filename, url);
}
public override IQLPreviewItem GetPreviewItem(QLPreviewController controller, nint index)
{
int idx = int.Parse(index.ToString());
if (idx < sources.Length)
return sources.ElementAt(idx);
return null;
}
public override nint PreviewItemCount(QLPreviewController controller)
{
return (nint)sources.Length;
}
}
}
Finally you can call
DependencyService.Get<IDocumentView>().DocumentView(file.path, "Title of the view");
to display the file in question.

Related

How can i open PDF (In new window, or download it) in Post Method

The first one of the following examples is working perfectly. The second one istn working, the pdf is saved to the database but the Onpost Method isnt working
1.( Working Example) I am doing this with _taget="blank and calling an empty Razor-Page.
The code looks like this.
HTML
<a class="btn btn-outline-dark" data-toggleToolTip="tooltip" data-placement="top"
title="Anzeigen" asp-page="/Invoices/DisplayInvoiceAsPdf" asp-route-invoiceId="#item.Nr" target="_blank">
<i class="fa-solid fa-download"></i>
</a>
Code behind of empty Razor-Page:
public class DisplayInvoiceAsPdfModel : PageModel
{
private readonly IDataRepository _DataRepository;
public DisplayInvoiceAsPdfModel(IDataRepository DataRepository)
{
_DataRepository = DataRepository;
}
public Kopfdaten Kopfdaten { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Kopfdaten = await _DataRepository.GetDataById(id);
if(Kopfdaten.Pdf == null)
{
return NotFound();
}
return File(Kopfdaten.Pdf, "application/pdf");
}
}
When i click on the Button the Pdf will be opened in a new tab (In Google Chrome).
2.(Not working Example): Im Creating a Preview Pdf in the OnPostmMethod, The PDf should be opened after the Pdf is created and stored to My database. I want to open Pdf with await OnPostPDf(id)
if (y == "OK")
{
//Ok=> Open Pdf in new Tab
await OnPostPDF(id);
if (testBool == true)
{
//Refresh page ,error
return RedirectToPage("Invoice", new { Id = adrId});
}
else
{
//Post
return RedirectToPage("/Index");
}
}
OnpostPdf looks like this:
public async Task<IActionResult> OnPostPDF(int id)
{
Kopfdaten kopfdaten = new Kopfdaten();
kopfdaten = await _DataRepository.DataById(id);
if (kopfdaten.Pdf == null)
{
return NotFound();
}
return File(kopfdaten.Pdf, "application/pdf");
}
Get Data ById
public async Task<Kopfdaten> GetDataById(int id)
{
try
{
return await _Context.Kopfdaten.FindAsync(id);
}
catch (Exception ex)
{
throw new Exception($"Couldn't retrieve entities: {ex.Message}");
}
}
Kopfdaten model:
public partial class Kopfdaten
{
public int Id { get; set; }
public int InVoiceNumber { get; set; }
public string Text { get; set; }
public int AdressId{ get; set; }
public byte[] Pdf { get; set; }
}
"How can I open PDF (In new window, or download it) in Post Method"
So there are 3 ways to handle this kind of scenario.
Way: 1 When You have an existing PDF file in your application folder
In this scenario, suppose you have PDF file in your application folder like below:
Solution:
public ActionResult DisplayPDFFromExistingFile()
{
string physicalPath = "wwwroot/KironGitHub.pdf";
byte[] pdfBytes = System.IO.File.ReadAllBytes(physicalPath);
MemoryStream ms = new MemoryStream(pdfBytes);
return new FileStreamResult(ms, "application/pdf");
}
Output:
Way: 2 When you want to display PDF on your browser from database Model
Solution:
public ActionResult DisplayPdfOnBrowserFromDatabaseList()
{
var data = _context.Members.ToList();
var pdf = data.ToPdf();
MemoryStream ms = new MemoryStream(pdf);
return new FileStreamResult(ms, "application/pdf");
}
Note: To handle this scenario you need to use 3rd party Nuget package which is ArrayToPdf You can search on Nuget package on your visual studio. You can also check the GitHub Link. Finally need to add using ArrayToPdf; on top.
Output:
Way: 3 When you want to download PDF on your browser from database model or existing file
Solution:
public ActionResult DownloadPDFOnBrowser()
{
var data = _context.Members.ToList();
var byteArray = data.ToPdf();
MemoryStream stream = new MemoryStream(byteArray);
string mimeType = "application/pdf";
return new FileStreamResult(stream, mimeType)
{
FileDownloadName = "DatabaseListToPdf.pdf"
};
}
Note: As described above we need to add using ArrayToPdf; on top. To handle this scenario you need to use 3rd party Nuget package which is ArrayToPdf You can search on Nuget package on your visual studio. See the screenshot below:
You can also check the GitHub Link
Output:
These are the use case scenario of how we could handle (PDF) the issue you are having.

Xamarin.Forms: How to download an Image, save it locally and display it on screen?

I have an issue with opening JPG file in native application on Android.
I'm using newest release of Xamarin Essentials, there is some functionality called Launcher.
Here is my code
await Launcher.TryOpenAsync("file:///" + localPath);
My local path is file stored in Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);.
Whenever I try to open that file I am getting error:
file:////data/user/0/mypackagename/files/.local/share/Screenshot.jpg exposed beyond app through Intent.getData()
I found several solutions here on stackoverflow, but I don't want to use Intent because my application is designed to be cross-platform (I would like to avoid native platform coding if this possible).
Launcher throws error on iOS also:
canOpenURL: failed for URL: ** -- file:///" - error: "This app is not allowed to query for scheme file
What am I doing wrong here?
Step 1: Download the Image
We can use HttpClient to download the image.
HttpClient.GetByteArrayAsync will retrieve the image data and save it in memory.
In DownloadImage below, we will retrieve the image as a byte[].
static class ImageService
{
static readonly HttpClient _client = new HttpClient();
public static Task<byte[]> DownloadImage(string imageUrl)
{
if (!imageUrl.Trim().StartsWith("https", StringComparison.OrdinalIgnoreCase))
throw new Exception("iOS and Android Require Https");
return _client.GetByteArrayAsync(imageUrl);
}
}
Step 2 Save Image to Disk
Now that we've downloaded the image, we will save it to disk.
Xamarin.Essentials.Preferences allows us to save items to disk using key-value pairs. Since byte[] is just a pointer to memory, we'll have to first convert the byte[] to base64-string before we can save it to disk.
static class ImageService
{
static readonly HttpClient _client = new HttpClient();
public static Task<byte[]> DownloadImage(string imageUrl)
{
if (!imageUrl.Trim().StartsWith("https", StringComparison.OrdinalIgnoreCase))
throw new Exception("iOS and Android Require Https");
return _client.GetByteArrayAsync(imageUrl);
}
public static void SaveToDisk(string imageFileName, byte[] imageAsBase64String)
{
Xamarin.Essentials.Preferences.Set(imageFileName, Convert.ToBase64String(imageAsBase64String));
}
}
Step 3 Retrieve the Image for Display
Now that we've downloaded the image and saved it to disk, we need to be able to retrieve the image from disk to display it on the screen.
GetFromDisk below retrieves the image from disk and converts it to Xamarin.Forms.ImageSource.
static class ImageService
{
static readonly HttpClient _client = new HttpClient();
public static Task<byte[]> DownloadImage(string imageUrl)
{
if (!imageUrl.Trim().StartsWith("https", StringComparison.OrdinalIgnoreCase))
throw new Exception("iOS and Android Require Https");
return _client.GetByteArrayAsync(imageUrl);
}
public static void SaveToDisk(string imageFileName, byte[] imageAsBase64String)
{
Xamarin.Essentials.Preferences.Set(imageFileName, Convert.ToBase64String(imageAsBase64String));
}
public static Xamarin.Forms.ImageSource GetFromDisk(string imageFileName)
{
var imageAsBase64String = Xamarin.Essentials.Preferences.Get(imageFileName, string.Empty);
return ImageSource.FromStream(() => new MemoryStream(Convert.FromBase64String(imageAsBase64String)));
}
}
Example: Using ImageService in a Xamarin.Forms.ContentPage
class App : Application
{
public App() => MainPage = new MyPage();
}
class MyPage : ContentPage
{
readonly Image _downloadedImage = new Image();
public MyPage()
{
Content = _downloadedImage;
}
protected override async void OnAppearing()
{
const string xamrainImageUrl = "https://cdn.dribbble.com/users/3701/screenshots/5557667/xamarin-studio-1_2x_4x.png"
const string xamarinImageName = "XamarinLogo.png";
var downloadedImage = await ImageService.DownloadImage(xamrainImageUrl);
ImageService.SaveToDisk(xamarinImageName, downloadedImage);
_downloadedImage.Source = ImageService.GetFromDisk(xamarinImageName);
}
}

How to pass device token to PCL from Android MainActivity?

I have added code in MainActivity.cs file for generating Device Id. Now I want to pass that device token to my PCL project main page, How's that possible? I also want to know about how to generate Device Token in IOS app? and how to pass that token to Portable Class Library?
Code Sample :
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
if (Intent.Extras != null)
{
foreach (var key in Intent.Extras.KeySet())
{
var value = Intent.Extras.GetString(key);
Log.Debug("Key: {0} Value: {1}", key, value);
}
}
FirebaseApp.InitializeApp(this);
var instanceId = FirebaseInstanceId.Instance;
if (FirebaseInstanceId.Instance.Token != null)
Log.Debug("MyToken", FirebaseInstanceId.Instance.Token.ToString());
}
}
I need this "My Token" data on login page button Click event. Hows this possible?
My Login Page Code is
public partial class LoginPage : ContentPage
{
private readonly DataService _dataService = new DataService();
public LoginPage()
{
InitializeComponent();
}
private async Task BtnLogin_ClickedAsync(object sender, EventArgs e)
{
var result = await _dataService.Authentication(TxtUserName.Text, TxtPassword.Text,"MyToken");
if (result.AccessToken != null)
{
await Navigation.PushModalAsync(new MainMasterPage());
GlobalClass.userToken = result;
}
else
await DisplayAlert("", Resource.InvalidMessage, Resource.OkText);
}
}
Welcome to the Realm of Dependency Injection :)
documentation can be found here
You need to create a interface on your PCL then reference that on your Native project
Example:
Create class DeviceToke.cs in your PCL
public interface ITextToSpeech
{
void Speak(string text);
}
Then in your native project, you can do the following:
sample code:
[assembly: Dependency(typeof(TextToSpeechAndroidImpl))]
namespace IocAndDiXamarinForms.Droid
{
public class TextToSpeechAndroidImpl : Java.Lang.Object, ITextToSpeech, TextToSpeech.IOnInitListener
{
TextToSpeech speaker;
string toSpeak;
public void Speak(string text)
{
var ctx = Forms.Context; // useful for many Android SDK features
toSpeak = text;
if (speaker == null)
{
speaker = new TextToSpeech(ctx, this);
}
else
{
var p = new Dictionary<string, string>();
speaker.Speak(toSpeak, QueueMode.Flush, p);
}
}
#region IOnInitListener implementation
public void OnInit(OperationResult status)
{
if (status.Equals(OperationResult.Success))
{
var p = new Dictionary<string, string>();
speaker.Speak(toSpeak, QueueMode.Flush, p);
}
}
#endregion
}
}
You can use the Xamarin messaging center to pass a message back from your platform-specific classes to your PCL ViewModel. You'll need to subscribe to the message in your VM, and send the message from your Android or iOS class. Then you can store the value in your VM and use it when the user clicks login.
Sending the message:
Xamarin.Forms.MessagingCenter.Send(FirebaseInstanceId.Instance.Token.ToString(), "InstanceId");
Subscribing in your VM:
MessagingCenter.Subscribe<string> (this, "InstanceId", (InstanceId) => {
// use the InstanceId as required
});
});
A handy solution is to define a publicly accessible static StrToken property in some public class, e.g. App:
public static Size Token;
and OnCreate on Android:
App.StrToken = FirebaseInstanceId.Instance;

Xamarin Forms Webview local

I am trying to load a Web application (Readium) in a webview with Xamarin locally. As a target I have UWP, Android and iOS.
I can not get the index.html page open, I have embedded the web in each of the projects, according to https://developer.xamarin.com/guides/xamarin-forms/user-interface/webview/ but I get a blank page.
have implemented the dependency service for each application such as (UWP)
assembly: Dependency(typeof(BaseUrl))]
namespace WorkingWithWebview.UWP
{
public class BaseUrl : IBaseUrl
{
public string Get()
{
return "ms-appx-web:///";
}
}
}
However, creating a new UWP project (without Xamarin), it works well, using the method NavigateToLocalStreamUri(uri, new StreamUriWinRTResolver()) with
public IAsyncOperation<IInputStream> UriToStreamAsync(Uri uri)
{
if (uri == null)
{
throw new Exception();
}
string path = uri.AbsolutePath; 
return GetContent(path).AsAsyncOperation();
}
private async Task<IInputStream> GetContent(string path)
{  
try
{
Uri localUri = new Uri("ms-appx:///cloud-reader" + path);
StorageFile f = await StorageFile.GetFileFromApplicationUriAsync(localUri);
IRandomAccessStream stream = await f.OpenAsync(FileAccessMode.Read);
return stream;
}
catch (Exception)
{
throw new Exception("Invalid path");
}
}
In what way would the same be done in Xamarin Forms?
Thanks you.
Finally I achieve to load local content adding a custom render for each platform.
Example (UWP):
[assembly: ExportRenderer(typeof(WebView), typeof(WebViewRenderer))]
namespace DisplayEpub.UWP{
public class CustomWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var customWebView = Element as WebView;
Control.Source = new Uri(string.Format("ms-appx-web:///Assets/pdfjs/web/viewer.html"));
}
}
}
}
I've followed the example of Xamarin docs to display PDF using custom render, targeting 3 platforms. I've tested it on Android and Windows:
https://developer.xamarin.com/recipes/cross-platform/xamarin-forms/controls/display-pdf/

ASP.NET MVC Saving a view as HTML file

I have a pretty basic page that has a few images via relative paths that I need to save to a .html file.
I have used this in the past but had problems with the images.
public static class ActionResultExtensions
{
public static string Capture(this ActionResult result, ControllerContext controllerContext)
{
using (var capture = new ResponseCapture(controllerContext.RequestContext.HttpContext.Response))
{
result.ExecuteResult(controllerContext);
return capture.ToString();
}
}
}
public class ResponseCapture : IDisposable
{
private readonly HttpResponseBase _response;
private readonly TextWriter _originalWriter;
private StringWriter _localWriter;
public ResponseCapture(HttpResponseBase response)
{
_response = response;
_originalWriter = response.Output;
_localWriter = new StringWriter();
response.Output = _localWriter;
}
public override string ToString()
{
_localWriter.Flush();
return _localWriter.ToString();
}
public void Dispose()
{
if (_localWriter == null) return;
_localWriter.Dispose();
_localWriter = null;
_response.Output = _originalWriter;
}
}
I'd like to know how to get the result WITH all of the CSS and images downloaded along with the HTML in a nice little package.
If you need it all in an HTML file you will need to do a tonne of nasties to inject css, js and images into the actual document, however you could use something like an MHT file which is a bundle of all resources required for a page.
this does the MHT thing by the looks, probably other options out there as well.
http://www.aspnetmht.com/Examples.aspx

Categories