XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2018/09/06

什麼是 XAML 與 Xamarin.Forms

什麼是 XAML 與 Xamarin.Forms

在這個章節,我們要來了解什麼 XAML,以及什麼是 Xamarin.Forms ,並且了解各種 Xamarin.Forms 專案開發的方式有哪些。

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

什麼是XAML

XAML 為 Extensible Application Markup Language 縮寫,是一種宣告式的標記語言 (Declarative Markup Language),早期是用於 WPF (Windows Presentation Foundation) 開發框架下,用來取代 Windows Forms 開發工具,並協助開發者能夠開發出 Windows 作業系統下的 圖形化使用者介面 GUI (Graphic User Interface) 應用程式;我們會透過 XAML 語言來宣告應用程式中會顯示給使用者看到的各個視窗畫面內容,然而,我們可以使用 XAML 進行行動裝置應用程式的開發,在 Xamarin.Forms 下,我們則是會透過 XAML 來宣告行動應用程式 Mobile App 的每個頁面內容。在此,我們先來了解幾個名詞。
我們說 XAML 是一種宣告式的標記語言 (Declarative Markup Language),而什麼是宣告式語言 Declarative Language,根據 國家教育研究院的詞彙解釋,所謂的宣告式語言表示為 一種程式設計語言,係以告訴電腦要什麼,而不是告訴電腦要如何完成的方式設計程式。宣告式語言與我們正在使用的 C# 程式語言有著明顯的不同,c# 語言需要使用不同演算法來明確的指出,接下來的指令該如何去執行,而宣告式語言則不用;我們經常用於存取資料庫的 SQL 語言,也是屬於一種宣告式語言,C# 內 LINQ 同樣也是宣告式語言。
而在 XAML 中提到的標記 Markup 則又是甚麼呢?因為 XAML 本身是微軟所開發與設計出來的宣告式標記語言,他本身是就是 XML,也就是當您使用 XAML 語言的時候,必須要能夠符合 XML 的語法和規範;XML 本身當初是設計用來傳送與攜帶資料之用,並不是用於呈現或者展示資料之用,不過 HTML 則是與 XAML 類似,他們都可以用於表現資料之用,也就是可以用於描述使用者介面會看到的各個視覺控制項。在 XML 語言中,一個 XML 文件的字元將會區分成為 標記 Markup 與 內容 Content,標記 Markup 是用來定義資料該要如何呈現之用,其通常是以 < 開頭 (開始標記),而已 > 結尾 (結束標記),並且在這兩個標記之間的文字,我們稱為 項目 Element,而 [開始標記] [項目] [結束標記] 這樣的組合形成了一個 標籤 Tag ;我們在 Xamarin.Forms 中使用到的 XAML,定義了許多項目,我們可以透過這些項目來進行描述我們應用程式執行後,在行動裝置的螢幕上,會顯是那些內容。
例如,當我們建立一個 Xamarin.Forms 專案之後,就會看到 MainPage.xaml 檔案裡面已經幫我們建立了底下的內容。我們在這裡首先會看到使用標記定義的 ContentPage 標籤 <ContentPage>,代表整個行動裝置的螢幕,在這個螢幕內使用了版面配置標籤 <StackLayout> ,其會安排垂直堆疊方式來顯示出在 <StackLayout> 標籤類的子項目,最後,我們看到了 <Label/> 這個標籤,用來將文字字串顯示在螢幕上。
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:FirstXamarinFormsApp"
             x:Class="FirstXamarinFormsApp.MainPage">

    <StackLayout>
        <!-- Place new controls here -->
        <Label Text="Welcome to Xamarin.Forms!" 
           HorizontalOptions="Center"
           VerticalOptions="CenterAndExpand" />
    </StackLayout>

</ContentPage>
在使用 XAML 來進行使用者介面宣告的時候,每個使用者介面標記項目都會對應到 .NET Framework 中的特定類別,當我們在執行時期,系統會將 XAML 宣告的使用者介面標記項目使用這些對應的 .NET 類別,建立起相對應的物件 Object (或稱為 執行個體 Instance),當然,也會幫助我們設定這些物件的屬性狀態值,因此,這些畫面能夠表現出的視覺效果與顯示的內容,都是定義在這些類別,他們會做到這些事情。有了這樣的特性,我們就不需要進行使用者介面需求設計的時候,使用各個特定 .NET 類別,建立需要的物件、設定物件屬性狀態,接著操作這些物件來完成我們應用程式的視覺畫面內容。
有點要特別注意的,在 Xamarin.Forms 中的 XAML 宣告式標記語言,只能夠宣告各種使用介面與設定其屬性,無法定義各種程式處理邏輯 (大於、小於、不等於等等),例如,當手機螢幕為直式 (Portrait) 顯示狀,或者螢幕寬度大於 400 與裝置無關畫素 (DIP Device Independent Pixel)我們需要在畫面上顯示一個圖片使用者控制項。而且,我們也無法使用在 XAML 中內嵌 (inline) 任何 C# 程式碼 (相對來說,我們使用 HTML 設計網頁畫面的時候,是可以在 HTML 宣告是標記語言中內嵌 JavaScript 程式語言)。
我們來瀏覽一下 Xamarin.Forms 中的四大類型控制項參考,了解在 XAML 中可以使用的標記項目有哪些?

Pages 頁面

Xamarin.Forms 頁面為用於顯示整個行動裝置螢幕
使用者介面控制項說明
ContentPage是最簡單且最常見的類型頁面,代表整個行動裝置螢幕空間
MasterDetailPage管理兩個窗格的資訊,其中 Master 屬性頁面,通常顯示功能表清單,而 Detail 屬性頁面,顯示選取項目的頁面內容
NavigationPage管理使用堆疊架構為主的的頁面之間巡覽,其會出現在螢幕的最上方,稱之為導航工具列
TabbedPage使用索引標籤來巡覽不同的頁面
CarouselPage可讓使用者使用手指撥動瀏覽子頁面
TemplatedPage全螢幕內容控制項範本

Layouts 版面配置

Xamarin.Forms 版面配置用來撰寫使用者介面控制項至視覺化結構
底下為 使用單一內容的配置,也就是這些版面配置僅能夠包含一個子項目節點
使用者介面控制項說明
ContentView我們通稱為使用者控制項,可用於設計出可重複使用的組合式控制項
Frame可將包含在裡面的控制項,使用具有矩形外的框來顯示
ScrollView提供可捲動內容方式操作,以檢視超過螢幕大小的內容,具有水平或垂直捲動可供選擇
TemplatedView提供控制項的基本顯示範本內容
ContentPresenter上述樣板檢視版面配置要顯示的項目內容
底下為 具有多個子系的版面配置,也就是這些版面配置裡面可以包含多個子項目節點
使用者介面控制項說明
StackLayout使用堆疊方式來進行排列所包含的所有控制項,可以將子元素使用水平或垂直方式來排列
Grid使用類似 Excel,在格線中,指定資料列與行位置來顯示各子項目
AbsoluteLayout使用相對於父項目的指定位置來顯示這些子項目
RelativeLayout使用相對於子項目或其同層級項目的位置來顯示這些仔項目
FlexLayout可以根據CSS設定來彈性進行配置要顯示的子項目,稱為 彈性配置或彈性方塊

Views 檢視

檢視是使用者介面物件,例如標籤、 按鈕和滑桿,一般稱為控制項或是widget其他圖形化的程式設計環境中
使用者介面控制項說明
Label文字標籤,可顯示單行或多行文字字串
Image顯示專案內或者網路上的點矩陣圖片
BoxView顯示具有實心顏色矩形區塊
WebView顯示遠端網頁或本地端的 HTML 內容
OpenGLView可以在iOS 和 Android 專案中顯示OpenGL 圖形
Map使用作業系統本身原生軟體來顯示地圖
Button一個矩形物件,可顯示的文字與圖片,可與使用者產生互動
SearchBar使用作業系統本身原生控制項,讓使用者輸入要搜尋文字字串,並提供搜尋按鈕來觸發要搜尋的需求
Slider提供具捲動軸效果控制項,讓使用者透過滑動來選取指定數值
Stepper可使用向下或向上鈕選來選取指定數值
Switch類似 on/off 開關功能效果,提供使用者僅能夠選取其中一個布林值
DatePicker提供各原生作業系統中圖形化選取日期之選擇器控制項
TimePicker提供各原生作業系統中圖形化選取時間之選擇器控制項
Entry提供使用者可以輸入和編輯單行文字
Editor提供使用者可以輸入和編輯多行文字
ActivityIndicator當執行程式正在忙碌中,可以使用動畫來顯示正在忙碌中的視覺效果,此控制項不會提供任忙碌中的處理進度值
ProgressBar使用動畫來顯示應用程式正在處理長時間工作的完成百分比
Picker提供文字字串清單,讓使用者透過滑動來選取清單中的任一項目
ListView用於顯示集合類型的資料
TableView顯示資料列清單類型的項目

Cells 資料格

儲存格是特製化的項目,用於在資料表中的項目,並說明應該如何呈現在清單中的每個項目
使用者介面控制項說明
TextCell可顯示一或兩個文字字串
ImageCell類似 TextCell 控制項視覺效果,但包含了您設有一個點陣圖
SwitchCell可顯示 文字 與 Switch 控制項
EntryCell可顯示 文字與 Entry 控制項

什麼是 Xamarin.Forms

在 Visual Studio 2017 for Xamarin Tool 開發工具,可以讓我們使用 C# 程式語言並且搭配原生 SDK 功能,讓我們可以開發出在 Android / iOS / UWP 下執行的行動裝置應用程式,不過,在這樣的情境下進行開發出來的行動裝置應用程式專案,我們需要使用專屬個平台的 Xamarin 工具來進行設計:
  • Xamarin.Android 工具來幫助我們建立出 Android 平台下的應用程式,在這個專案內,我們可以使用 Android 原生 SDK 提供的使用者介面 API 來宣告這個應用程式要出現的畫面內容,並且可以使用全部該 SDK 內提供的相關 API 來進行專案需求設計。
  • Xamarin.iOS 工具來幫助我們建立出 iOS 平台下的應用程式,在這個專案內,我們可以使用 iOS 原生 SDK 提供的使用者介面 API 來宣告這個應用程式要出現的畫面內容,並且可以使用全部該 SDK 內提供的相關 API 來進行專案需求設計。
  • 關於 UWP 類型的應用程式,可以直接使用 Visual Studio 內建的原生 UWP SDK 來協助我們建立出 UWP 平台下的應用程式(因此,並沒有所謂的 Xamarin.UWP 這樣的開發工具存在),在這個專案內,我們可以使用 UWP 原生 SDK 提供的使用者介面 API 來宣告這個應用程式要出現的畫面內容,並且可以使用全部該 SDK 內提供的相關 API 來進行專案需求設計。
從上述說明我們可以知道,在這樣的開發環境下,我們需要使用 Xamarin.Android / Xamarin.iOS / UWP SDK 所提供的專屬 API 與開發工具,來進行各自行動應用專案的使用者介面設計,也就是我們需要讓使用者看到的畫面內容,也就是我們需要使用與原生 SDK 所提供 API 來直接進行使用者看到的畫面內容設計;並且,在各自專案下,不同作業系統的專案,也需要參考各原生 SDK 指定的生命週期管理環境,進行專案的設計。最後,總結這樣的設計方式,就是我們要針對每個行動應用平台,分別設計使用者要看到的頁面內容,不過,對於這個應用程式要使用到的商業處理邏輯,我們僅需要使用 C# 程式語言來設計一份程式碼即可。
不過, Visual Studio 2017 for Xamarin Tool 開發工具,也提供了第二中開發行動應用程式的方式,那就是 Xamarin.Forms Toolkit 工具集。那麼,究竟什麼是 Xamarin.Forms 呢?其實, Xamarin.Forms 僅是一個跨平台行動專案開發中,協助我們用來定義與設計使用者介面的工具,透過了 Xamarin.Forms API ( https://docs.microsoft.com/zh-tw/dotnet/api/Xamarin.Forms?view=xamarin-forms ) 提供豐富的類別庫,我們可以透過這些 API 提供的使用者介面設計類別,進行設計行動裝置應用程式下可以讓使用者看到的畫面內容,而不需要像上面採用原生 SDK 的設計方式,在每個作業系統平台下,要使用各平台 SDK 所指定的 API 來進行使用者介面設計。當我們完成專案設計之後,緊接著進行各行動裝置應用程式的建置,產生出來的各平台應用程式於執行的時候,會將 Xamarin.Forms 設計的使用者介面 API,分別自動對應到原生 SDK 上的使用者介面 API,並且使用原生 SDK 使用者介面 API 將最終畫面顯示到行動裝置 (手機或平板) 上。
因此,當我們採用 Xamarin.Forms 方式來進行跨平台行動應用專案開發的時候,並不需要精通每個原生平台的使用者介面 API 的使用方式,就可以產生出使用各原生平台下執行的應用程式,最重要的是,每個原生平台的應用程式,都是使用各原生平台的使用者介面控制項來顯示在螢幕上,而不是像是製作一個網頁,讓網頁透過 CSS 來模擬顯示出各平台下的使用者介面控制項。而採用 Xamarin.Forms 的開發方式有一個最為重要的好處,那就是我們僅需要使用 Xamarin.Forms API 設計出一套行動應用程式的畫面,就可以在每個原生平台下來顯示出來,這代表了我們在使用 Xamarin.Forms Toolkit 開發工具的時候,可以只使用一套使用者介面設計內容與一套該應用程式會用到的商業處理邏輯,就可以建置出 Android / iOS / UWP 三種行動應用程式,這樣比起前者,使用原生 SDK 與 原生使用者介面控制項來設計出來的行動應用程式,顯得更加快速、有效率與好維護。

直接使用 C# 來進行跨平台行動應用程式開發

我們可以說 Xamarin.Forms API 提供了一個抽象的跨平台使用者介面設計工具,每個平台會根據這些 Xamarin.Forms API 進行最後的實作,也就是採用原生 SDK 的使用者介面 API,這樣的觀念與我們進行程式碼設計中的相依性注入 Dependency Injection 設計模式,有著相同的好處與意義。
例如,若我們想要開發出一套跨平台的行動應用專案,使用該專案可以產生出可以在 Android / iOS / UWP 平台下執行的 App,在這個行動應用程式中,只有一個頁面,而在這個頁面內,將會有一個文字輸入盒、一個文字標籤、一個按鈕;不過,當使用者點選這個按鈕之後,會記錄下使用者點選了幾次這個按鈕,並且將這個累計點擊次數顯示在文字標籤上。像是這樣需求的應用程式,實際在 Android / UWP 平台下執行出來的成果如下面畫面截圖所示。
Just using C# for Xamarin.Forms Just using C# for Xamarin.Forms
這個說明範例專案 僅使用 C# 程式碼來創建出跨平台行動應用 App 原始碼,可以從 https://github.com/vulcanlee/XAMLInXamarin/tree/master/JustCSharpCode 取得。
為了要完成這個需求,我們使用了 Xamarin.Forms API 內的這幾個類別
  • 繼承這個類別的衍生類別,將來會成為與充滿整個螢幕頁面
  • 文字標籤控制項,這個控制項僅能夠顯示文字,使用者無法進行輸入文字
  • 按鈕控制項,使用者可以與這個控制項進行互動,當使用者點選這個按鈕之後,將會觸發 Clicked 這個事件
  • 堆疊版面配置,可以採用水平或垂直方式,堆疊所指定的控制項們
  • 文字輸入盒控制項,可以讓使用者輸入文字到這個控制項中
我們在 Visual Studio 的 Xamarin.Forms 專案內(也就是 .NET Standard 類別庫專案)建立一個 HomePage 類別,並且讓這個類別繼承 ContentPage 類,也就是說,HomePage 就是我們準備要顯示在行動應用 App 上的整個畫面處理類別。
Just using C# for Xamarin.Forms
底下將會是 HomePage 這個類別的定義,當我們設定我們 Xamarin.Forms 專案的第一個要顯示的頁面是 HomePage 類別產生的物件,整個應用程式執行結果,就會如同上面所描述的話面截圖與可以運作的動作。
在這個範例中,我們將會看到 Xamarin.Forms 中的四大類型使用者介面,分別是 頁面 Page、版面配置 Layout、控制項 Control (或者稱為 檢視 View)、檢視格 ViewCell,關於更多這四大類型的其他項目,我們將會於後續文章有更多的介紹。在 HomePage 頁面建構函式內,我們首先建立了兩個控制項:文字標籤 Label 與按鈕 Button ,也分別設定他們的運作屬性;由於按鈕控制項需要與使用者進行互動,因此,我們要訂閱按鈕控制項中的 Clicked 事件,當使用者點選這個按鈕之後,將會執行這個訂閱事件的委派方法;在這裡,我們訂閱事件的委派方法將採用 Lambda 匿名委派方法來定義,我們在委派方法內設計了一個點選按鈕次數的計數器,我們使用 整數型別的 index 這個區域變數來記錄使用者點選按鈕的次數,接這,將使用者點選按鈕的次數表示文字,設定到文字標籤的 Text 屬性上,一旦執行完成這個委派方法之後,在螢幕上就會在文字標籤控制項位置,顯示這些文字。
然後,我們設定了整個螢幕頁面要顯示的內容,將會由堆疊版面配置 StackLayout 來提供,在堆疊版面配置中,我們指定了 文字輸入盒控制項 Entry、文字標籤控制項 Label、按鈕控制項 Button,並且採用垂直堆疊方式來進行排列;整個堆疊版面配置將會填滿整個螢幕寬度並且位於螢幕高度的中間。
C Sharp / C#
// 這個類別將會成為與充滿整個螢幕頁面
class HomePage : ContentPage
{
    int index = 0;
    public HomePage()
    {
        // 建立一個文字標籤控制項
        Label label = new Label();
        // 建立一個按鈕控制項,並且設定該按鈕要顯示 確定 文字在按鈕上
        Button button = new Button()
        {
            Text = "確定"
        };
        // 設定當使用者點選這個按鈕之後,將會執行 Clicked 訂閱事件的委派方法
        button.Clicked += (s, e) =>
        {
            label.Text = $"點選按鈕次數 {++index}";
        };
        // 設定整個螢幕頁面會使用 StackLayout 版面配置來進行安排要顯示的內容
        this.Content = new StackLayout()
        {
            // 設定這個版面配置的運作屬性 Property
            Orientation = StackOrientation.Vertical,
            HorizontalOptions = LayoutOptions.Fill,
            VerticalOptions = LayoutOptions.Center,
            // 設定這個版面配置裡面,有顯示那些控制項內容
            Children =
            {
                // 建立一個文字輸入盒控制項
                new Entry()
                {
                    Placeholder = "請輸入您的名字"
                },
                label,
                button
            }
        };
    }
}
透過上述的說明,我們可以更深入的了解到 Xamarin.Forms 在幫助我們跨台行動應用程式開發上所提供的幫助,那就是提供了許多類別 API,讓我們設計整個行動應用程式要顯示的內容;不過,您可以從執行結果的螢幕截圖看到,這些控制項在每個平台上,都是使用原生 SDK 提供的控制項來顯示出來,並且使用預設控制項的顯示行為。
我們來看看 Xamarin.Forms 是如何做到這樣的結果,我們先以 ContentPage 這個頁面為例,當我們在進行 Xamarin.Forms 工具集進行設計的時候,我們並不需要知道這個應用程式未來要執行的平台是 Android / iOS / UWP,以及各原生作業系統下的要採用哪個原生 API 來顯示出 ContentPage 內容,我們會在每個原生行動專案下,分別實作 PageRenderer 這個類別,我們會在這個類別中,依照當時原生作業系統 SDK 所提供的各個類別 API,採用合適的 API 來顯示 ContentPage 內容,底下將分別列出這個範例中所用的 Xamarin.Forms 使用者介面項目,採用的是哪個 Renderer 類別名稱以及在每個原生作業系統下,使用的是哪個原生類別 API 來顯示在螢幕上。
Xamarin.FormsRendererAndroidiOSUWP
ContentPagePageRendererViewGroupUIViewControllerFrameworkElement
LabelLabelRendererTextViewUILabelTextBlock
ButtonButtonRendererButtonUIButtonButton
StackLayoutViewRendererViewUIViewFrameworkElement
EntryEntryRendererEditTextUITextViewTextBox
透過底下的圖片說明,可以幫助您對於 Xamarin.Forms Renderer 這個技術上有更深入的認識。首先,下圖為我們剛剛提到的範例專案中 HomePage 頁面,我們使用 Xamarin.Forms API 提供的各個使用者介面類別,設計出我們期望顯示在使用者行動裝置上的 App。
Xamarin.Forms UI Element
當我們編譯與建置完成這個 Xamarin.Forms 專案之後,接著要在 Android 平台下來進行執行,原先在 Xamarin.Forms 內使用到的各個使用者介面項目,會由 Android 平台下的各個 XXXXRenderer 來替換,在每個 XXXXRenderer 類別內,則會使用 Android SDK 內的各個使用者介面類別作為最終要顯示在使用者行動裝置上的樣貌與可以表現的行為(例如,原先在 Xamarin.Forms 中的 ContentPage,在 Android 平台下,會使用 PageRenderer 來實作出來,並且選擇 Android 平台下的 ViewGroup 來顯示)。
Android UI Element
若我們想要在 iOS 平台下來進行執行,原先在 Xamarin.Forms 內使用到的各個使用者介面項目,會由 iOS 平台下的各個 XXXXRenderer 來替換,在每個 XXXXRenderer 類別內 ,則會使用 iOS SDK 內的各個使用者介面類別作為最終要顯示在使用者行動裝置上的樣貌與可以表現的行為(例如,原先在 Xamarin.Forms 中的 StackLayout iOS 平台下,會使用 ViewRenderer 來實作出來,並且選擇 iOS 平台下的 UIView 來顯示)。
iOS UI Element
最後,我們要在 UWP 平台下來進行執行,原先在 Xamarin.Forms 內使用到的各個使用者介面項目,會由 UWP 平台下的各個 XXXXRenderer 來替換,在每個 XXXXRenderer 類別內,則會使用 UWP SDK 內的各個使用者介面類別作為最終要顯示在使用者行動裝置上的樣貌與可以表現的行為(例如,原先在 Xamarin.Forms 中的 Entry,在 UWP 平台下,會使用 EntryRenderer 來實作出來,並且選擇 UWP 平台下的 TextBox 來顯示)。
UWP UI Element
這也就是為什麼使用 Xamarin.Forms 所開發出來的跨平台行動應用程式,所產生出來的每個平台下可執行的 App,都是使用原生 SDK 控制項的做法,也就是這樣,實際執行 Xamarin.Forms 所開發出來的 App,因為都是採用原生 SDK API,因此,執行效能也比起其他跨平台開工具,具有更高的執行效能。

使用 XAML + Code Behind 來進行跨平台行動應用程式開發

首先,我們要先知道,XAML 是用來幫助我們設計手機畫面中的使用者介面的一個輔助工具,我們所有螢幕畫面可以看到的內容,都可以使用 XAML 來進行宣告;以我們上面看到的例子,是單純僅使用 C# 程式碼來進行整個行動應用程式的開發,現在,透過 XAML 這個宣告式標記語言,我們使用關注點分離設計原則,將所有使用者要看到的內容,都使用 XAML 來進行宣告,其他商業處理邏輯部分,將會使用 C# 程式碼來處理。對於 C# 程式碼的部分,將會有兩種架構可以選擇,那就是採用 Code Behind 設計方法 或者使用 MVVM 設計模式,對於後者,我們會在後面有更加詳盡的說明,在這裡,我們將來看看如何使用 XAML 宣告式標記語言與 Code Behind 設計方法,完成上面所提到的同樣專案需求。在看完這個部分之後,您可以比較單純僅使用 C# 程式語言與 Xamarin.Forms API 來進行跨平台行動應用專案的開發與使用 XAML 並搭配 Code Behind 設計方法之間的差異。
這個說明範例專案 使用 XAML + Code Behind 來進行跨平台行動應用程式開發 原始碼,可以從 https://github.com/vulcanlee/XAMLInXamarin/tree/master/XAMLCodeBehind 取得。
當我們使用 Visual Studio 開發工具建立一個 XAML 檔案的時候 (在這裡,我們需要建立一個名稱為 HomePage 的內容頁面 ContentPage),在 Xamarin.Forms 開發環境下,會幫我們建立一個副檔名為 .xaml 的檔案,您可以直接使用滑鼠雙擊這個檔案,例如,如下圖,滑鼠雙擊 [HomePage.xaml] 這個節點,我們變可以在這個檔案中撰寫任何 XAML 支援的宣告式標記語言;然而,若我們想要在這個頁面中使用 C# 程式語言來的任何商業處理邏輯,像是我們上面所提到的需求,您可以找到該頁面的節點,使用滑鼠點擊該節點前面的空心三角形,此時,您將會看到出現了 [HomePage.xaml.cs] 這個節點,現在,您可以使用滑鼠雙擊這個節點,便會看到 Visual Studio 顯示出一個可以撰寫程式碼的視窗,最後,您就可以在這裡使用 C# 程式語言,開始撰寫出各頁面所需要的商業邏輯了。這樣的設計方式,就是使用後置程式碼 (Code Behind)進行開發 Xamarin.Forms 的應用程式。
XAML vs Code Behind
讓我們來看看如何透過 XAML 與 Code Behind 設計方法來創建出剛剛的應用範例,首先,我們打開 [HomePage.xaml] 這個節點,並且填入底下的內容。現在,在 ContentPage 頁面內,會有一個 StackLayout 的堆疊版面配置項目 (Element) ,在 StackLayout 堆疊版面配置內,我們只訂了三個項目,分別是 文字輸入盒 Entry、文字標籤 Label、按鈕 Button。我們也設定了 StackLayout 的三個屬性:堆疊排列方式 Orinetation 將會採用 垂直 Vertical 的堆疊方式排列,整個堆疊版面配置將會填滿整個螢幕寬度 HorizontalOption 並且位於螢幕高度的中間 VerticalOption。
另外,為了要讓 Code Behind 程式碼可以存取這些螢幕上的使用者介面控制項內容,我們還需要使用 x:Name 這個擴充標記來設定每個控制項在 .NET 環境下的變數名稱。在 Button 控制項中,我們使用了 Clicked 這個事件,設定當使用者點選這個按鈕之後,需要執行 Code Behind 的 button_Clicked 委派方法。
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"
             x:Class="XAMLCodeBehind.HomePage">
    <ContentPage.Content>
        <StackLayout
            Orientation="Vertical"
            HorizontalOptions="Fill"
            VerticalOptions="Center">
            <Entry
                x:Name="entry"
                Placeholder="請輸入您的名字"
                />
            <Label
                x:Name="label"
                />
            <Button
                x:Name="button"
                Text="確定"
                Clicked="button_Clicked"
                />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>
一旦我們編譯這個 Xamarin.Forms 專案, Visual Studio for Xamarin 工具將會幫我們為每個 XAML 項目都產生出一個 頁面名稱.xaml.g.cs (例如,我們的頁面 XAML 項目名稱為 HomePage.xaml,此時編譯器將會為我們產生一個 HomePage.xaml.g.cs 檔案)。想要看到這個檔案,請點選 Visual Studio 方案總管上方工具列圖示的 [顯示所有檔案] 這個圖片,然後,我們可以展開 [obj] 這個節點,接著在 [Debug] > [netstandard2.0] 目錄中,可以找到 HomePage.xaml.g.cs 這個檔案;讓我們打開這個檔案,來看看編譯器幫我們產生的這個檔案,究竟有哪些內容呢?
Compiler Generate XAML Code
在這個 HomePage.xaml.g.cs 檔案內,我們看到了編譯器幫我們建立了一個 partial class HomePage 類別,而且我幫助我們建立了一個 [InitializeComponent] 方法 (等下我們在頁面的 Code Behind 建構函式內,也會看到要來使用這個方法),這個輔助方法將會用來幫助我們根據 XAML 宣告的內容,產生出相對應的 .NET 下會用到的 Xamarin.Forms 類別物件,在這裡,將會透過 Xamarin.Forms.Xaml.Extensions.LoadFromXaml 方法來讀取編譯過的 XAML 檔案內容,以便知道要產生出那些 .NET 中會用到的物件。另外,因為我們在 XAML 中有使用 [x:Name] 這個擴充標記功能,首先會在這裡幫助我們建立這些欄位在這個類別中,並且會使用 Xamarin.Forms.NameScopeExtensions.FindByName 方法,幫助我們設定這些欄位要指定到那些使用者介面物件 (這些物件將是我們在 XAML 中宣告的項目)。原則上,在我們開發與設計 Xamarin.Forms 專案過程中,是可以忽略與不用理會這些由編譯器幫助我們產生的檔案。最後有個最重要的事情,有任何需要,請不要來修改這個檔案中的任何內容,因為,每次在進行建置與編譯 Xamarin.Forms 專案的時候,這個檔案都會被重新建立起來,因此,若您直接來修改這個檔案內容,將會於建置過程中,所有的修改內容都會消失不見。
C Sharp / C#
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::Xamarin.Forms.Xaml.XamlResourceIdAttribute("XAMLCodeBehind.HomePage.xaml", "HomePage.xaml", typeof(global::XAMLCodeBehind.HomePage))]

namespace XAMLCodeBehind {


    [global::Xamarin.Forms.Xaml.XamlFilePathAttribute("HomePage.xaml")]
    public partial class HomePage : global::Xamarin.Forms.ContentPage {

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "2.0.0.0")]
        private global::Xamarin.Forms.Entry entry;

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "2.0.0.0")]
        private global::Xamarin.Forms.Label label;

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "2.0.0.0")]
        private global::Xamarin.Forms.Button button;

        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "2.0.0.0")]
        private void InitializeComponent() {
            global::Xamarin.Forms.Xaml.Extensions.LoadFromXaml(this, typeof(HomePage));
            entry = global::Xamarin.Forms.NameScopeExtensions.FindByName<global::Xamarin.Forms.Entry>(this, "entry");
            label = global::Xamarin.Forms.NameScopeExtensions.FindByName<global::Xamarin.Forms.Label>(this, "label");
            button = global::Xamarin.Forms.NameScopeExtensions.FindByName<global::Xamarin.Forms.Button>(this, "button");
        }
    }
}
讓我們現在來打開 Code Behind 這個檔案節點,也就是 [HomePage.xaml.cs] 這個檔案,首先,我們看到了這個檔案內容也是一個 partial class HomePage 類別宣告,將會與剛剛由編譯器產生的同名類別,結合成為我們所要使用的 HomePage 頁面內別。在這個 HomePage 建構函式內,我們看到了他會執行 InitializeComponent() 這個方法,而這個方法我們剛剛在上面由編譯器產生的檔案中有看到,不過,千萬要注意,請不要把 Code Behind 中的 InitializeComponent() 呼叫移除,因為,您再也無法透過這個方法來進行讀取 XAML 中的各個使用者介面宣告標記,進而幫助我們產生在 .NET 中會用到的相關物件,簡單說,就是沒有任何使用者介面可以使用,最終造成您編譯 Xamarin.Forms 專案的時候會失敗。
另外一個是 button_Clicked 方法,從這個方法的函式簽章就可以看的出來,他是一個用於 .NET 事件所會用到的一個委派方法,也就是在系統中若有某個特定事件觸發的時候,將會來執行這個方法,而這個方法將會是我們在 XAML 按鈕中,使用按鈕的 Clicked 這個事件來設定的事件委派方法,也就是當使用按下這個按鈕,將會執行這個方法。在這個事件內,我們會使用 label.Text 來指定 文字標籤控制項要顯示的內容,在這裡要了解 [label] 這個欄位,是由呼叫 InitializeComponent() 這個方法幫我們所建立的。
C Sharp / C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace XAMLCodeBehind
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class HomePage : ContentPage
    {
        int index = 0;
        public HomePage ()
        {
            InitializeComponent ();
        }

        private void button_Clicked(object sender, EventArgs e)
        {
            label.Text = $"點選按鈕次數 {++index}";
        }
    }
}
現在,您可以實際建置與執行這個使用 XAML 與 Code Behind 開發技術而設計成的專案,您將會看實際執行的 App,不論是在整個 App 呈現內容與運作行為,都與剛剛我們僅使用 C# 程式碼,搭配 Xamarin.Forms 相關 API 類別來設計的專案,執行結果完全都一模一樣。這點是無庸置疑的,因為當我們使用 XAML 來幫助我們進行使用者畫面的使用者介面宣告的時候,將會透過編譯器的幫助,使用 XAML 中各個使用者介面項目所對應到的實際 Xamarin.Forms API 類別,來幫我們建立起相關這些在 .NET 開發與執行上會用到的物件,因此,我們無須自己使用 C# 程式碼,使用 new 運算子來自己建立起這些物件;另外,搭配了 Code Behind,我們可以在這裡進行該應用程式的運作商業邏輯的設計。

使用 XAML + MVVM 來進行跨平台行動應用程式開發

至於另外一種開發方式,那就是採用 MVVM (模型-檢視-檢視模型 Model-View-ViewModel) 設計模式,我們並不會使用剛剛談論到的後置程式碼 Code Behind 節點,我們會另外建立一個新的類別,在這裡,我們稱這個類別為 檢視模型 ViewModel,相關的商業處理邏輯將會在這個新類別中來進行開發與設計;可是,問題來了,我們要怎麼把這個頁面與這個檢視模型 (ViewModel)串接起來,讓我們做到,當使用者按下某個按鈕之後,會執行檢視模型內的指定方法,或者可以在檢視模型中,透過 C# 所撰寫的各種程式碼,可以將處理結果內容,更新到頁面上的使用者介面控制項中,這個時候,我們就需要借助於 資料綁定(繫結) Data Binding 這個機制。
這個說明範例專案 使用 XAML + MVVM 來進行跨平台行動應用程式開發 原始碼,可以從 https://github.com/vulcanlee/XAMLInXamarin/tree/master/XAMLMVVM 取得。
當要在 Xamarin.Forms 專案中採用 MVVM 設計模式,我們先在 .NET Standard 專案中,建立兩個方案資料夾,分別是 ViewModels (下圖標記1號處) 與 Views (下圖標記2號處),前者將會用來儲存我們所開發的 ViewModel 檢視模型類別(下圖標記3號處),後者將會用來存放我們新建立的 XAML 頁面檔案(下圖標記4號處)。我們延續相同的範例,使用 MVVM 的設計模式來進行開發。
XAML vs MVVM
首先,我們還是先建立一個 HomePage 的 XAML 頁面檔案,在這裡的 XAML 將會與前面的 Code Behind 後置程式碼專案具有相同類似的 XAML 內容,不過,我們可以移除 [x:Name] 這個延伸標記的宣告,因為,我們不再需要透過 Code Behind 設計方法,來存取頁面上各個使用者介面控制項,但是,我們要使用什麼方式來在 C# 程式碼中,進行變更頁面上的使用者介面控制項的狀態值;在這裡,我們將會透過在進行 Xamarin.Forms 專案開發上會用到的一個非常重要的技術,那就是資料綁定 (Data Binding) 技術,透過這個技術,只要我們在檢視模型 ViewModel 類別中有進行該類別的屬性值異動,透過了資料綁定技術,就可以同步的進行螢幕畫面上的使用者介面控制項屬性的變更與存取。
所以,我們在底下的 XAML 宣告中,看到了在 Label 這個 項目 Element / 檢視 View 中,設定 Label.Text 這個文字標籤控制項的屬性 attribute,將會使用資料綁定的技術與檢視模型中的 C# 屬性 Property,也就是 ClickedButtonMessage 進行綁定;另外,對於按鈕控制項,我們也不再使用之前用的事件,也就是 Clicked 這個事件,而改用了 Button.Command 這個命令屬性 Command Attribute,這個時候,我們僅需要在檢視模型中有建立一個型別為 ICommand 介面型別的屬性 Property,這裡使用的是 ButtonTapCommand,如此,當使用者點選螢幕上的這個按鈕之後,就會執行我們在檢視模型中 ButtonTapCommand 這個屬性 Property 設定的委派命令方法。
最後,我們可以透過 Code Behind 中該頁面類別的建構函式,設定這個頁面的資料綁定來源,也就是要指定該頁面會用到的檢視模型物件,不過,在這裡,我們是直接 XAML 檔案中,來完成這樣的設計需求。我們設定了 ContentPage.BindingContext 這個屬性 Attribute,其參考到一個 HomePageViewModel 檢視模型的 C# 物件。
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" 
             x:Class="XAMLMVVM.Views.HomePage">
    <ContentPage.BindingContext>
        <viewModels:HomePageViewModel
            xmlns:viewModels="clr-namespace:XAMLMVVM.ViewModels"
            />
    </ContentPage.BindingContext>
    <ContentPage.Content>
        <StackLayout
            Orientation="Vertical"
            HorizontalOptions="Fill"
            VerticalOptions="Center">
            <Entry
                Placeholder="請輸入您的名字"
                />
            <Label
                Text="{Binding ClickedButtonMessage}"
                />
            <Button
                Text="確定"
                Command="{Binding ButtonTapCommand}"
                />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>
現在,我們可以開始來設計這個頁面 HomePage 需要用到的檢視模型。對於每個檢視模型 ViewModel 類別,我們都須實作 .NET Framework 2.0 就已經存在有的 INotifyPropertyChanged 介面,接著,在這裡宣告檢視 View ,也就是 XAML 頁面檔案中有使用到 Binding 關鍵字的相關要綁定的屬性。我們在 HomePageViewModel 這個類別的建構函式內,建立一個 Command 類別的物件,指定給 ButtonTapCommand C# 屬性 Property,並且在產生 Command 型別物件的時候,需要傳入一個委派方法,這個方法將會在使用者點選按鈕的時候,需要執行的方法。在這個委派方法,我們使用匿名委派方法 Lambda 的語法來設計,裡面僅有一行程式碼: ClickedButtonMessage = $"點選按鈕次數 {++index}"; 也就是會產生一個字串,其內容說明了使用者總共點選了按鈕幾次,由於我們將這個新產生的文字設定到 ClickedButtonMessage 屬性上,在這個屬性的設定屬性方法中,會呼叫 OnPropertyChanged() 方法,如此,就會發出一個 INotifyProperty 的通知事件,通知螢幕上有綁定這個屬性的控制項,要更新其內容。
C Sharp / C#
public class HomePageViewModel : INotifyPropertyChanged
{
    int index = 0;

    #region 實作 INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// 發出 INotifyPropertyChanged 事件,通知畫面要更新指定頁面中綁定的屬性 Attribute,使用 ViewModel 內的指定屬性 Property 來更新
    /// </summary>
    /// <param name="propertyName"></param>
    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        var changed = PropertyChanged;
        if (changed == null)
            return;

        changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion

    #region 用於綁定到螢幕上的 Label.Text 屬性 Attribute 上,這裡將用於資料綁定
    private string _ClickedButtonMessage;

    public string ClickedButtonMessage
    {
        get { return _ClickedButtonMessage; }
        set { _ClickedButtonMessage = value; OnPropertyChanged(); }
    }
    #endregion

    // 綁定按鈕中的 Command 屬性 Attribute,這裡將用於命令綁定
    public ICommand ButtonTapCommand { get; set; }

    public HomePageViewModel()
    {
        ButtonTapCommand = new Command(() =>
        {
            // 使用者點擊按鈕之後,將會執行此命令的委派方法
            ClickedButtonMessage = $"點選按鈕次數 {++index}";
        });
    }
}
這樣的開發與設計方式,就是在 Xamarin.Forms 使用 MVVM 設計模式搭配資料綁定來進行開發的說明,至於更多關於 MVVM 與 資料綁定 Data Binding 的使用方式,請參考後面的內容。

2018/08/28

使用 Visual Studio 工具箱 Toolbox 來設計頁面 XAML 語言

使用 Visual Studio 2017 15.8 工具箱 Toolbox 來設計頁面 XAML 語言


了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

在以往我們進行 Xamarin.Forms 專案開發的時候,我們需要使用 C# 來設計這個行動應用程式的商業邏輯運作行為,對於這個行動應用程式,我們將會透過 XAML 宣告標記語言來設計可用於跨平台的頁面之使用者介面內容。不過,要如何使用這項功能呢,您需要升級 VS4W Visual Studio 2017 for Windows 到 Visual Studio 2017 15.8 版本,並且升級到 Xamarin.Forms NuGet 套件到 3.0 以上的版本。
Xamarin.Forms Toolbar
讓我們使用 Prism Template Pack (2.0.9版本) 來建立一個 Xamarin.Forms 專案
Xamarin.Forms Prism Template Pack
當專案建立完成之後,我們打開 MainPage.xaml 檔案,接著,請打開工具箱視窗,若您找不到工具箱視窗,可以點選 Visual Studio 2017 功能表,點選 [檢視] > [工具箱]。不過,您會很失望,雖然,我這裡的 VS2017 已經升級到 15.8 以上版本,可是,還是看不到關於 Xamarin.Form XAML 的工具箱。
Xamarin.Forms MainPage.xaml
檢查一下您的專案使用的 Xamarin.Forms NuGet 套件,結果發現到 Prism Template Pack 專案樣板建立起來的 Xamarin.Forms 專案,使用的是 2.5.0.122203 版本的 NuGet 套件。
Xamarin.Forms Prism Template Pack
因此,我們需要把整個方案中的所有專案都升級到 Xamarin.Forms 3.0 以上版本的 NuGet 套件,不過,若您只是升級 .NET Standard 專案中的 Xamarin.Forms NuGet 套件到最新版本,您將會得到底下錯誤訊息;所以,記得要把所有方案內的專案,都升級 Xamarin.Forms NuGet 套件到最新版本
Xamarin.Forms NuGet 套件
Xamarin.Forms tasks do not match targets. Please ensure that all projects reference the same version of Xamarin.Forms, and if the error persists, please restart the IDE.    XFToolbar.Android
最後,若您完成了上述動作,您可以切換到 MainPage.xaml 視窗中,打開工具箱視窗,您就會看到了 XAML 可以使用到的各個控制項。您可以點選工具箱中 [Controls] / [Layouts] / [Cells] 的任一項目,拖拉到 XAML 適當地方,Visual Studio 就會幫您產生相關 XAML 語法出來。在這裡,我們從 [工具箱] > [Controls] 拖拉 [Entry] 控制項到右邊的 MainPage.xaml 視窗中的 </StackLayout> 項目前,此時,就會自動產生出 Entry 的 XAML 的語言宣告 <Entry Placeholder="" /> 出來
Xamarin.Forms NuGet 套件

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程

2018/08/10

在 Xamarin.Forms 使用 SkiaSharp 來進行 2D 圖形繪製 2 : 使用 MVVM 設計方式,在 ViewModel 內來呼叫 SkiaSharp API

根據微軟官方文件 SkiaSharp 簡介 上的說明,SkiaSharp 提供豐富且功能強大 2D 圖形 API,可用來呈現到 2D 的緩衝區。 您可以使用這些來實作自訂使用者介面項目和可整合到您的應用程式的 2D 圖形。 SkiaSharp 是.NET 繫結至Skia程式庫會繼承此文件庫的強大的功能。這也就是說,您可以建立一個 SKCanvasView 控制項,我們便擁有了一個畫布,使用 SkiaSharp 所提供的相關 API,即可以開發出繪製出各種效果的圖片。
在這篇文章中,我們將會 使用 MVVM 設計方式,在 ViewModel 內來呼叫 SkiaSharp API,而在前一篇文章中,我們都是在頁面的 Code Behind 內撰寫呼叫 SkiaSharp 程式碼。
在這個練習專案頁面中,我們設計了兩個 200x200 的 SKCanvasView 控制項,並且使用了 Prism 所提供的 行為 Behavior 功能中的 EventToCommandBehavior,讓我們指定了這些 SKCanvasView 控制項內的指定事件 PaintSurface 若被觸發的話,將會自動去執行 ViewModel 內的 PaintSurfaceCommand 命令。
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:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
             xmlns:local="clr-namespace:XFSkiaMVVM"
             x:Class="XFSkiaMVVM.Views.MainPage"
             Title="{Binding Title}">

    <ContentPage.Resources>
        <ResourceDictionary>
            <local:SKPaintSurfaceEventArgsConverter x:Key="SKPaintSurfaceEventArgsConverter"/>
        </ResourceDictionary>
    </ContentPage.Resources>

    <ScrollView
        >
        <StackLayout HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
            <BoxView
                Color="Black"
                WidthRequest="200" HeightRequest="200"/>
            <skia:SKCanvasView
                BackgroundColor="Black"
                IgnorePixelScaling="True" 
                WidthRequest="200" HeightRequest="200"
                >
                <skia:SKCanvasView.Behaviors>
                    <behavior:EventToCommandBehavior
                        EventName="PaintSurface"
                        Command="{Binding PaintSurfaceCommand}"
                        EventArgsConverter="{StaticResource SKPaintSurfaceEventArgsConverter}"
                    />
                </skia:SKCanvasView.Behaviors>
            </skia:SKCanvasView>
            <skia:SKCanvasView
                BackgroundColor="Black"
                IgnorePixelScaling="False" 
                WidthRequest="200" HeightRequest="200"
                >
                <skia:SKCanvasView.Behaviors>
                    <behavior:EventToCommandBehavior
                        EventName="PaintSurface"
                        Command="{Binding PaintSurfaceSelfScaleCommand}"
                        EventArgsConverter="{StaticResource SKPaintSurfaceEventArgsConverter}"
                        />
                </skia:SKCanvasView.Behaviors>
            </skia:SKCanvasView>
        </StackLayout>
    </ScrollView>

</ContentPage>
不過,在這裡,您也看到 EventArgsConverter="{StaticResource SKPaintSurfaceEventArgsConverter}" 這個 XAML 敘述,底下將會是這個 數值轉換器 Value Converter 之 SKPaintSurfaceEventArgsConverter 定義程式碼。由於我們需要在 ViewModel 內取得該事件傳入進來的參數,因此,我們建立一個數值轉換器,並且使用 EventArgsConverter 來指定要將事件傳入進來的參數,可以我們在 ViewModel 內來使用。我們可以看到,我們會透過數值轉換器傳入進來的 value 參數值,轉型成為該事件的參數型別,也就是,SKPaintSurfaceEventArgs,並且將這個值回傳回去。
C#
using SkiaSharp.Views.Forms;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Xamarin.Forms;

namespace XFSkiaMVVM
{
    class SKPaintSurfaceEventArgsConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var skPaintSurfaceEventArgs = value as SKPaintSurfaceEventArgs;
            if (skPaintSurfaceEventArgs == null)
            {
                throw new ArgumentException("Expected value to be of type SKPaintSurfaceEventArgs", nameof(value));
            }
            return skPaintSurfaceEventArgs;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
那麼,究竟要如何在 ViewModel 內讀取該事件傳入進來的參數值呢?在這裡,當我們要宣告事件2命令的命令時候,要使用泛型的方式來宣告其型別,也就是類似這樣 public DelegateCommand<SKPaintSurfaceEventArgs> PaintSurfaceCommand { get; set; } 在這裡,我們就看到了剛剛的數值轉換器的轉型型別,也就是我們命令中的要指定的泛型型別。接下來,在我們建立這個命令的物件時候,就可以使用 PaintSurfaceCommand = new DelegateCommand<SKPaintSurfaceEventArgs>(args => { ... } 這樣敘述來建立,我們使用了 Lambda 匿名函式傳入到這個命令建構式內,而我們在這個匿名函式內,就可以透過 args 這個參數變數,取得原先在 SKCanvasView 控制項 PaintSurface 事件內的事件參數物件。有了 SKPaintSurfaceEventArgs 這個參數物件,我們便可以開始進行針對 SKCanvasView 畫布,呼叫各種 SkiaSharp API,進行繪圖需求工作了。
若您仔細觀察執行結果,您將會發現到第一個 SKCanvasView 控制項所繪製出來的圓形與線條會有鋸齒狀,而且,這裡所指定畫線與畫圓的 API 使用到的尺寸都是 與裝置無關的畫素,也就是沒有根據當時螢幕的縮放比來自行計算出真實畫素實際值,這是因為,我們在 SKCanvasView 控制項內,設定了 IgnorePixelScaling="True" 屬性值,當 IgnorePixelScaling 屬性值為真,我們在 SKCanvasView 控制項內的所指定的尺寸,就可以使用 XAML 中設定的尺寸大小, SkiaSharp 會自動幫我們進行縮放,可是,您看到這樣的效果並不是很好,因為,會有鋸齒狀出現。
所以,我們還是建議類似 PaintSurfaceSelfScaleCommand 命令中的做法,使用 float fooScale = info.Width / 200.0f; canvas.Scale(fooScale); 敘述,呼叫 Scale 方法,指定縮放比率值,這樣就不會有鋸齒狀出現了。
C#
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace XFSkiaMVVM.ViewModels
{

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public DelegateCommand<SKPaintSurfaceEventArgs> PaintSurfaceCommand { get; set; }
        public DelegateCommand<SKPaintSurfaceEventArgs> PaintSurfaceSelfScaleCommand { get; set; }
        private readonly INavigationService _navigationService;

        public MainPageViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;

            PaintSurfaceCommand = new DelegateCommand<SKPaintSurfaceEventArgs>(args =>
           {
               SKImageInfo info = args.Info;
               SKSurface surface = args.Surface;
               SKCanvas canvas = surface.Canvas;

               canvas.Clear();

               SKPaint fooCirclePaint = new SKPaint
               {
                   Style = SKPaintStyle.Fill,
                   Color = SKColors.DeepSkyBlue,
                   StrokeWidth=1,
               };

               SKPaint fooLinePaint = new SKPaint
               {
                   Style = SKPaintStyle.Stroke,
                   Color = SKColors.White,
                   StrokeWidth = 1,
               };

               canvas.DrawCircle(100, 100, 100, fooCirclePaint);
               canvas.DrawLine(0, 0, 200, 140, fooLinePaint);
           });

            PaintSurfaceSelfScaleCommand = new DelegateCommand<SKPaintSurfaceEventArgs>(args =>
           {
               SKImageInfo info = args.Info;
               SKSurface surface = args.Surface;
               SKCanvas canvas = surface.Canvas;

               canvas.Clear();

               float fooScale = info.Width / 200.0f;

               SKPaint fooCirclePaint = new SKPaint
               {
                   Style = SKPaintStyle.Fill,
                   Color = SKColors.DeepSkyBlue,
                   StrokeWidth = 1,
               };

               SKPaint fooLinePaint = new SKPaint
               {
                   Style = SKPaintStyle.Stroke,
                   Color = SKColors.White,
                   StrokeWidth = 1,
               };

               canvas.Scale(fooScale);
               canvas.DrawCircle(100, 100, 100, fooCirclePaint);
               canvas.DrawLine(0, 0, 200, 140, fooLinePaint);
           });

        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {

        }

    }
}
  • Android 平台執行結果
    SkiaSharp
  • UWP 平台執行結果
    SkiaSharp

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程