XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2017/04/30

Xamarin.Forms 中,關聯式可綁定 Bindable Picker 練習

在 2017.2.27 看到一篇文章 New Bindable Picker Control for Xamarin.Forms,提到了 Xamarin.Forms 2.3.4 版本,將會提供可綁定的 Picker 控制項更新,剛好今天有空,就順手做了一下測試。
這事是要做個關聯連動式的 Picker 應用,在這個手機頁面中,將會有兩個 Picker
  • 第一個 Picker 將會是主分類的選單
  • 第二個 Picker 則會是次分類選單
使用者於點選完成主分類選單之後,次分類選單的 Picker 可以選擇的項目,將會根據主分類選單的選擇項目,自動產生出來;也就是說,第二個 Picker 內可以選擇的清單項目,會與第一個 Picker 所選擇的結果,產生連動的關係。
因此,立即使用 Xamarin.Forms 2.3.4 最新版的套件進行撰寫 View / ViewModel,可是,突然發現到,Xamarin.Forms 2.3.4 所提供的可綁定 Picker,卻沒有相對應的 Command 可以來設定,若要做到上述的功能,還是要繼續使用事件的方式,在 Code Behind 內寫相關的程式碼。
無奈之下,只好找回之前從網路上找到的可綁定 Picker(這個客製化控制項,提供了 SelectedItemCommand,當使用者點選不同 Picker 項目後,將會執行這個命令)
這個練習中的專案原始碼,您可以在底下 GitHub 中找到
底下將會說明如何做到這樣的功能:

建立一個可綁定Picker的自訂控制項 (Custom Control)

由於,我們只是要擴增原有 Picker 的功能,讓其切換選擇項目之後,可以執行命令,因此,我們這裡需要先建立這個自訂控制項,不過,因為該自訂控制項在各原生平台下所呈現的視覺不會有任何改變,所以,我們也不需要在每個平台,撰寫任何 Renderer 程式碼。
底下將會是這個可執行命令的可綁定 Picker 自訂控制項原始碼。
    public class BindablePicker : Picker
    {
        public static void Init()
        {

        }

        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource",
                    typeof(IEnumerable), typeof(BindablePicker), null,
                    //propertyChanged: OnItemsSourceChanged);
                    propertyChanged: (bindable, oldvalue, newvalue) => ((BindablePicker)bindable).OnItemsSourceChanged(bindable, oldvalue, newvalue));

        //propertyChanged: (bindable, oldvalue, newvalue) => ((WrapView)bindable).ItemsSource_OnPropertyChanged(bindable, oldvalue, newvalue));


        public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create("SelectedItem",
                    typeof(IEnumerable), typeof(BindablePicker), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);

        public static readonly BindableProperty SelectedItemCommandProperty = BindableProperty.Create("SelectedItemCommand",
            typeof(ICommand), typeof(BindablePicker), null);

        public BindablePicker()
        {
            SelectedIndexChanged += (o, e) =>
            {
                if (SelectedIndex < 0 || ItemsSource == null || !ItemsSource.GetEnumerator().MoveNext())
                {
                    SelectedItem = null;
                    return;
                }

                var index = 0;
                foreach (var item in ItemsSource)
                {
                    if (index == SelectedIndex)
                    {
                        SelectedItem = item;
                        break;
                    }
                    index++;
                }
            };
        }

        public ICommand SelectedItemCommand
        {
            get { return (ICommand)GetValue(SelectedItemCommandProperty); }
            set { SetValue(SelectedItemCommandProperty, value); }
        }

        public IEnumerable ItemsSource
        {
            get
            {
                return (IEnumerable)GetValue(ItemsSourceProperty);
            }
            set
            {
                SetValue(ItemsSourceProperty, value);
            }
        }

        public Object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set
            {
                if (SelectedItem != value)
                {
                    SetValue(SelectedItemProperty, value);
                    InternalUpdateSelectedIndex();

                    if (SelectedItemCommand != null)
                    {
                        SelectedItemCommand.Execute(value);
                    }
                }
            }
        }

        public event EventHandler<SelectedItemChangedEventArgs> ItemSelected;

        private void InternalUpdateSelectedIndex()
        {
            var selectedIndex = -1;
            if (ItemsSource != null)
            {
                var index = 0;
                foreach (var item in ItemsSource)
                {
                    if (item != null && item.Equals(SelectedItem))
                    {
                        selectedIndex = index;
                        break;
                    }
                    index++;
                }
            }
            SelectedIndex = selectedIndex;
        }

        public BindablePicker KeepBindablePicker = null;
        private void OnItemsSourceChanged(BindableObject bindable, object oldval, object newval)
        {
            var boundPicker = (BindablePicker)bindable;
            KeepBindablePicker = boundPicker;
            var oldvalue = oldval as IEnumerable;
            var newvalue = newval as IEnumerable;


            if (oldvalue != null)
            {
                var observableCollection = oldvalue as INotifyCollectionChanged;

                // Unsubscribe from CollectionChanged on the old collection
                if (observableCollection != null)
                    observableCollection.CollectionChanged -= OnCollectionChanged;
            }

            if (newvalue != null)
            {
                var observableCollection = newvalue as INotifyCollectionChanged;

                // Subscribe to CollectionChanged on the new collection 
                //and fire the CollectionChanged event to handle the items in the new collection
                if (observableCollection != null)
                    observableCollection.CollectionChanged += OnCollectionChanged;
            }

            boundPicker.InternalUpdateSelectedIndex();
        }

        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            if (KeepBindablePicker == null)
            {
                return;
            }

            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (var item in args.NewItems)
                    {
                        KeepBindablePicker.Items.Add(item as string);
                    }
                    break;
                case NotifyCollectionChangedAction.Move:
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (var item in args.OldItems)
                    {
                        KeepBindablePicker.Items.Remove(item as string);
                    }
                    break;
                case NotifyCollectionChangedAction.Replace:
                    KeepBindablePicker.Items[args.NewStartingIndex] = args.NewItems[0] as string;
                    break;
                case NotifyCollectionChangedAction.Reset:
                    KeepBindablePicker.Items.Clear();
                    break;
                default:
                    break;
            }
        }

        private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var boundPicker = (BindablePicker)bindable;
            if (boundPicker.ItemSelected != null)
            {
                boundPicker.ItemSelected(boundPicker, new SelectedItemChangedEventArgs(newValue));
            }
            boundPicker.InternalUpdateSelectedIndex();
        }

    }

宣告要測試的頁面 XAML 內容

由於我們需要引用我們設計的自訂控制項,因此,需要加入一個額外命名空間,指向到這個自訂控制項的 .NET 命名空間中。
xmlns:customControl="clr-namespace:XFCorelPicker.CustomControls"
接者,就可以使用 customControl 命名空間前置詞,引用我們開發的自訂控制項 BindablePicker
在這裡,
  • 我們定義了 SelectedItem 屬性,綁訂到 ViewModel 內的 .NET 屬性 SelectedMainCategory,這樣若想要知道使用者點選了哪個項目的時候,在 ViewModel 內,只需要查看這個 .NET 屬性即可。
  • 我們定義了 ItemsSource 屬性,綁訂到 ViewModel 內的 .NET 屬性 MainCategoryList,這樣我們便可以在 ViewModel 內,指定這個 Picker 可以選擇的項目清單內容。
  • 我們定義了 SelectedItemCommand 屬性,綁訂到 ViewModel 內的 .NET 屬性 MainCategoryChangeCommand 命令,這樣,當使用者選擇了不同的項目時候,就會執行呼叫這個命令。
        <customControl:BindablePicker
            Title="請選擇主分類"
            SelectedItem="{Binding SelectedMainCategory}"
            ItemsSource="{Binding MainCategoryList}"
            SelectedItemCommand="{Binding MainCategoryChangeCommand}"
            TextColor="Red"
            />
<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:customControl="clr-namespace:XFCorelPicker.CustomControls"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFCorelPicker.Views.MainPage"
             Title="關聯式可綁定Picker Lab">
    <StackLayout
        Margin="30,0"
        HorizontalOptions="FillAndExpand" VerticalOptions="Center">
        <Label
            Text="{Binding fooMyTask.Name}"/>
        <customControl:BindablePicker
            Title="請選擇主分類"
            SelectedItem="{Binding SelectedMainCategory}"
            ItemsSource="{Binding MainCategoryList}"
            SelectedItemCommand="{Binding MainCategoryChangeCommand}"
            TextColor="Red"
            />
        <customControl:BindablePicker
            Title="請選擇次分類"
            SelectedItem="{Binding SelectedSubCategory}"
            ItemsSource="{Binding SubCategoryList}"
            TextColor="Red"
            />
        <Button
            Text="變更工作名稱"
            Command="{Binding 變更工作名稱Command}"
            />
    </StackLayout>
</ContentPage>

設計 ViewModel 內容

當 MainCategoryChangeCommand 命令執行的時候,我們便會重新定義 SubCategoryList 這個物件的集合項目內容,接著,透過資料綁定模式,頁面中的次分類 Picker,就會有與主分類選擇項目相關的清單可以選擇了。
            MainCategoryChangeCommand = new DelegateCommand(() =>
            {
                SubCategoryList.Clear();
                for (int i = 0; i < 50; i++)
                {
                    SubCategoryList.Add($"{SelectedMainCategory} - {i}");
                }
            });

補充說明

若您想要使用 Xamarin.Forms 2.3.4 的 Picker 做到同樣的效果,而不使用自訂 Picker 控制項,您可以使用上一篇文章 在 Xamarin.Forms 中,如何使用 Prism EventToCommandBehavior 提供的事件轉換到命令的行為 所提到的 EventToCommandBehavior 做法;也就是,當主分類的 Picker 的 SelectedIndexChanged 事件觸發的時候,就會執行 MainCategoryChangeCommand 命令。
實際的 View 宣告與 ViewModel 的程式碼邏輯,可以參考 EventToCommandBehaviorPage.xaml / EventToCommandBehaviorPageViewModel.cs 這兩個檔案。

2017/04/29

在 Xamarin.Forms 中,如何使用 Prism EventToCommandBehavior 提供的事件轉換到命令的行為

這是升級到 Prism 6.3 版本之後,Prism 開發框架提供了許多新的功能,其中一個那就是 EventToCommandBehavior 這個功能,它提供了將 XAML 控制項的指定事件,當這個事件觸發的時候,會執行所設定的命令。
在以往,我們要使用這樣功能的時候,都會使用其他的 NuGet 套件,其中,將利用 Blend SDK 做出來的套件,則是更加好用;可是,往往在進行企業內部跨平台應用開發專案的時候,其實,會用到的這些行為,EventToCommandBehavior 則是最多、最實用的一個。
現在,只要您有使用 Prism 開發框架,就可以直接使用這個功能。
在這篇文章中,將會設計一個頁面,這個頁面上會有兩個按鈕,分別會使用 Prism 的 EventToCommandBehavior 將按鈕的 Clicked 事件,設定當這個事件觸發的時候,會執行同一個命令。在這裡,為了要能夠區分出是哪個按鈕所按下的,因此,使用了命令綁定中的 CommandParameter 來做分辨。
這篇文章中所使用的專案範例原始檔案,您可以從這裡取得

頁面 XAML

頁面中的 XAML 內容相當的簡單,要使用 Prism 的 EventToCommandBehavior 功能,首先,您需要宣告一個命名空間,如此,才能夠在其他的 XAML 控制項中,將 Prism 所提供的 EventToCommandBehavior 行為,加入到控制項的 Behaviors 集合內。
xmlns:behavior="clr-namespace:Prism.Behaviors;
在 XAML 中的每個 視覺項目 Visual Element 都會有個 Behaviors 屬性,我們可以將您所要指定的行為,加入到這個屬性中即可。
在底下的兩個按鈕控制項中,我們使用了這個方式,將 Prism 的EventToCommandBehavior加入到這兩個按鈕中。
在按鈕1中,使用了
  • EventName
    指定何種事件要被觸發的時候,才會執行這個行為
  • Command
    事件被觸發之後,要執行 ViewModel 內的命令是哪個
  • CommandParameter
    執行這個命令的時候,是否要傳遞哪個引數到命令中
其中,在按鈕1 中的 CommandParameter,我們使用了資料綁定的方式,指定要傳遞過去的物件值為所指定被綁定的屬性。
                <behavior:EventToCommandBehavior
                    EventName="Clicked"
                    Command="{Binding 請按下我Command}"
                    CommandParameter="{Binding 請按下我1}"/>
其中,在按鈕1 中的 CommandParameter,我們將要傳送過去的內容,直接使用字串的方式寫在 XAML 上;在這裡要表達的是,CommandParameter 的內容,可以使用資料綁定或者直接設定的方式來提供命令所要接收的參數內容。
                <behavior:EventToCommandBehavior
                    EventName="Clicked"
                    Command="{Binding 請按下我Command}"
                    CommandParameter="請按下我2"/>

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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFPrismBehavior.Views.MainPage"
             Title="MainPage">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding Title}" />

        <Button
           Text="{Binding 請按下我1}"
           >
            <Button.Behaviors>
                <behavior:EventToCommandBehavior
                    EventName="Clicked"
                    Command="{Binding 請按下我Command}"
                    CommandParameter="{Binding 請按下我1}"/>
            </Button.Behaviors>
        </Button>

        <Button
           Text="{Binding 請按下我2}"
           >
            <Button.Behaviors>
                <behavior:EventToCommandBehavior
                    EventName="Clicked"
                    Command="{Binding 請按下我Command}"
                    CommandParameter="請按下我2"/>
            </Button.Behaviors>
        </Button>

    </StackLayout>
</ContentPage>

頁面所使用到的 ViewModel

在 ViewModel 中,我們需要定義綁定在 View 中的命令物件,在這裡,我們需要使用 DelegateCommand 的泛型型別來宣告,由於這個命令要傳送過來的參數型別為 string,因此,在泛型中就設定了 string 型別。
        public DelegateCommand<string> 請按下我Command { get; set; }
在定義與產生這個命令物件的時候,我們可以透過該 DelegateCommand 泛型型別,取得 XAML 命令中所指定的 CommandParameter 值;在這個範例中,我們使用了 Prism 的 IPageDialogService 服務,將內容顯示在螢幕上。
            請按下我Command = new DelegateCommand<string>(async x =>
            {
                await _dialogService.DisplayAlertAsync("資訊", $"你按下的按鈕是: {x}", "確定");
            });

其他說明

由於現在 Xamarin.Forms 中的 ListView 中,關於使用者點選某筆紀錄的時候,僅僅提供了事件觸發的方式,並沒有提供相對應的命令可供使用,因此,我們可以透過這篇文章所提到的作法,並且參考 ListView 文件中有的事件,將其對應到我們在 ViewModel 內建立的命令物件,就可以在 ViewModel 中處理使用者點選某筆紀錄後,要執行的各種程式邏輯。