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 這兩個檔案。

沒有留言:

張貼留言