[C#][Xamarin Form][PHP]下載PDF檔並在手機上顯示

  • 972
  • 0
  • 2018-06-25

使用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);
     }

   }
}

到此完成修改。
原理可看最上面的參考連結。