XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2017/11/15

Xamarin.Forms / .NET Standard 體驗之旅 2 : 使用 Acr.UserDialogs 元件

由於 Prism Template Pack 已經開始支援創建出使用 .NET Standard 的共用類別庫專案,在接下來的幾篇文章,我將會針對在開發 Xamarin.Forms 專案的時候,會使用到不同需求功能的 NuGet 套件,進行測試與說明。
首先,我先針對在進行 Xamarin.Forms 專案開發時候,必備擴充 NuGet 套件 Acr.UserDialogs 進行測試,當然,在使用任何套件之前,強烈建議一定要先觀看官方的使用說明文件,因為,大部分的 Xamarin.Forms 擴充套件,都須做些適度的初始化設定工作。

測試範例專案原始碼

這篇文章中的測試範例專案原始碼,您可以從這裡取得

安裝最新的 Prism Template Pack

首先,請先確認您有安裝或者更新到最新的 Prism Template Pack 這個 Visual Studio 擴充功能,您可以點選 Visual Studio 2017 功能表的 [工具] > [擴充功能與更新] 選項,看到底下 [擴充功能與更新] 對話視窗,確認有安裝這個擴充 Prism Template Pack 功能。
今天有更新到 2.0.7 版本,所以,我們將會使用今天更新的最新 Prism Template Pack 擴充功能套件。
Prism Template Pack 2.0.7

開始建立專案

請點選功能表 [檔案] > [新增] > [專案],此時,您會看到底下的 [新增專案] 對話視窗。
我們點選 Prism Template Pack 提供的 Xamarin.Forms 專用的專案樣板,請點選 [已安裝] > [Visual C#] > [Prism] > [Prism Blank App (Xamarin.Forms)]。
接著在名稱欄位輸入該專案名稱後,點選 [確定] 按鈕,以建立此練習專案。
.NET Standard Xamarin.Forms Project
然後,在 [PRISM PROJEC WIZAD] 對話窗中,選擇您要跨平台的作業系統,容器 (Container) 這裡,我個人喜歡與習慣使用 Prism,您可以選擇您自己要用的容器,最後,點選 [CREATE PROJECT] 按鈕。
.NET Standard Xamarin.Forms Project

安裝 Acr.UserDialogs NuGet 套件

在這裡,請使用滑鼠右建,點選剛剛建立好的方案節點,選擇 [管理方案的 NuGet 套件] 項目。
在 [瀏覽] 標籤頁次上,輸入 Acr.UserDialogs 這個關鍵字,搜尋出這個套件。
從下圖中,我們看到搜尋出兩個 Acr.UserDialogs 相關套件,我們點選第一個,並且在右方區域,勾選 [NETStdUserDialogs] 這個 .NET Standard 專案(您可以不用勾選原生專案)
.NET Standard Xamarin.Forms Project
我們可以查看到這個 Acr.UserDialogs 套件,已經有支援 .NET Standard,也就是說,原則上,您所使用的 NuGet 套件,最好是能夠有支援 .NET Sandard 平台,使用上較不會遇到一些問題,若該套件還是僅支援 PCL ,這裡還是建議您先建立一個測試專案,測試在 .NET Standard 環境下使用是否有遇到任何問題。
.NET Standard Xamarin.Forms Project
當我們點選 [安裝] 按鈕之後,會看到 Visual Studio 會提示您這樣會安裝那些相依性的套件。
.NET Standard Xamarin.Forms Project

修正 View / ViewModel 開始使用 Acr.UserDialogs

安裝好套件之後,讓我們打開 MainPage.xaml 檔案,修改XAML內容。
這裡,我們僅在 StackLayout 版片配置內,加入一個按鈕項目,並且設定 Command 屬性,要綁定 ViewModel 內類型為 DelegateCommand 的物件 LoadCommand
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NETStdUserDialogs.Views.MainPage"
             Title="{Binding Title}">

    <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
        <Label Text="Welcome to Xamarin Forms and Prism!" />
        <Button Text="Load" Command="{Binding LoadCommand}"/>
    </StackLayout>

</ContentPage>
接著我們打開 MainPageViewModel.cs 檔案,修正 ViewModel 的商業邏輯程式碼。
我們透過了 UserDialogs.Instance 這個物件,取得現在可用的 UserDialogs 物件,並且要顯示出 Loading 這個對話窗,我們會暫停兩秒鐘之後,這個對話窗便會消失掉。
namespace NETStdUserDialogs.ViewModels
{
    public class MainPageViewModel : ViewModelBase
    {
        public DelegateCommand LoadCommand { get; set; }
        public MainPageViewModel(INavigationService navigationService) 
            : base (navigationService)
        {
            Title = "Main Page";
            LoadCommand = new DelegateCommand(async () =>
            {
                using (var dlg = UserDialogs.Instance.Loading("Test Progress"))
                {
                    await Task.Delay(2000);
                }
            });
        }
    }
}

修正 Android 專案,進行 UserDialogs 的初始化

如同前面強調的,使用 Xamarin.Forms 第三方套件的時候,務必要觀看使用說明,所以在 Acr.UserDialogs網頁中,有提到,我們需要在 Android 專案內進行初始化設定。
在 Android 專案內,找到 MainActivity.cs 檔案,打開這個檔案。
請在 [OnCreate] 方法內,加入這行 UserDialogs.Init(this); 敘述
protected override void OnCreate(Bundle bundle)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(bundle);

    UserDialogs.Init(this);

    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App(new AndroidInitializer()));
}

進行測試

最後,設定預設起始專案為 Android 專案,並且選擇適當的模擬器或者實體手機,開始進行測試。
使用 Acr.UserDialogs 相關對話窗的好處就是,如同底下測試結果,您會看到的是,這些對話窗將會是全螢幕顯示的。
Acr.UserDialogs 執行結果 Acr.UserDialogs 執行結果

2017/11/13

Xamarin.Forms 用 MVVM 開發的好處? Prism 和 cross-platform 開發(優劣)差異? (我發現寫在 ViewModel 內 程式碼要多好多)

剛剛收到一位學員提問的問題 XF用mvvm開發的好處? prism 和 cross-platform 開發(優劣)差異? (我發現寫在viewmodel 內 程式碼要多好多)
這個問題有相當多的 Xamarin.Forms 開發者都會有這樣的問題,我先簡單地描述問題中的各個技術名詞
在 Xamarin.Forms 中,所有的 UI 可以透過 XAML 宣告標記語言來描述出來;而在 XAML 定義 UI的過程中,我們是無法直接在 XAML 中撰寫商業邏輯到這個語言中(您也可以選擇不用 XAML,直接使用 C#來定義UI)。想要在每個頁面撰寫出各個商業邏輯處理程序與方法,您有兩種選擇 Code Behind / MVVM (Model View ViewModel)
而 Code Behind 的開發方式,則是非常舊的開發技術,那是之前在 Windows Forms 那個時候的開發技術採用的,它個優點是簡單、方便,缺點是它的頁面與商業邏輯程式代碼綁在一起,想要共用或分享上,會有些困難。
在進行 XAML 這樣 UI App 開發的時候(如 Xamarin.Forms),我們都會採用 MVVM 的開發方式 (若您在進行原生 Android / iOS 專案開發的時候,也是可以使用 MVVM 的開發方式,而不是使用這兩個原生平台提供的 MVC 開發方式)。
採用 MVVM 的開發方式,可以讓您的 XAML 頁面(View) 與該頁面的商業處理邏輯( ViewModel )徹底切隔開來,具有關注點分離的好處;也就是說,不會因為您變更了 XAML 內容,就導致 App 無法正常運作,反之亦然。因為商業邏輯都放在 ViewModel 內(與 UI 毫無關係),因此,您可以在這裡進行自動單元測試,確保 App 品質。
Prism 是一個開發框架,它幫助你建立一個具有相依性注入容器的 Bootstrap 相關程式碼,事先幫您建立好,讓您可以輕鬆的使用這些功能:ViewModel 自動綁定、導航服務、XAML行為擴充功能、事件聚合器、相依性管理服務、頁面對話窗等等(若您有使用過 Code Behind來開發過 App,而您又使用過 Prism 這些功能,您將會愛上 Prism 的這些功能所帶來的好處)。
採用預設專案樣板的 cross-platform 開發,您也是可以選擇搭配 MVVM 的開發方式或者使用 Code Behind 的開發方式;可是對於許多再進行 MVVM 開發方式的時候,會用到的許多需求,您都需要自行擴充與設計,例如 INotifyPropertyChanged INPC 這樣的機制,你需要寫出一個這樣的基底類別或者在每個 ViewModel 實作出這個介面等等。
當然,不論您使用 Prism 或者是 cross-platform 開發方式,您也可以全部都使用 Code Behind 開發您的 Xamarin.Forms App(完全不使用 MVVM 方式,使用事件綁定的方式),也有人喜歡使用 C# 來寫出整個 Xamarin.Forms App (也就是不使用任何 XAML 標記宣告語言),這些開發技術都是可以選擇的。不過,在使用 Code Behind 開發的時候,您需要去處理一些其他的問題,才能夠讓您的 App 開發上更加靈活。
還有一點我建議採用 MVVM 架構開發方式的理由是,當您遇到問題,而在網路上尋找問題解答的時候,大部分都會提供 MVVM 的解決方案(當然也會有 Code Behind的解決方式),不過,網路上相當多的 Xamarin.Forms 高手,都是採用 MVVM + Prism 的方式開發,對於這些高手寫的文章與提出的解決方案,對於同樣採用這樣開發技術的人,是更有加分的好處。
最後,青菜蘿蔔各有所好,使用您覺得好的開發方式,黑貓白貓,會抓老鼠的就是好貓,只要能夠開發出您要的 App 的技術,就是您要的技術。(若有遇到問題且專案全部都使用 Code Behind 開發,很抱歉,我無法對您提供任何有用的幫助)
若您一定要問我,Vulcan 您會選擇哪個方式,我會選擇 Prism + MVVM

2017/11/12

Xamarin.Forms / .NET Standard 體驗之旅 1 : 使用 Prism Template Pack 2.0 與 .NET Stardard 標準類別庫之建立 Xamarin.Forms App 搶先體驗

在今天,2017/11/12,Prism Template Pack 推出 2.0 版本,在這個新的版本所建立出來的 Xamarin.Forms 專案,將會使用 .NET Standard 標準類別庫來取代原先使用的 PCL Portable Class Library 可攜式類別庫,可是,當替換掉舊有的PCL類別庫,對於我們在開發 Xamarin.Forms 專案上會有甚麼新問題會發生呢?
在這裡,我們將實際使用 Prism Template Pack 2.0 的專案樣板,實際建立一個 Xamarin.Forms 專案,並且在這個專案中實作出各種頁面導航的應用,看看會有甚麼樣的變化。

升級 Prism Template Pack

首先,我們先要把您的 Visual Studio 2017 的 Prism Template Pack 擴充功能進行升級到 2.0 版本。
在這裡要特別注意, Prism Template Pack 2.0 版本將會僅僅支援 Visual Studio 2017,若您使用別的 Visual Studio 版本,您可以忽略掉這篇文章。
請確認您的 Prism Template Pack 已經升級了2.0版本
Prism Template Pack 2.0

使用 Prism Template Pack 2.0 建立 Xamarin.Forms 專案

  • 打開 Visual Studio 2017
  • 從功能表中選取 [檔案] > [新增] > [專案]
  • 在 [新增專案] 對話窗中,選擇 [已安裝] > [Visual C#] > [Prism] > [Prism Blank App (Xamarin.Forms)]
  • 在名稱欄位輸入 MyPrismPack2
    Prism Template Pack 2.0 New Project
  • 在 [PRISM PROJECT WIZARD] 對話窗中,勾選您要建立的跨平台類型,接著在 [Container] 容器下拉選單中,選擇 [Unity] 這個項目,最後,點選 [CREATE PROJECT]
    Prism 支援不同的 DI 套件,因此,您可以選擇您喜歡與常用的相依性注入套件名稱。
    Prism Template Pack 2.0 New Project
  • 稍微等候一段時間,這個 Xamarin 專案將會建立成功
  • 首先,我們來查看 MyPrismPack2 這個專案的屬性,使用滑鼠右擊這個專案,選擇 [屬性] 選項,您會看到類似下圖的畫面,表示這個專案真的是使用 .NET Standard 2.0 作為目標 Framework
    查看專案屬性
    同時,您也會從方案總管中看到了 MyProjectPack2 專案的相依性節點,有看到黃色警告圖示,而您可以從 Visual Studio 2017 [錯誤清單] 視窗中看到這兩個錯誤訊息,不過,在這裡我們可以暫時忽略掉這兩個警告訊息,因為,在您開發 Xamarin.Forms 跨平台專案的過程中,這兩個警告訊息,是不會對您造成任何問題的。
    一些警告訊息
NU1701    已使用 '.NETFramework,Version=v4.6.1' 而非專案目標架構 '.NETStandard,Version=v2.0' 還原了套件 'CommonServiceLocator 1.3.0'。此套件與您的專案並非完全相容。    MyPrismPack2    D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2\MyPrismPack2.csproj        

NU1701    已使用 '.NETFramework,Version=v4.6.1' 而非專案目標架構 '.NETStandard,Version=v2.0' 還原了套件 'Unity 4.0.1'。此套件與您的專案並非完全相容。    MyPrismPack2    D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2\MyPrismPack2.csproj

建置與測試

  • 滑鼠右擊專案節點 MyPrismPack2.Android,選擇 [建置] 選項,若建置成功,將會看到底下訊息輸出。
正在還原 NuGet 封裝...
為避免 NuGet 在建置期間還原封裝,請開啟 [Visual Studio 選項] 對話方塊,並按一下 [封裝管理員] 節點,然後取消核取 [允許 NuGet 在建置期間下載遺漏的封裝]。
1>------ 已開始建置: 專案: MyPrismPack2, 組態: Debug Any CPU ------
1>D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2\MyPrismPack2.csproj : warning NU1701: 已使用 '.NETFramework,Version=v4.6.1' 而非專案目標架構 '.NETStandard,Version=v2.0' 還原了套件 'CommonServiceLocator 1.3.0'。此套件與您的專案並非完全相容。
1>D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2\MyPrismPack2.csproj : warning NU1701: 已使用 '.NETFramework,Version=v4.6.1' 而非專案目標架構 '.NETStandard,Version=v2.0' 還原了套件 'Unity 4.0.1'。此套件與您的專案並非完全相容。
1>MyPrismPack2 -> D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2\bin\Debug\netstandard2.0\MyPrismPack2.dll
1>專案 "MyPrismPack2.csproj" 建置完成。
2>------ 已開始建置: 專案: MyPrismPack2.Android, 組態: Debug Any CPU ------
2>  MyPrismPack2.Android -> D:\Vulcan\Projects\MyPrismPack2\MyPrismPack2\MyPrismPack2.Android\bin\Debug\MyPrismPack2.Android.dll
2>  沒有辦法解決 "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 和 "mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" 之間的衝突。任意選擇 "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"。
========== 建置: 2 成功、0 失敗、0 最新、0 略過 ==========
  • 設定這個 MyPrismPack2.Android 專案為預設起始專案,並且開始執行這個專案,我們將會看到這個專案是可以順利執行的。
    .NET Standard 之 Xamarin.Android 執行結果

進行建立其他功能

接下來,我們會在這個專案建立兩個頁面,一個是 ListPage 另外一個是 Detail Page,我們將會模擬一個清單頁面 CRUD 簡單應用,看看這樣我們平常在實作開發 Xamarin.Forms 專案功能,在現在更換成 .NET Standard 2.0 之後,會不會有甚麼變化?
採用 .NET Standard 之 Xamarin.Android
採用 .NET Standard 之 Xamarin.Android
採用 .NET Standard 之 Xamarin.Android

使用.NET Standard開發 Xamarin.Forms App的測試過程

使用 Prism Template Pack 2.0 的心得

  • 由於 Prism Template Pack 2.0 擴充功能僅能夠在 Visual Studio 2017 上安裝,因此,想要使用這個功能的,必須要使用 Visual Studio 2017
  • Prism Template Pack 2.0 的共用類別庫已經轉換成為 .NET Standard 標準類別庫,因此,對於之前在 PCL 上可以使用的相關第三方套件,強烈建議您需要先行做些測試,確保這些尚未支援 .NET Standard 的第三方套件,可以正常於這個開發環境中運行
  • 這個測試範例雖然不是有很多功能,但是,這裡包含了這些功能在 .NET Standard 下是否可以正常運作的測試。
    • Prims 開發框架的各項功能
    • MVVM 的資料綁定與開發方式
    • Fody PropertyChanged 的 NuGet 套件可以正常使用
    • 頁面導航的各項操作:切換頁面、傳遞參數

2017/10/25

Xamarin.Forms 的 ListView 之延遲載入(Lazy Loading) 之設計方法

有些時候,當使用者要查詢後端資料庫的資料時候,因為所設定的查詢條件過於寬鬆,將會導致會從後端資料一次性的接收到成千上萬筆的資料,若您讓手機應用程式來接收這麼多的資料,將會有嚴重的效能上的問題出現,例如,要從網路一次讀取到這麼多的資料,會需要花點時間;要把這麼多的紀錄加入到 ListView 中,也有可能造成記憶體不足或者ListView操作延緩的現象產生。
在這個時候,我們就可以使用延遲載入 Lazy Loading 這樣的設計方法來解決此一問題。他的處理方式為,每次呼叫 Web API 的時候,後端 Web API 僅僅會回傳一定數量的紀錄(例如:最多 100 筆),當使用者捲動 ListView 的清單到最後一筆紀錄的時候,您的 Xamarin.Forms 程式,就會再度呼叫 Web API 服務,請求回傳接下來一定數量的紀錄(例如:101~200筆),並且把這些新讀取到的紀錄,顯示到 ListView 中。
想要做到這樣的功能,ListView 有提供兩個事件,ItemAppearing / ItemDisappearing 這兩個事件,可以供我們做出這樣的效果;在我們接下來的範例中,將會展示出這樣的設計方法。
我們透過了 XAML 的行為 Behavior (這裡的行為擴充,我們將會使用 Prism 所提供的擴充行為功能),設定當 ItemAppearing 事件發生的時候,會執行所對應的指定的 ViewModel 內的命令 ItemAppearingCommand,而我們在 ViewModel 內,將會設計,當這個命令 ItemAppearingCommand 被觸發的時候,並且現在所顯示的紀錄是全部紀錄的最後一筆,將會進行載入更多的紀錄到 ListView 內(這裡我們將會呼叫 Reload(fooLast.ID+1) 方法來達成)。
因此,在這個範例中,也充分展示出,如何不透過後置碼 Code Behind 的設計技術,也可以在 ViewModel 內,處理相關事件發生要處理的需求。
當然,為了簡化這個範例專案的複雜程度,我們要讀取的集合資料,並不會實際從網路呼叫 Web API 來取得,而是直接產生靜態的集合資料(若您要設計每次都從網路讀取資料,請記得要設計相關的 UX,告知使用者,現在這樣更新資料中,要不然,當在讀取更多資料的時候,手機應用程式,當時會卡卡的)。
底下是我們這個測試頁面 View 的 XAML 標記語言
我們首先宣告了一個 Prism Behavior 的命名空間 xmlns:behavior="clr-namespace:Prism.Behaviors; 接著在 ListView 內,加入一個行為設計,EventToCommandBehavior,並且指定當 ItemAppearing 事件發生的時候,就會需要執行 ViewModel 內的 ItemAppearingCommand 命令;另外,在這裡,我們使用了 EventArgsParameterPath 這個屬性,設定當觸發這個 ViewModel 內的命令時候,需要將是事件中的這個參數,傳入到命令中,這樣,我們才可以在該命令中,知道當時正在顯示的紀錄是哪一筆。
<?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"
             prism:ViewModelLocator.AutowireViewModel="True"
             xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
             x:Class="XFListViewLazy.Views.MainPage"
             Title="MainPage">
    <Grid
      >
        <ListView
            ItemsSource="{Binding MyDatas}"
            SelectedItem="{Binding SelectedMyData}"
            HasUnevenRows="True"
            >
            <ListView.Behaviors>
                <behavior:EventToCommandBehavior
                    EventName="ItemAppearing"
                    Command="{Binding ItemAppearingCommand}"
                    EventArgsParameterPath="Item"/>
            </ListView.Behaviors>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout>
                            <Label Text="{Binding ID}"/>
                            <Label Text="{Binding Name}" FontSize="30"/>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</ContentPage>
底下是上面 View 的相對應 ViewModel
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Xamarin.Forms;
using XFListViewLazy.Models;
using XFListViewLazy.Repositories;

namespace XFListViewLazy.ViewModels
{

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;


        public ObservableCollection<MyModel> MyDatas { get; set; } = new ObservableCollection<MyModel>();
        public MyModel SelectedMyData { get; set; }

        private readonly INavigationService _navigationService;
        public MyRepository _myRepository { get; set; }

        public DelegateCommand<MyModel> ItemAppearingCommand { get; set; }

        public MainPageViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;
            _myRepository = MyRepository.GetInstance();

            ItemAppearingCommand = new DelegateCommand<MyModel>((x) =>
            {
                var fooLast = MyDatas.Last();
                if (x.ID == fooLast.ID)
                {
                    Reload(fooLast.ID+1);
                }
            });
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            Reload(0);
        }

        public void Reload(int last)
        {
            var foo = _myRepository.GetNext(last);
            foreach (var item in foo)
            {
                MyDatas.Add(item);
            }
        }

    }

}

2017/10/07

Xamarin.Forms - 從網路下載檔案,並且儲存到手機中,接著,可以使用安裝在手機內的應用程式,開啟這個檔案

這是一份 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 實作物件。
有關 Android 平台下關於這些資料夾的說明,請參考 https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOWNLOADS
[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 實作物件。
有關 iOS 平台下關於這些資料夾的說明,請參考 https://developer.xamarin.com/guides/ios/application_fundamentals/working_with_the_file_system/
[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);

            //Device.OpenUri(new Uri(filePath));
            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;
        }
    }
}