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