這是一份 Xamarin.Forms 高等進階開發技術筆記,我們在這裡將會針對如何設計一個 Xamarin.Forms 應用專案,可以下載網路上的圖片、音樂、影片、文件檔案(pdf, ppt, doc, xls等等)到手機上,並且使用手機上預設安裝的 App,來開啟這些已經下載的檔案。
想要完成這樣的需求程式設計,您需要了解如何在 Xamarin.Forms 使用底下的技術:
MVVM開發框架,在這裡,我們使用 Prism 作為我們開發與設計 MVVM 架構的框架
安裝與使用 HttpClient 來下載網路資源
使用 using 來操作 IDisposable 實作類別物件
使用 PCLStorage 套件,將剛剛下載的網路資源,儲存到本機裝置記憶卡內
可以指定儲存檔案的地方,不是該應用程式的沙箱,而是可以由使用者自行存取的目錄下。
使用相依性服務注入技巧,注入可以開啟該檔案的物件與使用其相關方法
測試頁面
在這裡,我們設計一個頁面,在這個頁面上,僅有兩個控制項,一個是 Picker 另外一個是 Button;使用者可以透過 Picker 選擇要下載檔案的類型,接著按下按鈕,就會開始進行下載該檔案與開啟該檔案。
底下是這個測試頁面的 XAML 內容
<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
prism:ViewModelLocator.AutowireViewModel="True"
x:Class="XFFileDownload.Views.MainPage"
Title="MainPage">
<Grid
BackgroundColor="White">
<StackLayout
HorizontalOptions="Center" VerticalOptions="Center">
<Picker
ItemsSource="{Binding FileSourceTypes}"
SelectedItem="{Binding FileSourceTypeSelect}"
/>
<Button Text="下載檔案"
Command="{Binding DownloadCommand}"/>
</StackLayout>
<BoxView
Color="Gray"
IsVisible="{Binding ShowMask}"
/>
</Grid>
</ContentPage>
接下來,我們來看看這個頁面 ViewModel 的程式碼
首先,我們在 OnNavigatedTo
事件方法中,進行 Picker 下拉選單資料項目的初始化。
在建構函式內 public MainPageViewModel(INavigationService navigationService, IPublicFileSystem publicFileSystem, IOpenFileByName openFileByName)
,我們將會把我們等下要設計的兩個介面 IPublicFileSystem
與 IOpenFileByName
,在這使用建構函式注入物件的方式,將這兩個介面的原生專案中所實作出來並且產生的物件,注入到這個頁面 ViewModel內,讓相關程式碼可以進行相關操作。
由於 PCLStorage 套件所提供的 FileSystem
類別,僅會提供該應用程式沙箱可以使用到的資料夾,因此,我們將會透過 _PublicFileSystem.PublicDownloadFolder
來取得指向該原生平台下的可以公開存取的 IFolder
資料夾。
我們使用 IFile
物件, file ,建立一個檔案,並且使用 file.OpenAsync(FileAccess.ReadAndWrite)
取得可以讀寫的 stream 物件。
此時,我們將會建立 HttpClient
物件,client,使用 client.GetStreamAsync(url)
方法,取得這個網路資源的 stream 物件,並且使用敘述 fooStream.CopyTo(fooFileStream);
將從網路取得的檔案內容,複製到本機檔案中;若您取得網路檔案的方法需要使用 Post 的方式來取得,您也是使用同樣的方式,取得 HttpResponseMessage.Content.ReadAsStreamAsync
的 stream 物件,就可以進行寫入到本機儲存空間內。
在上述的檔案與目錄操作的程式碼,我們都是使用 PCLStorage 套件所提供的類別與相關方法。
最後,我們使用 _OpenFileByName.OpenFile(file.Path);
將這個檔案,使用安裝在手機中的應用程式來開啟。
public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
{
public event PropertyChangedEventHandler PropertyChanged;
private readonly INavigationService _navigationService;
public ObservableCollection<string> FileSourceTypes { get; set; } = new ObservableCollection<string>();
public string FileSourceTypeSelect { get; set; }
public bool ShowMask { get; set; } = false;
public DelegateCommand DownloadCommand { get; set; }
IPublicFileSystem _PublicFileSystem;
IOpenFileByName _OpenFileByName;
public MainPageViewModel(INavigationService navigationService,
IPublicFileSystem publicFileSystem, IOpenFileByName openFileByName)
{
_navigationService = navigationService;
// 注入各平台的非應用程式專屬的沙箱資料夾
_PublicFileSystem = publicFileSystem;
// 使用手機內安裝的App,開啟指定的檔案
_OpenFileByName = openFileByName;
DownloadCommand = new DelegateCommand(async () =>
{
ShowMask = true;
#region 依據所選擇的項目,設定下載來源與檔案名稱
string filename = "";
string url = "";
if (FileSourceTypeSelect.ToLower() == "pdf")
{
filename = "vulcan.pdf";
url = "https://www.tutorialspoint.com/csharp/csharp_tutorial.pdf";
}
else if (FileSourceTypeSelect.ToLower() == "image")
{
filename = "vulcan.png";
url = "https://pluralsight.imgix.net/paths/path-icons/csharp-e7b8fcd4ce.png";
}
else if (FileSourceTypeSelect.ToLower() == "mp3")
{
filename = "vulcan.mp3";
url = "http://video.ch9.ms/ch9/4855/ca67b144-e675-48a2-a0f2-706af9644855/DataTemplateSelector.mp3";
}
else if (FileSourceTypeSelect.ToLower() == "video")
{
filename = "vulcan.mp4";
url = "http://video.ch9.ms/ch9/4855/ca67b144-e675-48a2-a0f2-706af9644855/DataTemplateSelector.mp4";
}
else if (FileSourceTypeSelect.ToLower() == "ppt")
{
filename = "vulcan.ppt";
url = "http://people.csail.mit.edu/mrub/talks/small_world/Seminar07_rubinstein.ppt";
}
else if (FileSourceTypeSelect.ToLower() == "doc")
{
filename = "vulcan.doc";
url = "http://im2.nhu.edu.tw/download.php?filename=270_2af7568a.doc&dir=personal_subject/&title=C%23-%E7%AC%AC%E4%B8%80%E7%AB%A0";
}
#endregion
// 取得要存放該檔案的資料夾
// FileSystem 為 PCLStorage 提供的應用程式沙箱的相關資料夾
IFolder rootFolder = _PublicFileSystem.PublicDownloadFolder;
try
{
// 建立這個檔案
IFile file = await rootFolder.CreateFileAsync(filename,
CreationCollisionOption.OpenIfExists);
// 取得這個檔案的 Stream 物件
using (var fooFileStream = await file.OpenAsync(FileAccess.ReadAndWrite))
{
using (HttpClientHandler handle = new HttpClientHandler())
{
// 建立 HttpClient 物件
using (HttpClient client = new HttpClient(handle))
{
// 取得指定 URL 的 Stream
using (var fooStream = await client.GetStreamAsync(url))
{
// 將網路的檔案 Stream 複製到本機檔案上
fooStream.CopyTo(fooFileStream);
}
}
}
}
_OpenFileByName.OpenFile(file.Path);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
ShowMask = false;
});
}
public void OnNavigatedFrom(NavigationParameters parameters)
{
}
public void OnNavigatingTo(NavigationParameters parameters)
{
}
public void OnNavigatedTo(NavigationParameters parameters)
{
FileSourceTypes.Clear();
FileSourceTypes.Add("PDF");
FileSourceTypes.Add("Image");
FileSourceTypes.Add("MP3");
FileSourceTypes.Add("Video");
FileSourceTypes.Add("PPT");
FileSourceTypes.Add("Doc");
}
}
取得公開資料夾的介面與實作
首先,我們在 PCL 專案內,建立 IPublicFileSystem
這個介面,這個介面內僅有四個屬性需要在原生平台內實作出來。
public interface IPublicFileSystem
{
IFolder PublicDownloadFolder { get; }
IFolder PublicPictureFolder { get; }
IFolder PublicMovieFolder { get; }
IFolder PublicDCIMFolder { get; }
}
在 Android 平台下,我們實作 IPublicFileSystem
介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
來宣告這個介面實作,可以用於相依性服務注入之用。
我們在這個介面實作中,取得個公開資料夾的絕對路徑,並且建立 PCLStorage 套件的 FileSystemFolder
類別物件,這就是我們要使用的 IFolder
實作物件。
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace XFFileDownload.Droid.Services
{
class PublicFileSystem : IPublicFileSystem
{
public IFolder PublicDownloadFolder
{
get
{
var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicPictureFolder
{
get
{
var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicMovieFolder
{
get
{
var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryMovies).AbsolutePath;
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicDCIMFolder
{
get
{
var localAppData = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDcim).AbsolutePath;
return new FileSystemFolder(localAppData);
}
}
}
}
在 iOS 平台下,我們實作 IPublicFileSystem
介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
來宣告這個介面實作,可以用於相依性服務注入之用。
我們在這個介面實作中,取得個公開資料夾的絕對路徑,並且建立 PCLStorage 套件的 FileSystemFolder
類別物件,這就是我們要使用的 IFolder
實作物件。
[assembly: Xamarin.Forms.Dependency(typeof(PublicFileSystem))]
namespace XFFileDownload.iOS.Services
{
class PublicFileSystem : IPublicFileSystem
{
public IFolder PublicDownloadFolder
{
get
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicPictureFolder
{
get
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicMovieFolder
{
get
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return new FileSystemFolder(localAppData);
}
}
public IFolder PublicDCIMFolder
{
get
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
return new FileSystemFolder(localAppData);
}
}
}
}
取得公開資料夾的介面與實作
首先,我們在 PCL 專案內,建立 IOpenFileByName
這個介面,這個介面內僅有一個方法需要在原生平台內實作出來。
public interface IOpenFileByName
{
void OpenFile(string fullFileName);
}
在 Android 平台下,我們實作 IOpenFileByName
介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
來宣告這個介面實作,可以用於相依性服務注入之用。
這個實作方法,首先,將這個檔案,複製到公開的資料夾內,也就是說,您下載的檔案,是可以儲存在應用程式沙箱資料夾內,也是可以正常使用手機內安裝的應用程式來開啟這個檔案的。
最後,我們使用 Intent intent = new Intent(Intent.ActionView);
來讓使用者選擇適合的應用程式,來開啟這個檔案。
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace XFFileDownload.Droid.Services
{
public class OpenFileByName : IOpenFileByName
{
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 = "";
string extension = Path.GetExtension(filePath);
// get mimeTye
switch (extension.ToLower())
{
case ".txt":
application = "text/plain";
break;
case ".doc":
case ".docx":
application = "application/msword";
break;
case ".pdf":
application = "application/pdf";
break;
case ".xls":
case ".xlsx":
application = "application/vnd.ms-excel";
break;
case ".jpg":
case ".jpeg":
case ".png":
application = "image/jpeg";
break;
default:
application = "*/*";
break;
}
//Android.Net.Uri uri = Android.Net.Uri.Parse("file://" + filePath);
Android.Net.Uri uri = Android.Net.Uri.FromFile(file);
Intent intent = new Intent(Intent.ActionView);
intent.SetDataAndType(uri, application);
intent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask);
Forms.Context.StartActivity(intent);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
在 iOS 平台下,我們實作 IOpenFileByName
介面,並且記得要使用 [assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
來宣告這個介面實作,可以用於相依性服務注入之用。
我們建立一個類別 UIDocumentInteractionControllerDelegateClass
,用來顯示這個檔案。
[assembly: Xamarin.Forms.Dependency(typeof(OpenFileByName))]
namespace XFFileDownload.iOS.Services
{
public class OpenFileByName : IOpenFileByName
{
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;
}
}
}