XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2017/05/05

Xamarin 企業級行動化開發平台環境建置攻略 - Visual Studio 2017 安裝與設定

這份影片將會帶領大家,從無到有的進行 Visual Studio 2017 + Xamarin
開發環境的安裝與相關設定


ListView 的顯示紀錄客製化與互動應用

我們使用 Xamarin.Forms 進行對於 ListView 的使用,很多人都會覺得有些好奇,究竟相關 ListView 上的不同操作與應用,該如何撰寫 View / ViewModel 的 XAML / C# 程式碼呢?
我撰寫的一個範例專案(原始碼位於 https://github.com/vulcanlee/xamarin-forms-develop-notes-example/tree/master/XFListShowImg ),用來說明底下需求:
  1. 在Listview新增判斷邏輯,假如是值True,呈現某張圖,若是False,則不秀圖出來
  2. 當使用者點選某個紀錄上的 BoxView 控制項的時候,若該圖片沒有顯示出來,則需要修正,經圖片顯示出來。
這是一個上過課的學員提問的問題,我使用底下範例專案做出說明。

ListView 的 XAML

由於,我們需要將集合資料使用 ListView 顯示出來,當然,我們需要孰悉 ListView 的各種屬性的操作;在這裡,我們需要使用 ListView 的這些屬性
  • ItemsSource : 將會綁訂到 ListView 上的 ObservableCollection<T> 的 C# 屬性,只要我們有增/修/刪 這個 ObservableCollection<T> 的物件內容,ListView 則會自動進行更新。
  • SelectedItem : 當使用者點選某個紀錄項目的時候,當時點選的紀錄項目,將會透過資料綁定方法,把當時點選的紀錄物件值,綁定到 ViewModel 上的指定屬性物件上。
  • HasUnevenRows : 這個屬性用來指定,每個紀錄可以不用具有相同的高度。

每筆紀錄的顯示內容 ViewCell

要設計每筆紀錄在 ListView 上要呈現甚麼樣貌,我們就需要使用 ViewCell 來宣告;在底下,列出了這個 ViewCell 的完整定義。
我們使用了 Grid 將 ViewCell 切割成為 2 Rows X 2 Columns,並且將 Label / BoxView / Image 這三個控制項,使用Grid附加屬性,指定這些控制項是要位於 Grid 的哪個格子上。
在這些控制項上,要顯示的內容或者顏色,將會透過資料綁定的方式,指定到當時紀錄的ViewModel 中的屬性上,在這裡要特別注意到,此時,ViewCell 的 BindingContext 物件,不是整個 ListView 要顯示的集合物件,而是,要顯示的那筆紀錄的物件。
關於圖片在某些情境(在這裡是依據 ViewModel 內的某個屬性值),不需要顯示出來;這樣的需求有很多種作法,也需要是開發環境的各種條件而定,在這裡,我們使用了 IsVisible="{Binding ShowImage}" 這樣的方式來處理,只要該記錄在 ViewModel 內的 ShowImage 的值為 False 的布林值,他就不會顯示在螢幕上,反之,就會顯示在螢幕上。
                    <ViewCell>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="*"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="100"/>
                            </Grid.ColumnDefinitions>

                            <Label 
                                Grid.Row="0" Grid.Column="0"
                                Text="{Binding Number}"/>
                            <BoxView
                                Grid.Row="1" Grid.Column="0"
                                Color="{Binding BoxColor}">
                            </BoxView>
                            <Image
                                Grid.Row="0" Grid.Column="1"
                                Grid.RowSpan="2"
                                HorizontalOptions="Fill" VerticalOptions="Fill"
                                Aspect="AspectFill"
                                Source="{Binding ImageUrl}"
                                IsVisible="{Binding ShowImage}"/>
                        </Grid>
                    </ViewCell>

如何指定 ViewCell 內的命令,要綁定到 ViewModel 中的命令物件

我們這裡有個需求,那就是,使用者可以點選 BoxView 這個控制項,接著,我們需要判斷這個紀錄的圖片是否沒有顯示出來,若沒有,我們須將這個圖片切換成為可以顯示的模式。
我們想要將這個手勢操作命令定義在頁面用的 ViewModel 上,而不是在每筆紀錄的 ViewModel 上。
為了解決這個問題,我們需要在設定綁定命令的時候,變更當時的 BindingContext 的來源,在這裡,我們使用了底下方法:
我們設定了 BoxView.GestureRecognizers 的屬性,加入了 TapGestureRecognizer 這個物件;不過,當要使用 TapGestureRecognizer 的 Command 屬性的時候,我們使用了 Source 這個屬性,指定此次綁定的 BindingContext 的來源,是這個頁面的 ViewModel,接著,我們使用了 Path 這個屬性,指定了要綁定的命令路徑。
其中,x:Reference ThisPage 的 ThisPage 指的就是這個頁面,因為,我們在頁面上,設定了 x:Name="ThisPage" 這個延伸標記屬性設定。
                            <BoxView
                                Grid.Row="1" Grid.Column="0"
                                Color="{Binding BoxColor}">
                                <BoxView.GestureRecognizers>
                                    <TapGestureRecognizer 
                                        Command="{Binding Path=BindingContext.TapBoxCommand, Source={x:Reference ThisPage}}"
                                        CommandParameter="{Binding}"/>
                                </BoxView.GestureRecognizers>
                            </BoxView>

ViewModel 內的程式碼

在這裡,我們使用了 PropertyChanged.Fody 這個套件,因此,簡化了整個 ViewModel 的程式碼數量,關於要綁定到 View 中的各個屬性,其定義如下。
在這裡,要綁定到 ListView 上的集合資料,需要使用 ObservableCollection<MyItem> 這個型別,而命令的部分,我們使用了 Prism 提供的 DelegateCommand<MyItem> 的命令型別,這個泛型命令型別,可以讓我們接收到,來自 XAML 的 CommandParameter 屬性所綁定的內容,在這個應用範例中,CommandParameter 將會綁定當時所顯示的紀錄物件。
若您不想使用 DelegateCommand<MyItem> 泛型命令型別,您也可以使用 DelegateCommand 這個型別,不過,要取得使用者當時所點選的物件,您可以使用 SelectedMyItem 這個物件來取得當時點選的紀錄物件。
        public MyItem SelectedMyItem { get; set; }
        public ObservableCollection<MyItem> MyItems { get; set; } = new ObservableCollection<MyItem>();

        public DelegateCommand<MyItem> TapBoxCommand { get; set; }
當使用點選某筆紀錄後,將會執行底下程式碼:
            TapBoxCommand = new DelegateCommand<MyItem>(x =>
              {
                  if(x.ShowImage == false)
                  {
                      x.ShowImage = true;
                      x.BoxColor = Color.BlueViolet;
                  }
              });

執行結果的畫面

2017/05/01

Xamarin.Forms 使用 Prism 框架開發,如何修改 View 的名稱

之前上課的時候,有學員提問,若想要修改 View 的名稱,是不是只要修改 ViewName.xaml 這個檔案名稱就好了;不過,答案可不是如此。
在這個練習中,我們使用 Prism Template Pack 專案樣板,建立一個 Xamarin.Forms 的專案。在這個專案內,預設會幫我們產生一個 MainPage.xaml / MainPageViewModel 兩個檔案,分別是 View / ViewModel;若我們想要把這個頁面,修改成為 HomePage 的話,有哪些地方需要注意的,以及該如何做呢?

將 View 的名稱改名

首先,可以在 Visual Studio 的 核心PCL 專案內的 Views 資料夾內,找到 MainPage.xaml 檔案,滑鼠右擊這個項目,選擇 重新命名,接著輸入 HomePage
接著,打開這個 HomePage.xaml 檔案,找到 ContentPage 這個根節點。
<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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp2.Views.MainPage"
             Title="MainPage">
將 x:Class="PrismUnityApp2.Views.MainPage" 修改成為 x:Class="PrismUnityApp2.Views.HomePage",並且按下 Ctrl+S 將此次修改的內容存檔,而 ContentPage 的定義將會成為
<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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PrismUnityApp2.Views.HomePage"
             Title="MainPage">
在 核心PCL專案內的 Views 資料夾內,打開 HomePage.xaml.cs 檔案。
將 MainPage 類別,修正成為 HomePage,如下所示:
按下 Ctrl + S 將此次修改的內容存檔。
除了類別名稱之外,建構式名稱也需要一併修正。
    public partial class HomePage : ContentPage
    {
        public HomePage()
        {
            InitializeComponent();
        }
    }

修正 App.xaml.cs 的導航頁面與注入定義

在 核心PCL 專案內,找到並打開 App.xaml.cs 檔案,在 OnInitialized 方法內,呼叫 NavigationService.NavigateAsync 內的字串引數,把 MainPage 內容改成 HomePage
在 RegisterTypes 方法內,將 Container.RegisterTypeForNavigation<MainPage>(); 修改成為 Container.RegisterTypeForNavigation<HomePage>();
最後按下 Ctrl + S 存檔
    public partial class App : PrismApplication
    {
        public App(IPlatformInitializer initializer = null) : base(initializer) { }

        protected override void OnInitialized()
        {
            InitializeComponent();

            NavigationService.NavigateAsync("NavigationPage/HomePage?title=Hello%20from%20Xamarin.Forms");
        }

        protected override void RegisterTypes()
        {
            Container.RegisterTypeForNavigation<NavigationPage>();
            Container.RegisterTypeForNavigation<HomePage>();
        }
    }

將 ViewModel 的名稱改名

首先,可以在 Visual Studio 的 核心PCL 專案內的 ViewModels 資料夾內,找到 MainPageViewModel 檔案,滑鼠右擊這個項目,選擇 重新命名,接著輸入 HomePageViewModel
此時,Visual Studio 會提示您:
您在正重新命名檔案,您是否也要更新命名此專案中對於程式碼項目 'MainPageViewModel' 的所有參考?
請在這個對話窗中,點選 是(Y)

建置與執行

最後,請將 Android 專案設定為預設起始專案,並且執行這個專案。
若您的操作步驟都是正確的話,這個 App 是可以正常執行的。

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