這事是要做個關聯連動式的 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: (bindable, oldvalue, newvalue) => ((BindablePicker)bindable).OnItemsSourceChanged(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;
if (observableCollection != null)
observableCollection.CollectionChanged -= OnCollectionChanged;
}
if (newvalue != null)
{
var observableCollection = newvalue as INotifyCollectionChanged;
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}");
}
});
補充說明
實際的 View 宣告與 ViewModel 的程式碼邏輯,可以參考 EventToCommandBehaviorPage.xaml
/ EventToCommandBehaviorPageViewModel.cs
這兩個檔案。
在以往我們進行 Xamarin.Forms 專案開發的時候,我們需要使用 C# 來設計這個行動應用程式的商業邏輯運作行為,對於這個行動應用程式,我們將會透過 XAML 宣告標記語言來設計可用於跨平台的頁面之使用者介面內容。不過,要如何使用這項功能呢,您需要升級 VS4W Visual Studio 2017 for Windows 到 Visual Studio 2017 15.8 版本,並且升級到 Xamarin.Forms NuGet 套件到 3.0 以上的版本。