2017/6/27

Xamarin.Forms 教學系列文(十九.壹)Collection Views - Introduction & Picker





學習目標
  • 知道一下 Collection Views 是什麼
  • 三個 View 的差異
  • Picker 嘿嘿嘿

本章節要探討的是 Collection type 對應的 Collection View。

在資訊界,各類型的集合是不可缺少的,所有高階語言都支援陣列 (Arrays) 和結構 (Structure),
這兩個集合互相補足 - 陣列通常是相同類型的資料集,結構則是相關但不同類型的資料集。

為了擴充這些基本集合,.NET 添加了幾個有用的類別:
  • IEnumerable - 允許在資料集中尋覽
  • ICollection - 繼承自 IEnumerable,並增加計數的功能
  • IList - 繼承自 ICollection,支援索引 (Indexing)、 新增 (adding) 和移除 (removing)

回到本章節重點 -

Xamarin.Forms 定義了三種跟資料集合有關的 View:
  • Picker - 可讓使用者 選擇一個子項目,集合項目一般不超過 12 個
  • ListView - 通常有 很多很多 子項目,資料列的呈現通常為 同樣的格式
  • TableView - 通常為各種類型 Cell 的集合,用於顯示數據或管理用戶輸入,像是 各類型的表單應用程式設定


三個 Collection View
都內建捲軸 (Scrolling) 功能。

Picker vs. ListView
子項目都允許被點選,
但 Picker 的子項目僅 限於字串 (String),一般資料集較短
而 ListView 可顯示 任何方式 呈現的物件,通常資料集較長

ListView vs. TableView
ListView 和 TableView 很容易搞混,
因為都有使用 Cell 類別:
ListView 的 Cell 通常顯示 相同類型和相同格式 的物件,
TableView 則是 多種類型 Cell 的集合,且每個 Cell 都是獨立的功能。

若將這三個 View 與 C# 的 Data Type 相對應:
  • Picker - String Array
  • ListView - Object Array,通常是 List<T>,此集合中的各子項目通常會實作 INotifyPropertyChanged Interface (參閱 MVVM 章節)
  • TableView - 可以是一個結構 (Structure),也可以是一個類別 (Class),也可能是一個實作 INotifyPropertyChanged 的 ViewModel 

先從 Picker 來介紹 (這章節有點漫長QQ)~


Picker
Picker 適合用在項目少的集合,且集合的資料內容只能為字串 (String)

底下直接來看範例:
本範例可以先看到兩個屬性:
  1. Title - 就是 Title...
  2. Items - IList<string>類型,通常使用XAML中的 <x:String> 初始化
( Picker沒有 Content property attribute (可隱藏的 Tag),所以需要用 <Picker.Items> )

本範例中 Picker 的資料集為各種鍵盤的類型,選擇後改變上方 Entry 的鍵盤類別。
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="PickerDemo.PickerDemoPage">
    
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <StackLayout Padding="20" Spacing="50">
        
        <Entry x:Name="entry"
               Placeholder="Type something, type anything" />

        <Picker Title="Keyboard Type" 
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.Items>
                <x:String>Default</x:String>
                <x:String>Text</x:String>
                <x:String>Chat</x:String>
                <x:String>Url</x:String>
                <x:String>Email</x:String>
                <x:String>Telephone</x:String>
                <x:String>Numeric</x:String>
            </Picker.Items>
            <Picker.SelectedIndex>
                6
            </Picker.SelectedIndex>
        </Picker>
        
    </StackLayout>
</ContentPage>

執行結果




了解 XAML 配置後,再來看點選時的事件與屬性
- SelectedIndexChanged & SelectedIndex

iOS 按 Done 按鍵 或是 Android 按 OK 按鍵,都會觸發 SelectedIndexChanged 事件。

SelectedIndex 是一個值從零開始的屬性,無選擇時預設值為 -1,SelectedIndex 固定放在 Picker.Items 後面 。

本範例的 SelectedIndexChanged:
會先取得 Picker 的 SelectedIndex,再利用 items[SelectedIndex] 取得對應的項目
之後用到一個神奇的方法,利用 型態集合字串映射成屬性,再去設定 Entry.Keyboard。
public partial class PickerDemoPage : ContentPage
{
    public PickerDemoPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        if (entry == null)
            return;

        Picker picker = (Picker)sender;
        int selectedIndex = picker.SelectedIndex;

        if (selectedIndex == -1)
            return;

        string selectedItem = picker.Items[selectedIndex];

        //神奇的方法
        PropertyInfo propertyInfo = typeof(Keyboard).GetRuntimeProperty(selectedItem);
        entry.Keyboard = (Keyboard)propertyInfo.GetValue(null);
    }
}

執行結果:


Data Binding the Picker

Picker 的 Items 屬性不是 BindableProperty,因此 不能當作 data binding 的目標
Xamairn 2.3.4 新增 ItemSource 屬性可使用,官網資料如下
https://developer.xamarin.com/guides/xamarin-forms/user-interface/picker/populating-itemssource/

底下其實都不重要了,用 ItemSource 和 SelectedItem 就能處理。


若真的需要綁定集合的功能,請直接參考下一節並使用 ListView

我們發現,SelectedIndex 屬性是 TwoWay binding 的 BindableProperty,
看來也只能針對這個屬性做 data binding QQ

但若要在 SelectedIndex 做 data binding,
其值只取得 Index 是不夠用的...即便可以利用 SeletedItem 取得集合內容,問題出在字串類型也不適合作為 data binding 的值…

最好的解決方案是建立一個 Converter 工具,此工具可將 Index 和某 集合 之間做轉換,
而此 集合 內容就是我們要 Binding 的值,同時此集合也必須 完全對應於 Picker 的項目

ObjectToIndexConverter 
在此,我們建立一個 ObjectToIndexConverter 的 泛型 工具轉換 Index 和我們要 data binding 的值,
我們會將要轉換的 集合 放在 XAML 以方便維護。

ObjectToIndexConverter 類別內定義了一個公開屬性和兩個方法:
  • IList<T> items: 要被轉換的 集合。(為了擷取 XAML 上的值被定義為 ContentProperty )
  • Convert:value 是泛型物件,並 回傳 物件在 集合 中的 index
  • ConvertBack:value 是 集合 中的 index,並 回傳 該索引 項目

using System;
using System.Collections.Generic;
using System.Globalization;
using Xamarin.Forms;

namespace Xamarin.FormsBook.Toolkit
{
    [ContentProperty("Items")]
    public class ObjectToIndexConverter : IValueConverter
    {
        public IList Items { set; get; }
        public ObjectToIndexConverter()
        {
            Items = new List();
        }
        public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
        {
            if (value == null || !(value is T) || Items == null)
                return -1;
            return Items.IndexOf((T)value);
        }
        public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
        {
            int index = (int)value;
            if (index < 0 || Items == null || index >= Items.Count)
                return null;
            return Items[index];
        }
    }
}

實際使用 Converter
範例功能是使用 Picker 來為 Label 選擇字體大小:
Picker 是 Target,Label 的屬性是 Source,由於是 TwoWay binding,所以當更改 Target 值時,Source 也會跟著更動。

*Oplatform 寫法已更改,請參考此篇文章
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:toolkit= "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit" 
             x:Class="PickerBinding.PickerBindingPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    <StackLayout Padding="20" Spacing="50">
        <Label x:Name="label" Text="Sample Text" FontSize="16" />
        <Picker Title="Font Size">
            <Picker.Items>
                <x:String>Font Size = 8</x:String>
                <x:String>Font Size = 10</x:String>
                <x:String>Font Size = 12</x:String>
                <x:String>Font Size = 14</x:String>
                <x:String>Font Size = 16</x:String>
                <x:String>Font Size = 20</x:String>
                <x:String>Font Size = 24</x:String>
                <x:String>Font Size = 30</x:String>
            </Picker.Items>
            <Picker.SelectedIndex>
                <!-- Source -->
                <Binding Source="{x:Reference label}" Path="FontSize">
                    
                    <!-- 那個要轉換的 集合 -->
                    <Binding.Converter>
                        <toolkit:ObjectToIndexConverter x:TypeArguments="x:Double">
                            <x:Double>8</x:Double>
                            <x:Double>10</x:Double>
                            <x:Double>12</x:Double>
                            <x:Double>14</x:Double>
                            <x:Double>16</x:Double>
                            <x:Double>20</x:Double>
                            <x:Double>24</x:Double>
                            <x:Double>30</x:Double>
                        </toolkit:ObjectToIndexConverter>
                    </Binding.Converter>
                </Binding>
            </Picker.SelectedIndex>
        </Picker>
    </StackLayout>
</ContentPage>

執行結果:



沒有留言:

張貼留言