XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2016/07/14

Xamarin.Forms 檔案存取與拍照

檔案存取與拍照

在這個範例專案中,將會練習兩個在行動裝置開發過程中,相當重要的功能,那就是 檔案存取 與 行動裝置的專屬功能呼叫,在這裡將會展示如何在核心PCL專案內,呼叫個專屬平台的拍照功能,並且將拍照完成的照片回傳並顯示到行動裝置螢幕上。
由於每個行動裝置平台對於檔案存取與拍照功能的呼叫與使用方式都不盡相同,原則上,您可以透過相依服務注入(前面有提過的 DependencyService),此時,您需要定義一個服務介面,並且在各個平台實作出這些服務功能,讓核心PCL的類別可以呼叫。由於 Xamarin.Forms 提供了許多插件(Plugin),因此,可以簡化許多開發上的需求。
因此,可以從 Components / Xamarin https://components.xamarin.com/ 找到這些插件來使用,這裡,將會使用到 Media Plugin 與 PCL Storage 這兩個插件。

建立標籤式的樣板式頁面方案

  1. 首先,開啟您的 Visual Studio 2015
  2. 接著透過 Visual Studio 2015 功能表,選擇這些項目 檔案 > 新增 > 專案 準備新增一個專案。
  3. 接著,Visual Studio 2015 會顯示 新增專案 對話窗,請在這個對話窗上,進行選擇 Visual C# >Cross-Platform > Blank Xaml App (Xamarin.Forms Portable)
  4. 接著,在最下方的 名稱 文字輸入盒處,輸入 XFFilePhoto 這個名稱,最後使用滑鼠右擊右下方的 確定 按鈕。
  5. 當專案建立到一半,若您的開發環境還沒有建置與設定完成 Mac 電腦與 Xamarin Studio for Mac 系統,此時會看到 Xamarin Mac Agent Instructions 對話窗出現,這個對話窗是要提醒您進行與 Mac 電腦連線設定,這是因為,您無法在 Windows 作業系統進行 iOS 相關應用程式的建立與設計工作,而是需要透過 Mac 電腦安裝的 XCode 來協助您完成這些 iOS 應用程式的建立工作。不過,這不影響您繼續開發 Xamarin.Forms 的應用程式,只不過此時,您無法編譯與執行 iOS 的應用程式。
  6. 接著會看到 新的通用Windows專案 對話視窗,此時,您只需要按下 確定 按鈕即可,此時,專案精靈會繼續完成相關平台的專案建立工作。
  7. 最後,整個新的 Xamarin.Forms 專案就建立完成了。

準備安裝套件

請依照底下說明,將這些插件安裝到方案的所有專案中。

Media Plugin

  • 滑鼠右擊方案節點 XFFilePhoto,接著選擇 管理方案的 NuGet 套件
  • 在 NuGet - 解決方案 子標籤頁次出現後,點選 瀏覽
  • 請在搜尋文字輸入盒內,輸入 Xam.Plugin.Media,接著點選 Xam.Plugin.Media 項目;在右方點選要安裝到全部的專案內,最後,點選 安裝 按鈕
    在方案內加入NuGet套件

PCL Storage

  • 滑鼠右擊方案節點 XFFilePhoto,接著選擇 管理方案的 NuGet 套件
  • 在 NuGet - 解決方案 子標籤頁次出現後,點選 瀏覽
  • 請在搜尋文字輸入盒內,輸入 PCLStorage,接著點選 PCLStorage 項目;在右方點選要安裝到全部的專案內,最後,點選 安裝 按鈕

MainPage

  • 在安裝好所有的擴充插件(Plugins),打開核心PCL 的 MainPage.xaml 檔案,將底下的 XAML 宣告定義複製到這個檔案內。
  • 首先為了避免在 iOS 平台執行時候,最上方的狀態烈被遮蔽掉的問題,因此,透過ContentPage.Padding 來定義整個頁面皆內縮 20dp。
  • 這個頁面的控制項,使用了 Grid 版面配置來進行所有控制項的定位,透過 Grid.Row Grid.Column附加屬性,可以指定控制項要出現在哪個位置上。
  • 這個頁面定義內容相當簡單,有四個 Image 控制項,五個按鈕,其中四個按鈕會啟動行動裝置的拍照功能,進行拍照一張照片,接著,顯示在行動裝置螢幕上;而最後一個按鈕,則是展示了如何使用 PCLStorage 插件所提供的功能,進行檔案存取。

MainPage.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:local="clr-namespace:XFFilePhoto"
             x:Class="XFFilePhoto.MainPage">

  <ContentPage.Padding>20</ContentPage.Padding>

  <Grid

    >
    <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Image x:Name="image1" Grid.Row="0" Grid.Column="0"/>
    <Image x:Name="image2" Grid.Row="0" Grid.Column="1"/>
    <Image x:Name="image3" Grid.Row="2" Grid.Column="0"/>
    <Image x:Name="image4" Grid.Row="2" Grid.Column="1"/>

    <Button x:Name="buttonImage1" Text="拍照" Clicked="OnbuttonImage1" Grid.Row="1" Grid.Column="0" />
    <Button x:Name="buttonImage2" Text="拍照" Clicked="OnbuttonImage2" Grid.Row="1" Grid.Column="1" />
    <Button x:Name="buttonImage3" Text="拍照" Clicked="OnbuttonImage3" Grid.Row="3" Grid.Column="0" />
    <Button x:Name="buttonImage4" Text="拍照" Clicked="OnbuttonImage4" Grid.Row="3" Grid.Column="1" />
    <Button x:Name="buttonFile" Text="檔案存取" Clicked="OnbuttonFile" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" />

  </Grid>

</ContentPage>
  • 請將下列 C# 程式碼複製到 MainPage.xaml.cs內。
  • 在拍照按鈕處理事件中,會呼叫 public async Task 拍照(ImageLocation fooImageLocation = ImageLocation.Location1) 方法,並且傳遞列舉參數,告知是要將拍照圖片顯示在哪個 Image 控制項。
  • 拍照方法,使用了 CrossMedia.Current 取得跨平台的拍照物件,透過 TakePhotoAsync 非同步方法,呼叫當時行動裝置平台的拍照功能,一旦,使用這確定拍照完成,就會將所拍照照片資訊回傳給 file物件;若使用者取消拍照,則 file 物件則會為空值。
    當取得了拍照照片檔案資訊,將會透過 PCLStorage 所提供的功能 TakePhotoAsync IFile 取得可以存取這個照片的內容,接著,將這個照片透過方法 MoveAsync 將其搬移到該應用程式本地資料夾下。
    最後,使用 IFile 提供的方法 OpenAsync 取得對該照片檔案的 Stream,並且產生 ImageSource 物件,接著綁定到指定的 Image 控制項,此時,行動裝置螢幕上,就會顯示出該圖片。
  • 至於 檔案存取 按鈕,則是透過 IFolder IFile 這兩個介面,取得指定檔案的存取權,使用WriteAllTextAsync ReadAllTextAsync 對該檔案進行讀寫動作。由於這個檔案會繼續存在於行動裝置上(除非使用者移除了該應用程式),因此,當下次應用程式再度開啟後,是可以把這些資料重新取回。

MainPage.xaml.cs

using PCLStorage;
using Plugin.Media;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace XFFilePhoto
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        async void OnbuttonImage1(object sender, EventArgs e)
        {
            await 拍照(ImageLocation.Location1);
        }

        async void OnbuttonImage2(object sender, EventArgs e)
        {
            await 拍照(ImageLocation.Location2);
        }

        async void OnbuttonImage3(object sender, EventArgs e)
        {
            await 拍照(ImageLocation.Location3);
        }

        async void OnbuttonImage4(object sender, EventArgs e)
        {
            await 拍照(ImageLocation.Location4);
        }

        async void OnbuttonFile(object sender, EventArgs e)
        {
            IFolder rootFolder = FileSystem.Current.LocalStorage;
            IFolder sourceFolder = await FileSystem.Current.LocalStorage.CreateFolderAsync("MyDatas", CreationCollisionOption.ReplaceExisting);
            IFile sourceFile = await sourceFolder.CreateFileAsync("MyFile.dat", CreationCollisionOption.ReplaceExisting);

            await sourceFile.WriteAllTextAsync("這是我自己寫入檔案的內容");
            var fooContent = await sourceFile.ReadAllTextAsync();
            await DisplayAlert("讀出檔案的內容", fooContent, "OK");
        }

        public async Task 拍照(ImageLocation fooImageLocation = ImageLocation.Location1)
        {
            string fooImageFilename = $"{fooImageLocation.ToString()}.jpg";
            var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
            {
                Directory = "Sample",
                Name = fooImageFilename
            });

            if (file == null)
                return;

            IFolder rootFolder = FileSystem.Current.LocalStorage;
            IFolder sourceFolder = await FileSystem.Current.GetFolderFromPathAsync(file.Path.Replace(fooImageFilename, ""));
            IFile sourceFile = await FileSystem.Current.GetFileFromPathAsync(file.Path);

            IFolder TargetFolder = await rootFolder.CreateFolderAsync("Images", CreationCollisionOption.OpenIfExists);

            await sourceFile.MoveAsync($"{TargetFolder.Path}/{fooImageFilename}");
            IFile fileX = await TargetFolder.GetFileAsync(fooImageFilename);

            var s = await fileX.OpenAsync(FileAccess.Read);
            switch (fooImageLocation)
            {
                case ImageLocation.Location1:
                    image1.Source = ImageSource.FromStream(() => s);
                    break;
                case ImageLocation.Location2:
                    image2.Source = ImageSource.FromStream(() => s);
                    break;
                case ImageLocation.Location3:
                    image3.Source = ImageSource.FromStream(() => s);
                    break;
                case ImageLocation.Location4:
                    image4.Source = ImageSource.FromStream(() => s);
                    break;
                default:
                    break;
            }
        }
    }

    public enum ImageLocation
    {
        Location1,
        Location2,
        Location3,
        Location4,

    }
}

實際執行畫面

Android 執行結果

請在方案總管內,滑鼠右擊 XFFilePhoto.Droid 專案,選擇 設定為起始專案,接著按下 F5 開始執行。
Android執行結果1
Android執行結果2

佈署注意事項

iOS 執行結果

請在方案總管內,滑鼠右擊 XFFilePhoto.iOS 專案,選擇 設定為起始專案,接著按下 F5 開始執行。
iOS執行結果1
iOS執行結果2

佈署注意事項

Xamarin.Forms 訊息中心

訊息中心

Xamarin.Forms 提供一個訊息通訊機制,稱作 訊息中心 (MessagingCenter),透過訊息中心這個機制,可以減少 Xamarin.Forms 程式開發的耦合性,也就是說,讓各個類別間不要有關聯關係。在訊息中心機制內,包含了簡單的訊息傳送與接收服務。
例如:在 Xamarin.Forms 內,訊息中心使得 View Model 可以與其他類別物件通訊,而不需要知道這些物件的型別,雙方彼此只需要知道通訊的訊息合約內容即可。這樣的應用可以在這個專案範例中看到,MainPageViewModel 與 MainPage 透過訊息中心訂閱了相同的訊息,當訊息發送出來之後,兩者都會同時收到,不過,任一方或者雙方,都可隨時取些訂閱這訊息。
因此,在訊息中心的架構下,共有兩個部分所組成:
  • 訂閱 (Subscribe)
    當訂閱某個訊息,這表示需要隨時聆聽(Listen)是否有與其有關的訊息傳送進來,並且執行相關的動作。在 Xamarin.Forms 的訊息中心機制下,多個訂閱者可以同時聆聽同一個相同送出訊息。
  • 傳送訊息 (Send)
    透過傳送訊息,告知其他該訊息的訂閱者,可以執行相關動作。

MainPage

  • 在安裝好所有的擴充插件(Plugins),打開核心PCL 的 MainPage.xaml 檔案,將底下的 XAML 宣告定義複製到這個檔案內。
  • 首先為了避免在 iOS 平台執行時候,最上方的狀態烈被遮蔽掉的問題,因此,透過ContentPage.Padding 來定義整個頁面皆內縮 20dp。
  • 這個頁面的控制項,使用了 ScrollView 版面配置來進行所有控制項的定位,因此,當螢幕無法顯示所有控制項的時候,使用者可以透過手勢滑動螢幕的操作方式,看到其他內容。
  • 在這個 XAML 中,使用到的 ContentPage.Resources 功能,在這裡所宣告的內容,都可以直接在這個頁面內來引用;其中,<x:Double x:Key="TopicLabelFontSize">30</x:Double> 這個是宣告了一個Double 型別的物件,他的值為 30,在其他控制項若要參考這個物件,可以使用 StaticResource 這個延伸標記功能取得這個物件值。
  • 在這個頁面內,定義了大量的 Label 控制項,並且都有定義 x:Name 延伸標記,而這個延伸標記是用於讓 code-behind 的C#程式碼,可以存取到這個控制項。因此,實際的處理邏輯,都存在 code-behind 程式碼內。

MainPage.xaml

這個頁面 XAML 宣告內容相當的簡單。在 StackLayout 版面配置下,定義了三個按鈕與一個 ListView。
<?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:local="clr-namespace:XFMessage"
             x:Class="XFMessage.MainPage">

  <StackLayout
    Orientation="Vertical"
    HorizontalOptions="Start" VerticalOptions="Center">
    <Button x:Name="button說嗨" Text="說嗨" Clicked="Onbutton說嗨Click"/>
    <Button x:Name="button對約翰說嗨" Text="對約翰說嗨" Clicked="Onbutton對約翰說嗨Click"/>
    <Button x:Name="button停止訂閱顯示警告" Text="停止訂閱顯示警告" Clicked="Onbutton停止訂閱顯示警告Click"/>
    <ListView x:Name="listView" />
  </StackLayout>

</ContentPage>
  • 請將下列 C# 程式碼複製到 MainPage.xaml.cs內。
  • 在建構式內,設定這個頁面的 BindingContext 屬性是一個 MainPageViewModel 型別的物件;作為所有頁面控制項的資料綁定來源。另外,在建構式內也訂閱了訊息中心的一個有參數的訊息,當街收到這類訊息之後,就會顯示一個警告對話窗。
  • Onbutton停止訂閱顯示警告Click 按鈕事件則是處理取消訂閱訊息事件,也就是當按下這個按鈕之後,就不再會看到警告訊息對話窗了。

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace XFMessage
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();

            BindingContext = new MainPageViewModel();

            // Subscribe to a message (which the ViewModel has also subscribed to) to pop up an Alert
            MessagingCenter.Subscribe<MainPage, string>(this, "Hi", (sender, arg) => {
                DisplayAlert("接收到訊息", "參數=" + arg, "OK");
            });

            listView.SetBinding(ListView.ItemsSourceProperty, "Greetings");

        }

        void Onbutton說嗨Click(object sender, EventArgs e)
        {
            MessagingCenter.Send<MainPage>(this, "Hi");
        }

        void Onbutton對約翰說嗨Click(object sender, EventArgs e)
        {
            MessagingCenter.Send<MainPage, string>(this, "Hi", "約翰");
        }

        void Onbutton停止訂閱顯示警告Click(object sender, EventArgs e)
        {
            MessagingCenter.Unsubscribe<MainPage, string>(this, "Hi");
            DisplayAlert("停止訂閱",
                "這個頁面已經停止接收訊息,所以,不再會出現任何警告內容;然而, ViewModel 仍然會繼續接收訊息",
                "OK");
        }
    }
}

MainPageViewModel

  • 請在核心PCL專案節點使用滑鼠右擊,選擇 加入 > 類別,接著在名稱欄位旁的文字輸入盒中,填入MainPageViewModel。最後,請將底下程式碼複製到剛剛產生的檔案內。
  • MainPageViewModel 這個類別,是 MVVM 架構下的 ViewModel,也就是負責與 View 介接的類別,在這個範例中,這個 MainPageViewModel 提供了整個 MainPage 頁面中的資料繫結來源,因此,可以在 MainPage 的建構式內,看到 BindingContext 這個屬性的值,設定為 new MainPageViewModel();。而在這個頁面中的 ListView 控制項的資料來源,也是由 MainPageViewModel 這個 ViewModel 所提供的。
  • 這個 ViewModel 內,定義了一個 ObservableCollection<string> 類別物件,作為 ListView 控制項的資料來源。而在建構式中,訂閱了兩個訊息,一個是有傳遞參數的,一個是沒有傳遞參數的。不論是哪一個,當街收到所送出的訊息通知,就會在相對應的處理事件中,將所接收到的訊息內容,加入到ListView 內。

MainPageViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace XFMessage
{
    public class MainPageViewModel
    {
        public ObservableCollection<string> Greetings { get; set; }

        public MainPageViewModel()
        {
            Greetings = new ObservableCollection<string>();

            MessagingCenter.Subscribe<MainPage>(this, "Hi", (sender) => {
                Greetings.Add("Hi");
            });

            MessagingCenter.Subscribe<MainPage, string>(this, "Hi", (sender, arg) => {
                Greetings.Add("Hi " + arg);
            });
        }
    }
}

實際執行畫面

Android 執行結果

請在方案總管內,滑鼠右擊 XFMessage.Droid 專案,選擇 設定為起始專案,接著按下 F5 開始執行。
點選 說嗨 按鈕,此時,在 MainPageViewModel 內訂閱的訊息,就會將所接收到的訊息,加入到 ListView。
Android_執行結果1
點選 對約翰說嗨 按鈕,此時,在 MainPageViewModel 內訂閱的訊息,就會將所接收到的訊息,加入到 ListView。另外,在 MainPage 內訂閱的事件,則是會顯示出警告對話視窗。
Android_執行結果2
點選 停止訂閱顯示警告 按鈕,此時,會取消 MainPage 內訂閱的訊息,下次按下 對約翰說嗨 按鈕,則是不會顯示出警告對話視窗。
Android_執行結果3
因為已經取消 MainPage 內訂閱的訊息,此時按下 對約翰說嗨 按鈕,不會顯示出警告對話視窗。
Android_執行結果4

佈署注意事項

iOS 執行結果

請在方案總管內,滑鼠右擊 XFMessage.iOS 專案,選擇 設定為起始專案,接著按下 F5 開始執行。
點選 說嗨 按鈕,此時,在 MainPageViewModel 內訂閱的訊息,就會將所接收到的訊息,加入到 ListView。
iOS_執行結果1
點選 對約翰說嗨 按鈕,此時,在 MainPageViewModel 內訂閱的訊息,就會將所接收到的訊息,加入到 ListView。另外,在 MainPage 內訂閱的事件,則是會顯示出警告對話視窗。
iOS_執行結果2
點選 停止訂閱顯示警告 按鈕,此時,會取消 MainPage 內訂閱的訊息,下次按下 對約翰說嗨 按鈕,則是不會顯示出警告對話視窗。
iOS_執行結果3
因為已經取消 MainPage 內訂閱的訊息,此時按下 對約翰說嗨 按鈕,不會顯示出警告對話視窗。
iOS_執行結果4

佈署注意事項

參考