XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2019/05/27

使用 PushSharp 來推送 Push Notification 到 Xamarin.Forms 開發的 Android 或者 iOS App 上

使用 PushSharp 來推送 Push Notification 到 Xamarin.Forms 開發的 Android 或者 iOS App 上

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


最近在幫客戶處理 Xamarin.Forms 推播 Push Notification 需求的時候,產生了一個問題,那就是原先的軟體都可以正常收到來自 Apple APNS 的推播訊息,突然間,客戶向我反映,現在正在開發的測試版本 App,突然間收不到來自後台伺服器推送出來的推播訊息,詢問我是不是我這裡程式做了甚麼修正,導致這樣的問題。由於整個系統規劃架構是,當 App 第一次啟動的時候,從來自遠端 PNS 取得了該裝置的 Token,會於使用者登入系統的時候,將此 Token 傳送到遠端 Web API 服務,接著,後端的 Web API 服務,則會將此 Token 再向 Azure Notification Hub 進行註冊;而當有狀況需要推送到 App 的時候,後端 API 服務需要呼叫 Azure Notification Hub 的 SDK,請求 Azure Notification Hub 幫忙向 Apple APNS 推送出這個訊息的推播內容。
因此,首先就透過 Azure Notification Hub 的測試推播功能,發送出一個該 App 使用者可以接收的推播訊息,可是,該 App 還是沒有收到任何推播內容,最後,是在客戶的幫忙,將 App 取得的推播 Token,使用他寫的程式,直接向 Apple APNS 發送推播通知請求,結果,我開發的 App 是可以接收到這個推播通知,所以,可以證明 Xamarin.Forms App 對於來自遠端 Apple APNS 推播內容,是可以正常運作的。
經過這次的經驗,我覺得我這裡還是需要有個這樣的直接對於 Google / Apple 的 PNS 系統,直接送出推播通知請求,以便日後方便針對這樣的問題進行除錯。
要完成這樣的需求,首先需要安裝 PushSharp 的 NuGet 套件,這裡將會建立一個 Console App,完成底下的測試推播程式碼(在該篇文章的最後面)。
在這裡的程式碼將會由 PushSharp 所提供的範例說明程式碼,整理出可以對於 Android / iOS App 推送出 Push Notification 的程式碼,因此,想要對 iOS App 推送出一個推播內容,就可以呼叫 PushiOS 這個方法,而需要將該 App 取得的推播 Token,傳送進去,若想要對 Android App 推送出一個推播內容,就可以呼叫 PushAndroid 這個方法,當然,這裡也需要將該 App 取得的推播 Firebase 的 Token,傳送進去。

針對 iOS 裝置進行訊息推播

在 PushiOS 方法中,這個敘述 string sendMessage = File.ReadAllText("iOS.json", System.Text.Encoding.UTF8); 將會從這個專案目錄下,讀取出要發送出去的推播內容,這裡是一個 JSON 格式內容,如下所示:
C Sharp / C#
{
  "aps": {
    "alert": {
      "title": "推播主題",
      "body": "Notification Hub test notification"
    },
    "sound": "default"
  }
}
透過 apnsBroker.QueueNotification(new ApnsNotification{DeviceToken = deviceid, Payload = JObject.Parse(sendMessage)}); 方法呼叫,可以將指定 App 推播 Token ,也就是 deviceid 這個參數,與上述指定的推播內容,傳送到 Apple APNS ( Apple Push Notification Service ) 上,進而 iOS App 就會收到這個推播通知。
在建立可以用於 Apple APNS 推播的物件時候,會需要使用到 var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox, Constants.APNS_P12FILENAME, Constants.PUSH_CERT_PWD); 敘述,在這裡的第一個參數使用了 ApnsConfiguration.ApnsServerEnvironment.Sandbox ,表示使用的是開發模式下的推播,因此,需要從 Apple Developer 蘋果開發人員網頁中,點選設定的 App ID,下載出該推播憑證檔案,轉換成為 .p12 憑證格式;在這裡是將這個憑證檔案放入測試專案內,指定 輸出到目錄屬性為一律複製,因此,第二個引述將會指向這個檔案所在的路徑,而第三個引數則是要使用這個憑證的時候,需要使用到的密碼。
在這個測試專案中,對於 Android.json (Android 推播內容), iOS.json (iOS 推播內容) 與 com.vulcan.azurehub.p12 (iOS 推播憑證) 都已經放置到該專案內。
想要進行 iOS 的推播測試,需要使用實體裝置,例如,這裡使用的一台 iPhone 手機 (不像 Android 平台,可以使用模擬器來進行測試,只要該模擬器上有安裝 Google Play 軟體即可),底下為進行 iOS 裝置下的推送測試結果,這個範例程式是可以送出讓 iOS App 上收到該推播通知。
在底下,測試了當送出推播訊息之後,App 正好在前景狀態與在背景狀態情境下,所看到收到推播訊息的畫面。

針對 Android 裝置進行訊息推播

在 PushAndroid 方法中,這個敘述 string sendMessage = File.ReadAllText("Android.json", System.Text.Encoding.UTF8); 將會從這個專案目錄下,讀取出要發送出去的推播內容,這裡是一個 JSON 格式內容,如下所示:
C Sharp / C#
{
  "data": {
    "title": "推播主題",
    "message": "Notification Hub test notification"
  }
}
透過 gcmBroker.QueueNotification(new GcmNotification{ RegistrationIds = new List<string> { regId }, Data = JObject.Parse(sendMessage) }); 方法呼叫,可以將指定 App 推播 Token ,也就是 regId 這個參數,與上述指定的推播內容,傳送到 FCM ( Firebase Cloud Messaging ) 上,進而 Google App 就會收到這個推播通知。
在建立可以用於 Apple APNS 推播的物件時候,會需要使用到 var config = new GcmConfiguration(Constants.GCM_SENDER_ID, Constants.AUTH_TOKEN, null); 敘述,第一個引數,GCM_SEND_ID 與第二個引數, AUTH_TOKEN 可以從 Firebase Console 網站中取得。
底下為進行 Android 裝置下的推送測試結果,這個範例程式是可以送出讓 Android App 上收到該推播通知。

發送 iOS / Android 推播的測試程式原始碼

C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        // 進行 iOS 裝置的推播
        PushiOS("ad1942af16e2d85d40fbc7e186555eb276ce67a8112fe5fbe0b6b33a1d404a3f");
        // 進行 Android 裝置的推播
        //PushAndroid("eJTHqO53Tbk:APA91bGUWCleYTdY39Ws1JmbtMnJm80RRsWm8sE5zINsd5YP4dDi_lqp1MHnD38Hr-x7UpIdx0VP3TEuOgpQ7W77bE_c0k4G2FGLK73GAiHsdqEJZhyF8rBIjC7hmfBHOoqteE_cWCbC");
    }

    static void PushiOS(string deviceid)
    {
        // Configuration (NOTE: .pfx can also be used here)
        var config = new ApnsConfiguration(ApnsConfiguration.ApnsServerEnvironment.Sandbox,
            Constants.APNS_P12FILENAME, Constants.PUSH_CERT_PWD);

        // Create a new broker
        var apnsBroker = new ApnsServiceBroker(config);

        // Wire up events
        apnsBroker.OnNotificationFailed += (notification, aggregateEx) =>
        {

            aggregateEx.Handle(ex =>
            {

                // See what kind of exception it was to further diagnose
                if (ex is ApnsNotificationException notificationException)
                {

                    // Deal with the failed notification
                    var apnsNotification = notificationException.Notification;
                    var statusCode = notificationException.ErrorStatusCode;

                    Console.WriteLine($"Apple Notification Failed: ID={apnsNotification.Identifier}, Code={statusCode}");

                }
                else
                {
                    // Inner exception might hold more useful information like an ApnsConnectionException            
                    Console.WriteLine($"Apple Notification Failed for some unknown reason : {ex.InnerException}");
                }

                // Mark it as handled
                return true;
            });
        };

        apnsBroker.OnNotificationSucceeded += (notification) =>
        {
            Console.WriteLine("Apple Notification Sent!");
        };

        // Start the broker
        apnsBroker.Start();

        string sendMessage = File.ReadAllText("iOS.json", System.Text.Encoding.UTF8);

        // Queue a notification to send
        apnsBroker.QueueNotification(new ApnsNotification
        {
            DeviceToken = deviceid,
            Payload = JObject.Parse(sendMessage)
        });

        // Stop the broker, wait for it to finish   
        // This isn't done after every message, but after you're
        // done with the broker
        apnsBroker.Stop();
    }

    static void PushAndroid(string regId)
    {
        // Configuration GCM (use this section for GCM)
        var config = new GcmConfiguration(Constants.GCM_SENDER_ID, Constants.AUTH_TOKEN, null);
        var provider = "GCM";

        // Configuration FCM (use this section for FCM)
        // var config = new GcmConfiguration("APIKEY");
        // config.GcmUrl = "https://fcm.googleapis.com/fcm/send";
        // var provider = "FCM";

        // Create a new broker
        var gcmBroker = new GcmServiceBroker(config);

        // Wire up events
        gcmBroker.OnNotificationFailed += (notification, aggregateEx) =>
        {

            aggregateEx.Handle(ex =>
            {

                // See what kind of exception it was to further diagnose
                if (ex is GcmNotificationException notificationException)
                {

                    // Deal with the failed notification
                    var gcmNotification = notificationException.Notification;
                    var description = notificationException.Description;

                    Console.WriteLine($"{provider} Notification Failed: ID={gcmNotification.MessageId}, Desc={description}");
                }
                else if (ex is GcmMulticastResultException multicastException)
                {

                    foreach (var succeededNotification in multicastException.Succeeded)
                    {
                        Console.WriteLine($"{provider} Notification Succeeded: ID={succeededNotification.MessageId}");
                    }

                    foreach (var failedKvp in multicastException.Failed)
                    {
                        var n = failedKvp.Key;
                        var e = failedKvp.Value;

                        Console.WriteLine($"{provider} Notification Failed: ID={n.MessageId}, Desc={e.Message}");
                    }

                }
                else if (ex is DeviceSubscriptionExpiredException expiredException)
                {

                    var oldId = expiredException.OldSubscriptionId;
                    var newId = expiredException.NewSubscriptionId;

                    Console.WriteLine($"Device RegistrationId Expired: {oldId}");

                    if (!string.IsNullOrWhiteSpace(newId))
                    {
                        // If this value isn't null, our subscription changed and we should update our database
                        Console.WriteLine($"Device RegistrationId Changed To: {newId}");
                    }
                }
                else if (ex is RetryAfterException retryException)
                {

                    // If you get rate limited, you should stop sending messages until after the RetryAfterUtc date
                    Console.WriteLine($"{provider} Rate Limited, don't send more until after {retryException.RetryAfterUtc}");
                }
                else
                {
                    Console.WriteLine("{provider} Notification Failed for some unknown reason");
                }

                // Mark it as handled
                return true;
            });
        };

        gcmBroker.OnNotificationSucceeded += (notification) =>
        {
            Console.WriteLine("{provider} Notification Sent!");
        };

        // Start the broker
        gcmBroker.Start();

        string sendMessage = File.ReadAllText("Android.json", System.Text.Encoding.UTF8);

        // Queue a notification to send
        gcmBroker.QueueNotification(new GcmNotification
        {
            RegistrationIds = new List<string> { regId },
            Data = JObject.Parse(sendMessage)
        });

        // Stop the broker, wait for it to finish   
        // This isn't done after every message, but after you're
        // done with the broker
        gcmBroker.Stop();
    }
}

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




2019/04/26

使用 StackLayout 配合 BindableLayout.ItemTemplateSelector 建立一個動態資料樣板的呈現的效果

使用 StackLayout 配合 BindableLayout.ItemTemplateSelector 建立一個動態資料樣板的呈現的效果

在 Xamarin.Forms 3.5 版本推出的時候,有一個相當好用的功能,那就是 BindableLayout ,有興趣的人可以參考 Xamarin.Forms 3.5: A Little Bindable Love 這篇文章,只要是版面配置繼承於 Layout ,都可以使用 BindableLayout 這個附加屬性 Attached Property,這包括了:AbsoluteLayuot, FlexLayout, Grid, RelativeLayout, StackLayout。
在這篇文章中,將要來練習使用 StackLayout 這個版面配置,但是不會在 StackLayout 版面配置內指定這些仔檢視,而是透過 BindableLayout.ItemsSource 來指定要顯示在 StackLayout 內的子檢視 View 的物件,並且每個子檢視的樣貌都會不相同,會依據當時該物件值來決定要顯示甚麼樣貌,也就是要顯示哪個 DataTemplate,所以,這裡將會使用 BindableLayout.ItemTemplateSelector
該文件的專案原始碼可以透過 GitHub 來取得

建立一個 使用 StackLayout 專案

  • 開啟 Visual Studio 2019 程式
  • 當 Visual Studio 2019 開始 視窗 出現之後,請點選左下角的 [建立新專案] 選項
  • 當 [建立新專案] 對話窗出現之後,請在中間最上方的搜尋文字輸入盒中輸入 [prism] 關鍵字,搜尋所有與 Prism 有關的專案樣板
  • 請選擇 [Prism Blank App (Xamarin.Forms)] 這個專案樣板
  • 當出現 [設定新的專案] 對話窗,請在 [專案名稱] 輸入 [XF3003]
  • 最後點選該對話窗右下方的 [建立] 按鈕
  • 現在將會看到 [PRISM PROJECT WIZARD] 對話窗,請勾選 ANDROID, iOS, UWP 三個行動裝置平台,接著在底下 [Container] 下拉選單,選擇 Unity 項目
  • 最後,點選 [CREATE PROJECT] 按鈕,以便產生 Xamarin.Forms 專案

安裝需要用到的 PropertyChanged.Fody NuGet 套件

  • 當這個 Xamarin.Forms 專案建立成功之後,請在該方案中,找到 Xamarin.Forms 使用的專案(這是一個 .NET Standard 類別庫,簡稱為 SCL ),請在該專案中,使用滑鼠右擊 [相依性] 節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: XXX] 視窗中,點選 [瀏覽] 標籤頁次,並且在下方的搜尋文字輸入盒中,輸入 [propertychanged.fody] 關鍵字,搜尋出這個 NuGet 套件
  • 當出現 [PropertyChanged.Fody] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內
  • 請查看 Xamarin.Forms 專案內,並沒有 [FodyWeavers.xml] 這個檔案,因此,使用滑鼠右擊 Xamarin.Forms 專案節點,選擇 [建置] 選項
  • 當建置完成之後,在這個 Xamarin.Forms 專案內將會出現 [FodyWeavers.xml] 檔案

升級 Xamarin.Forms 套件到大於 3.5 板本以上

因為 BindableLayout 將會在 Xamarin.Forms 3.5 版才有支援,因此,現在這個時間點,將會升級到最新的 3.6 版本
  • 使用滑鼠右擊方案節點(方案總管最上方的那個節點),選擇 [管理方案的 NuGet 套件]
  • 點選 [更新] 標籤頁次
  • 勾選該標籤頁次內的所有項目
  • 點選右上方的更新按鈕,就可以升級這些套件到最新版本了

建立資料模型

  • 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
  • 將新增資料夾的名稱設定為 [Models]
  • 滑鼠右擊剛剛建立的 [Models] 資料夾,選擇 [加入] > [類別]
  • 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [ItemBlock]
  • 點選右下方的 [新增] 按鈕
  • 將底下程式碼填入到這個新建立的類別檔案內
C Sharp / C#
namespace XF3003.Models
{
    public enum ItemBlockTypeEnum
    {
        Label,
        BoxView,
        Entry,
    }
    public class ItemBlock
    {
        public ItemBlockTypeEnum ShowViewType { get; set; }
        public string LabelText { get; set; }
        public int CountIndex { get; set; }
    }
}

建立資料樣板選擇器的類別

  • 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
  • 將新增資料夾的名稱設定為 [DataTemplateSelectors]
  • 滑鼠右擊剛剛建立的 [DataTemplateSelectors] 資料夾,選擇 [加入] > [類別]
  • 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [MyItemTemplateSelector]
  • 點選右下方的 [新增] 按鈕
  • 將底下程式碼填入到這個新建立的類別檔案內
這個 MyItemTemplateSelector 類別將需要繼承 DataTemplateSelector 這個類別,並且要覆寫實作出 OnSelectTemplate 這個方法,而這個方法的主要作用在於會接收一個型別為 object 的 item 物件,該物件就是當時要顯示在螢幕上的物件,這個時候,可以根據該物件屬性值,決定當時要顯示出哪個資料樣板 DataTemplate,如此,就可以做出動態顯示不同檢視項目的效果了。
在這個類別類,將會宣告三個 DataTemplate 屬性,這三個 DataTemplate 屬性將會在該頁面的 XAML 中來宣告與指定,因此,在這裡將不會有任何的定義。
C Sharp / C#
public class MyItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate LabelDataTemplate { get; set; }
    public DataTemplate BoxViewDataTemplate { get; set; }
    public DataTemplate EntryDataTemplate { get; set; }
    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        ItemBlock foo = (ItemBlock)item;
        if (foo.ShowViewType == ItemBlockTypeEnum.Label)
        {
            return LabelDataTemplate;
        }
        else if (foo.ShowViewType == ItemBlockTypeEnum.BoxView)
        {
            return BoxViewDataTemplate;
        }
        else
        {
            return EntryDataTemplate;
        }
    }
}

建立使用 BindableLayout.ItemTemplateSelector 的頁面與商業邏輯

  • 在 [Views] 資料夾內,打開 [MainPage.xaml] 檔案
  • 修正使用底下的 XAML 語言宣告
在這裡將會看到在 StackLayout 版面配置內,並沒有指定任何的子檢視,而是透過 BindableLayout.ItemsSource 來指定要顯示的物件,而該物件要顯示那些 XAML 檢視項目,這裡是透過 BindableLayout.ItemTemplateSelector 來決定。
設計技巧就是要使用到 XAML 的 資源 Resource,在此會使用 ContentPage.Resources 來宣告這些資源;在前面提到過的三種資料樣板 LabelDataTemplate BoxViewDataTemplate EntryDataTemplate 將會在這裡進行定義,另外,也需要建立一個 XAML 命名空間 xmlns:LocalDataTemplates="clr-namespace:XF3003.DataTemplateSelectors" ,將會透過該命名空間可以參考到上面所設計的 MyItemTemplateSelector 類別,也會在這裡分別指定該類別中的三個屬性所要參考的物件值。
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:LocalDataTemplates="clr-namespace:XF3003.DataTemplateSelectors"
             x:Class="XF3003.Views.MainPage"
             Title="StackLayout的ItemTemplateSelector 應用練習">

    <ContentPage.Resources>
        <ResourceDictionary>
            <DataTemplate x:Key="labelTemplate">
                <Grid>
                    <Label Text="{Binding LabelText}"/>
                </Grid>
            </DataTemplate>
            <DataTemplate x:Key="boxViewTemplate">
                <Grid>
                    <BoxView Color="LightPink"/>
                </Grid>
            </DataTemplate>
            <DataTemplate x:Key="entryTemplate">
                <Grid>
                    <Entry Placeholder="{Binding LabelText}"/>
                </Grid>
            </DataTemplate>
            <LocalDataTemplates:MyItemTemplateSelector 
                x:Key="myItemTemplateSelector"
                LabelDataTemplate="{StaticResource labelTemplate}"
                BoxViewDataTemplate="{StaticResource boxViewTemplate}"
                EntryDataTemplate="{StaticResource entryTemplate}"/>
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid>
        <StackLayout
            Orientation="Vertical"
            BindableLayout.ItemsSource="{Binding myItemList}"
            BindableLayout.ItemTemplateSelector="{StaticResource myItemTemplateSelector}">
        </StackLayout>
    </Grid>
</ContentPage>
  • 在 [ViewModels] 資料夾內,打開 [MainPageViewModel.xaml] 檔案
  • 修正使用底下的 C# 程式碼
C Sharp / C#
namespace XF3003.ViewModels
{
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using Prism.Events;
    using Prism.Navigation;
    using Prism.Services;
    using XF3003.Models;

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public ObservableCollection<ItemBlock> myItemList { get; set; } = new ObservableCollection<ItemBlock>();
        private readonly INavigationService navigationService;

        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;

        }

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Label,
                LabelText = "User Account"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Entry,
                LabelText = "Please Enter Account"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Label,
                LabelText = "User Password"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.Entry,
                LabelText = "Please Enter Password"
            });
            myItemList.Add(new ItemBlock()
            {
                ShowViewType = ItemBlockTypeEnum.BoxView,
            });
        }

        public void OnNavigatingTo(INavigationParameters parameters)
        {
        }

    }
}

執行結果




2019/04/25

使用 CollectionView 建立一個能以 GridView 呈現的效果

使用 CollectionView 建立一個能以 GridView 呈現的效果

CollectionView 現階段還是在 Preview 階段,當 Xamarin.Forms 4.0 (現階段 Xamarin.Forms 的版本是 3.6) 正式推出的時候,將可以使用這個 CollectionView 檢視。這個檢視除了具有 ListView 的功能之外,它還可以使用 GridView 的模式來顯示出每筆資料,現在,就來實際建立這個專案來體驗設計過程吧。
該文件的專案原始碼可以透過 GitHub 來取得

建立一個 使使用 FlexLayout 專案

  • 開啟 Visual Studio 2019 程式
  • 當 Visual Studio 2019 開始 視窗 出現之後,請點選左下角的 [建立新專案] 選項
  • 當 [建立新專案] 對話窗出現之後,請在中間最上方的搜尋文字輸入盒中輸入 [prism] 關鍵字,搜尋所有與 Prism 有關的專案樣板
  • 請選擇 [Prism Blank App (Xamarin.Forms)] 這個專案樣板
  • 當出現 [設定新的專案] 對話窗,請在 [專案名稱] 輸入 [XF3002]
  • 最後點選該對話窗右下方的 [建立] 按鈕
  • 現在將會看到 [PRISM PROJECT WIZARD] 對話窗,請勾選 ANDROID, iOS 這兩個行動裝置平台,接著在底下 [Container] 下拉選單,選擇 Unity 項目
  • 最後,點選 [CREATE PROJECT] 按鈕,以便產生 Xamarin.Forms 專案

安裝需要用到的 PropertyChanged.Fody NuGet 套件

  • 當這個 Xamarin.Forms 專案建立成功之後,請在該方案中,找到 Xamarin.Forms 使用的專案(這是一個 .NET Standard 類別庫,簡稱為 SCL ),請在該專案中,使用滑鼠右擊 [相依性] 節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: XXX] 視窗中,點選 [瀏覽] 標籤頁次,並且在下方的搜尋文字輸入盒中,輸入 [propertychanged.fody] 關鍵字,搜尋出這個 NuGet 套件
  • 當出現 [PropertyChanged.Fody] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內
  • 請查看 Xamarin.Forms 專案內,並沒有 [FodyWeavers.xml] 這個檔案,因此,使用滑鼠右擊 Xamarin.Forms 專案節點,選擇 [建置] 選項
  • 當建置完成之後,在這個 Xamarin.Forms 專案內將會出現 [FodyWeavers.xml] 檔案

安裝需要用到的 Xamarin.Essentials NuGet 套件

  • 在 [NuGet: XXX] 視窗中,搜尋文字輸入盒中,輸入 [Xamarin.Essentials] 關鍵字,搜尋出這個 NuGet 套件
  • 當出現 [Xamarin.Essentials] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內

修正因為安裝 Xamarin.Essentials 帶來的錯誤

現在,可以從 Visual Studio 2019 的錯誤視窗中,看到底下的錯誤訊息
錯誤    NU1107    偵測到 Xamarin.Android.Support.Compat 有版本衝突。請將 Xamarin.Android.Support.Compat 28.0.0.1 直接安裝/參考到專案 XF3002.Android 來解決此問題。 
 XF3002.Android -> XF3002 -> Xamarin.Essentials 1.1.0 -> Xamarin.Android.Support.Compat (>= 28.0.0.1) 
 XF3002.Android -> Xamarin.Android.Support.Design 27.0.2.1 -> Xamarin.Android.Support.Compat (= 27.0.2.1).    XF3002.Android    D:\Vulcan\GitHub\XCourse19\XF3002\XF3002\XF3002.Android\XF3002.Android.csproj    1
想要解決此一問題:
  • 使用滑鼠右擊方案節點(方案總管最上方的那個節點),選擇 [管理方案的 NuGet 套件]
  • 點選 [更新] 標籤頁次
  • 勾選該標籤頁次內的所有項目
  • 點選右上方的更新按鈕,就可以升級這些套件到最新版本了

升級 Xamarin.Forms 套件到 4.0 版本

  • 同樣的,使用滑鼠右擊方案節點(方案總管最上方的那個節點),選擇 [管理方案的 NuGet 套件]
  • 點選 [已安裝] 標籤頁次
  • 點選 [Xamarin.Forms] 套件項目
  • 勾選 [包括搶鮮版] 檢查盒,要求顯示搶鮮版的套件清單
  • 在右方的 Xamarin.Forms 套件清單中,勾選所有的專案
  • 在下方的版本下拉選單中,選擇 4.0 以上的版本,現階段只能夠看到 4.0.0.346134-pre9 這個版本
  • 點選右下方 [安裝] 按鈕,將這個 4.0 的 Xamarin.Forms 套件安裝到所有專案內

建立資料模型

  • 滑鼠右擊 Xamarin.Forms 專案,選擇 [加入] > [新增資料夾]
  • 將新增資料夾的名稱設定為 [Models]
  • 滑鼠右擊剛剛建立的 [Models] 資料夾,選擇 [加入] > [類別]
  • 在 [新增項目] 對話窗下方的 [名稱] 欄位中,輸入 [MyData]
  • 點選右下方的 [新增] 按鈕
  • 將底下程式碼填入到這個新建立的類別檔案內
C Sharp / C#
public class MyData : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Name { get; set; }
    public int OrderId { get; set; }
}

修正該 Android 專案的 MainActivity.cs

因為現階段 CollectionView 還是在預覽階段,所以,需要在原生專案的進入點內,要執行 Xamarin.Forms.Forms.Init 這個方法前,要增加先執行底下的方法。
  • 打開 [MainActivity.cs] 檔案
  • 將第 18 行,此行沒有任何程式碼,在此加入這個 C# 敘述 Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental");

建立使用 CollectionView 的頁面與商業邏輯

  • 在 [Views] 資料夾內,打開 [MainPage.xaml] 檔案
  • 修正使用底下的 XAML 語言宣告
在這裡的 CollectionView 將會透過 [ItemsSource] 這個可綁定屬性,從 ViewModel 物件內取得要顯示的清單集合資料,並且使用 [SelectionMode] 屬性,設定使用者僅能夠使用單選的方式來選取,最後透過 [SelectionChangedCommand] 這個命令,綁定到 ViewModel 內的 ICommand 屬性,也就是當使用者點選任一項目之後,將會觸發這個命令。
在 CollectionView 內,可以使用 [CollectionView.ItemsLayout] 來設定此次要顯示的資料格式,在這裡將會使用 [GridItemsLayout] 這個屬性來指定使用 GridView 的模式來呈現,並且是以垂直方式來顯示資料。
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="XF3002.Views.MainPage"
             Title="CollectionView 的應用練習">

    <StackLayout
        >
        <CollectionView
            x:Name="cv"
            ItemsSource="{Binding myItemList}"
            SelectionMode="Single"
            SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
            SelectionChangedCommand="{Binding SelectionChangedCommand}"
            >
            <CollectionView.ItemsLayout>
                <GridItemsLayout Orientation="Vertical" Span="2" />
            </CollectionView.ItemsLayout>
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Frame BorderColor="LightGray" CornerRadius="3" HasShadow="False">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="70"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="150"/>
                            </Grid.ColumnDefinitions>
                            <Label
                                HorizontalOptions="Start"
                                Text="{Binding Name}"
                                FontAttributes="Bold"
                                FontSize="20"/>
                            <Label
                                HorizontalOptions="End" VerticalOptions="End"
                                Text="{Binding OrderId}"
                                TextColor="Red"
                                FontSize="16"/>
                        </Grid>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </StackLayout>

</ContentPage>
  • 在 [ViewModels] 資料夾內,打開 [MainPageViewModel.xaml] 檔案
  • 修正使用底下的 C# 程式碼
C Sharp / C#
using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace XF3002.ViewModels
{
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using Prism.Events;
    using Prism.Navigation;
    using Prism.Services;
    using XF3002.Models;

    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public ObservableCollection<MyData> myItemList { get; set; } = new ObservableCollection<MyData>();
        public MyData SelectedItem { get; set; }
        public int GridItemsLayoutSpan { get; set; } = 2;
        public DelegateCommand SelectionChangedCommand { get; set; }
        private readonly INavigationService navigationService;
        private readonly IPageDialogService dialogService;

        public MainPageViewModel()
        {
            ReadData();
        }
        public MainPageViewModel(INavigationService navigationService, IPageDialogService dialogService)
        {
            this.navigationService = navigationService;
            this.dialogService = dialogService;
            SelectionChangedCommand = new DelegateCommand(() =>
            {
                dialogService.DisplayAlertAsync("Info", SelectedItem.Name, "OK");
            });
        }

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
            ReadData();
        }

        public void OnNavigatingTo(INavigationParameters parameters)
        {
        }
        public void ReadData()
        {
            int cc = 1;
            for (int i = 0; i < 10; i++)
            {
                myItemList.Add(new MyData() { Name = "Baboon", OrderId=cc++ });
                myItemList.Add(new MyData() { Name = "Capuchin Monkey", OrderId = cc++ });
                myItemList.Add(new MyData() { Name = "Blue Monkey", OrderId = cc++ });
                myItemList.Add(new MyData() { Name = "Squirrel Monkey", OrderId = cc++ });
                myItemList.Add(new MyData() { Name = "Golden Lion Tamarin", OrderId = cc++ });
                myItemList.Add(new MyData() { Name = "Howler Monkey", OrderId = cc++ });
                myItemList.Add(new MyData() { Name = "Japanese Macaque", OrderId = cc++ });
            }
        }
    }
}

執行結果