使用Xamarin Form開發手機App時,若需要顯示PDF檔案該怎麼做?
在此分為兩部份實作:Server端(網頁伺服器)、Client端(Xamarin)。
參考下列文章:
1. Xamarin.Forms - 從網路下載檔案,並且儲存到手機中,接著,可以使用安裝在手機內的應用程式,開啟這個檔案。
2. Xamarin.Android 執行時期的權限處理說明 1 最低的 Android 版本 / 目標 Android 版本 / 目標 Framework。
3. Xamarin.Android 執行時期的權限處理說明 2 使用 Plugin.Permissions。
4. Xamarin.Android 執行時期的權限處理說明 3 使用手機內其他 PDF App 來開啟在 download 資料夾內的檔案。
5. 使用FileProvider解决file:// URI引起的FileUriExposedException。
Server端
在此,伺服器端是使用PHP作為提供PDF下載的網頁。
首先,安裝好Xampp並啟動。
在「htdocs」資料夾內建立一個資料夾(命名為tst)作為「網站根目錄」,
將一份PDF檔(檔名為csharp_tutorial.pdf)放入tst資料夾內,即完成伺服器端的建置作業。
測試網頁,在瀏覽器上輸入網址(http://伺服器IP/tst/csharp_tutorial.pdf),可以看到PDF檔。
Client端
先建立一個Xamarin專案(命名為Pdftst)。(右鍵開新頁面檢視圖片較清楚)
在MainPage.xaml中的Content Page的內容改為一個Button並加入Click事件。
在MainPage.xaml.cs內自動產生Button_Clicked事件處理函式。
安裝所需套件
為了取得Server端PHP網頁上的PDF檔,
需要使用HttpClient套件,
在PCL專案上,以NuGet安裝Microsoft.Net.HttpClient。
安裝HttpClient完成後,
在PCL專案的參考會發現HttpClient的三角形警告圖樣,
可以忽略,不用理會。
在PCL專案上,安裝PCLStorage套件,
用以存取手機上的檔案資料。
安裝完套件先按F6編譯一下。
建立DependencyServic
各平台上存取檔案必須由自己平台的函式去實作,
Xamarin提供了DependencyServic的方式,
來取得各平台的回傳資料並在PCL專案內做應用。
先在PCL專案加入兩個介面分別為:「IOpenFileByName」和「IPublicFileSystem」。
IPublicFileSystem:
「IFolder」介面需要「Using PCLStorage」:
public interface IPublicFileSystem
{
IFolder PublicDownloadFolder { get; }
}
IOpenFileByName:
interface IOpenFileByName
{
void OpenFile(string fullFileName);
}
在Android專案內實作DependencyServic介面
在Android專案內建立「Services」資料夾,
加入「OpenFileByName.cs」和「PublicFileSystem.cs」檔案。
Android專案內的「OpenFileByName.cs」檔:
using System;
using System.IO;
using Android.Content;
using Pdftst.Droid.Services;
using Xamarin.Forms;
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace Pdftst.Droid.Services
{
public class OpenFileByName
{
public void OpenFile(string fullFileName)
{
try
{
var filePath = fullFileName;
var fileName = Path.GetFileName(fullFileName);
var bytes = File.ReadAllBytes(filePath);
string externalStorageState = global::Android.OS.Environment.ExternalStorageState;
var externalPath = global::Android.OS.Environment.ExternalStorageDirectory.Path + "/" +
global::Android.OS.Environment.DirectoryDownloads + "/" + fileName;
File.WriteAllBytes(externalPath, bytes);
Java.IO.File file = new Java.IO.File(externalPath);
file.SetReadable(true);
string application = "application/pdf";
string extension = Path.GetExtension(filePath);
var uri = Android.Net.Uri.FromFile(file);
var intent = new Intent(Intent.ActionView);
intent.SetDataAndType(uri, application);
intent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask);
Forms.Context.StartActivity(intent);
}
cacth(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Android專案內的「PublicFileSystem.cs」檔:
using PCLStorage;
using Pdftst.Droid.Services;
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace Pdftst.Droid.Services
{
public class PublicFileSystem
{
public IFolder PublicDownloadFolder
{
get
{
var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(
Android.OS.Environment.DirectoryDownloads).AbsolutePath;
return new FileSystemFolder(localAppData);
}
}
}
}
在IOS專案內實作DependencyServic介面
在IOS專案內建立「Services」資料夾,
加入「OpenFileByName.cs」和「PublicFileSystem.cs」檔案。
在IOS專案內的「OpenFileByName.cs」檔:
using System.IO;
using Foundation;
using Pdftst.iOS.Services;
using UIKit;
using Xamarin.Forms;
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace Pdftst.iOS.Services
{
public class OpenFileByName : IOpenFileByName
{
public void MakeDownloadFolder(string fullFileName, string mimeType)
{
//do nothing
}
public void OpenFile(string fullFileName)
{
var filePath = fullFileName;
var fileName = Path.GetFileName(fullFileName);
var PreviewController = UIDocumentInteractionController.FromUrl(
NSUrl.FromFilename(filePath));
PreviewController.Delegate = new UIDocumentInteractionControllerDelegateClass(
UIApplication.SharedApplication.KeyWindow.RootViewController);
Device.BeginInvokeOnMainThread(() =>
{
PreviewController.PresentPreview(true);
});
}
}
public class UIDocumentInteractionControllerDelegateClass
: UIDocumentInteractionControllerDelegate
{
UIViewController ownerVC;
public UIDocumentInteractionControllerDelegateClass(UIViewController vc)
{
ownerVC = vc;
}
public override UIViewController ViewControllerForPreview(
UIDocumentInteractionController controller)
{
return ownerVC;
}
public override UIView ViewForPreview(UIDocumentInteractionController controller)
{
return ownerVC.View;
}
}
}
在IOS專案內的「PublicFileSystem.cs」檔:
using System;
using PCLStorage;
using Pdftst.iOS.Services;
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace Pdftst.iOS.Services
{
public class PublicFileSystem : IPublicFileSystem
{
public IFolder PublicDownloadFolder
{
get
{
var localAppData = Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments);
return new FileSystemFolder(localAppData);
}
}
}
}
主程式
上述步驟都準備好後,
即可在Button_Clicked事件處理函式中透過DependencyServic方式,
處理各平台的檔案存取以顯示出PDF檔案。
MainPage.xaml.cs檔:
using PCLStorage;
using System;
using System.Diagnostics;
using System.Net.Http;
using Xamarin.Forms;
namespace Pdftst
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void Button_Clicked(object sender, EventArgs e)
{
await Task.Run(()=> ShowPdf());
}
private async Task ShowPdf()
{
string filename = "csharp_tutorial.pdf";
string url = "http://PHP伺服器IP/tst/csharp_tutorial.pdf";
var publicFileSystem = DependencyService.Get<IPublicFileSystem>();
var rootFolder = publicFileSystem.PublicDownloadFolder;
try
{
var file = await rootFolder.CreateFileAsync(filename,
CreationCollisionOption.OpenIfExists);
using (var fileStream = await file.OpenAsync(FileAccess.ReadAndWrite))
using (var client = new HttpClient(new HttpClientHandler()))
using (var stream = await client.GetStreamAsync(url))
{
stream.CopyTo(fileStream);
}
var openFileByName = DependencyService.Get<IOpenFileByName>();
openFileByName.OpenFile(file.Path);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
}
Android專案設定
開啟Android專案的屬性頁面,
選擇「Android Manifest」,
找到「Required permission」,
勾選「INTERNET」和「READ_EXTERNAL_STORAGE」。
完成。
問題1
在使用實體手機測試時,
在VS的Output視窗出現「Couldn't connect to logcat, GetProcessId returned: 0」錯誤訊息,
經查詢解決方式如下:
1.在Android專案的屬性內的「Use Fast Deployment (debug mode only)」取消勾選。
2.Android Options的下方Advanced內的「Supported architectures」全部勾選。
問題2
在Android手機上測試時Visual Studio的Output視窗出現下列錯誤訊息:
「Access to the path "/storage/emulated/0/Download/csharp_tutorial.pdf.pdf" is denied」
代表所使用的Android SDK版本號大於24,必須取得手機的權限才可以開啟pdf檔。
解決步驟如下:
1. 用NuGet安裝「Plugin.Permissions」套件在全部的平台專案上。
2. 在Android專案的Properties的「AndroidManifest.xml」檔案內的「application」標籤內,
加入下列程式碼:
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
3. 在Android專案內的「Resources」資料夾內加入「xml」資料夾,
在「xml」資料夾內加入「file_paths.xml」檔案。
「file_paths.xml」內容如下:
<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="Download" path="Download/" />
</paths>
4. 在Android專案內的「MainActivity.cs」檔案修改如下:
using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Plugin.Permissions;
using Plugin.CurrentActivity;
namespace Pdftst.Droid
{
[Activity(Label = "Pdftst", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
protected override void OnCreate(Bundle bundle)
{
CrossCurrentActivity.Current.Init(this, bundle);
base.OnCreate(bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
到此完成修改。
原理可看最上面的參考連結。