XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2016/10/05

Xamarin.Forms ListView 控制項的功能與特色

了解更多關於 [Xamarin.Android] 的使用方式
了解更多關於 [Xamarin.iOS] 的使用方式
了解更多關於 [Xamarin.Forms] 的使用方式
了解更多關於 [Hello, Android:快速入門] 的使用方式
了解更多關於 [Hello, iOS – 快速入門] 的使用方式
了解更多關於 [Xamarin.Forms 快速入門] 的使用方式

ListView 是在進行開發行動裝置應用程式中最基本,也是最複雜的控制項,在這份筆記中,將會說明如何使用底下各類功能。
  • ListView 的資本操作 (資料綁定)
  • 紀錄異動時,ListView同步更新顯示(ObservableCollection)
  • 取得剛剛點選的項目資料內容
  • 設定每列固定高度
  • 不均勻(Uneven) 的每筆紀錄高度
  • 每筆紀錄的分隔線與顏色設定
  • 紀錄列上可以有按鈕
  • 頁首與頁尾
  • 紀錄的互動功能表 (ViewCell.ContextActions)
  • 手勢更新(Pull to Refresh)資料
  • 指定捲動到某筆資料上
  • 點選項目之後,會取消剛剛的選取
ListView功能與特色
這份筆記的專案原始碼將可從底下網址取得:

ListView 基本應用 不可修改紀錄 (BasicPage)

在這個範例頁面中,展示了如何使用 ListView 的基本功能
不可修改紀錄 不可修改紀錄
  • 如何透過 MVVM 方式,將要顯示的資料列綁定到 ListView 控制項上
    ItemsSource="{Binding MyItemList, Mode=TwoWay}"
    可以透過 ItemsSource 這個屬性,使用 XAML 延伸標記功能,進行資料綁定;例如 ItemsSource="{Binding MyItemList, Mode=TwoWay}"。而在 ViewModel 中,您需要使用ObservableCollection<MyItem> 來宣告 ListView 控制項所要顯示的資料來源,使用ObservableCollection 的目的,是您可以透過訂閱內建的 CollectionChanged 事件,便可以得知當時資料新增、修改、刪除等狀態。
    在 ListView 宣告標記中,每筆資料的內容,可以透過 ListView.ItemTemplate > DataTemplate >ViewCell 來進行定義,在這頁範例中,使用 Grid 版面配置,定義了三個欄位(Column),並且使用了雙向資料繫結綁定了每筆紀錄的欄位;而每筆資料的紀錄來為型態為 MyItem,這個類別定義有列在底下程式碼清單中。
          <ListView.ItemTemplate>
          <DataTemplate>
            <ViewCell>
              <Grid
                >
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="*" />
                  <ColumnDefinition Width="*" />
                  <ColumnDefinition Width="36" />
                </Grid.ColumnDefinitions>
                <Label Grid.Column="0"
                  VerticalOptions="Center"
                  TextColor="Teal" FontSize="20"
                  Text="{Binding FirstName, Mode=TwoWay}" />
                <Label Grid.Column="1"
                  VerticalOptions="Center"
                  TextColor="Teal" FontSize="20"
                  Text="{Binding LastName, Mode=TwoWay}" />
                <Label Grid.Column="2"
                  VerticalOptions="Center"
                  TextColor="Blue" FontSize="30"
                  Text="{Binding Age, Mode=TwoWay}" />
              </Grid>
            </ViewCell>
          </DataTemplate>
        </ListView.ItemTemplate>
    
  • 如何取得點選的資料紀錄內容
    當使用者點選 ListView 上的任何一筆紀錄,其所點選的資料會呈現在 SelectedItem 屬性中,因此,透過了雙向資料繫結 SelectedItem="{Binding MyItemListSelected, Mode=TwoWay}" 就可以在檢視模型 (ViewModel) 內,取得當時使用者點選的那筆資料。而在 ViewModel 內的 MyItemListSelected 屬性,可以使用強型別的方式來宣告,這樣就可以在 ViewModel 內簡單的存取相關屬性。
  • 如何動態新增一筆資料到 ListView 內
    只要針對 ListView 的資料來源,也就是 MyItemList 物件,進行新增一筆物件,此時,在 ListView 上,就會同步更新了這筆資料。要了要能夠呈現在 ViewModel 中,新增一筆紀錄,而在 View 中也能夠動態更新,您需要使用 ObservableCollection<T> 來宣告使用紀錄列之物件。
          private ObservableCollection<MyItem> myItemList;
          public ObservableCollection<MyItem> MyItemList
          {
              get { return myItemList; }
              set { SetProperty(ref myItemList, value); }
          }
    
  • 如何動態刪除一筆資料到 ListView 內
    只要針對 ListView 的資料來源,也就是 MyItemList 物件,進行一筆紀錄的刪除動作,此時,在 ListView 上,就會同步刪除了這筆資料。
  • 為何無法修改所選取的資料
    雖然,在程式碼中,有使用 myItemListSelected 來進行選定的紀錄,修改其內容,但由於每筆 ListView 內的資料類別為 MyItem,在這個類別內的每個屬性,並沒有實作出INotifyPropertyChanged 介面,也就是說,就算當使用者點選了 修改 按鈕,試圖變更了 ViewModel 內的這筆紀錄內容,這些內容也不會顯示在 ListView 上,在下一個範例中,將會說明如何解決此一問題。
     public class MyItem
      {
          public string FirstName { get; set; }
          public string LastName { get; set; }
          public int Age { get; set; }
      }
    
    在 修改() 方法中,有註解兩段程式碼,這兩段程式碼也是可以做到修改紀錄的能力,只不過是透過了重新設定這筆紀錄或者將原紀錄刪除、接著在加入到原先位置上,達成可以顯示出修改紀錄在 ListView 上的效果,這樣的做法比較不太建議使用。

BasicPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.BasicPage"
             Title="ListView 基本應用 不可修改紀錄"
             >

  <StackLayout>
    <ListView
      ItemsSource="{Binding MyItemList, Mode=TwoWay}"
      SelectedItem="{Binding MyItemListSelected, Mode=TwoWay}"
      Margin="20,0"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="36" />
              </Grid.ColumnDefinitions>
              <Label Grid.Column="0"
                VerticalOptions="Center"
                TextColor="Teal" FontSize="20"
                Text="{Binding FirstName, Mode=TwoWay}" />
              <Label Grid.Column="1"
                VerticalOptions="Center"
                TextColor="Teal" FontSize="20"
                Text="{Binding LastName, Mode=TwoWay}" />
              <Label Grid.Column="2"
                VerticalOptions="Center"
                TextColor="Blue" FontSize="30"
                Text="{Binding Age, Mode=TwoWay}" />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <StackLayout Orientation="Vertical">
      <StackLayout Orientation="Horizontal"
                   HorizontalOptions="Center">
        <Button Text="新增" Command="{Binding 新增Command}" />
        <Button Text="修改" Command="{Binding 修改Command}" />
        <Button Text="刪除" Command="{Binding 刪除Command}" />
      </StackLayout>
    </StackLayout>
  </StackLayout>

</ContentPage>

BasicPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Models;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class BasicPageViewModel : BindableBase
    {
        private ObservableCollection<MyItem> myItemList;
        public ObservableCollection<MyItem> MyItemList
        {
            get { return myItemList; }
            set { SetProperty(ref myItemList, value); }
        }


        #region PropertyName
        private string propertyName;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public string PropertyName
        {
            get { return this.propertyName; }
            set { this.SetProperty(ref this.propertyName, value); }
        }
        #endregion


        private MyItem myItemListSelected;
        public MyItem MyItemListSelected
        {
            get { return myItemListSelected; }
            set { SetProperty(ref myItemListSelected, value); }
        }

        public DelegateCommand 新增Command { get; private set; }
        public DelegateCommand 修改Command { get; private set; }
        public DelegateCommand 刪除Command { get; private set; }
        public BasicPageViewModel()
        {
            MyItemList = MyItemRepository.Sample();

            新增Command = new DelegateCommand(新增);
            修改Command = new DelegateCommand(修改);
            刪除Command = new DelegateCommand(刪除);
        }

        private void 刪除()
        {
            var fooItem = MyItemList.FirstOrDefault(x => x.FirstName == myItemListSelected.FirstName &&
            x.LastName == myItemListSelected.LastName && x.Age == myItemListSelected.Age);
            if (fooItem != null)
            {
                MyItemList.Remove(fooItem);
            }
        }

        private void 修改()
        {
            if (myItemListSelected != null)
            {
                // 底下程式碼,無法讓 UI 同步更新已經修正的內容
                #region 直接修改綁定的物件 
                myItemListSelected.FirstName = "艾嬡";
                myItemListSelected.LastName = "林";
                myItemListSelected.Age = 33;
                #endregion

                // 若每筆紀錄的每個欄位沒有實作 INotifyPropertyChanged 介面
                // 則當修改某一紀錄的欄位時候,就無法立即反應、顯示更新結果
                // 您需要變通,重新設定這筆紀錄,或者刪除這筆紀錄再加入
                // 請參考底下兩個方法(這樣的方法比較複雜)

                #region 先找到、修改、再回去設定 (沒有動畫)
                //var fooItem = MyItemList.FirstOrDefault(x => x.FirstName == myItemListSelected.FirstName &&
                //x.LastName == myItemListSelected.LastName && x.Age == myItemListSelected.Age);

                //if (fooItem != null)
                //{
                //    var fooIdx = myItemList.IndexOf(fooItem);

                //    fooItem.FirstName = "艾嬡";
                //    fooItem.LastName = "林";
                //    fooItem.Age = 33;
                //    myItemList[fooIdx]= fooItem;
                //}
                #endregion

                #region 先找到、移除、再加入 (會有動畫)
                //var fooItem = MyItemList.FirstOrDefault(x => x.FirstName == myItemListSelected.FirstName &&
                //x.LastName == myItemListSelected.LastName && x.Age == myItemListSelected.Age);

                //if (fooItem != null)
                //{
                //    var fooIdx = myItemList.IndexOf(fooItem);
                //    myItemList.Remove(fooItem);

                //    fooItem.FirstName = "艾嬡";
                //    fooItem.LastName = "林";
                //    fooItem.Age = 33;
                //    myItemList.Insert(fooIdx, fooItem);
                //}
                #endregion
            }
        }

        private void 新增()
        {
            MyItemList.Add(new MyItem
            {
                FirstName = "文君",
                LastName = "簡",
                Age = 19
            });
        }
    }
}

## MyItem.cs

```cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace XFListView.Models
{
   public class MyItem
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

ListView 基本應用 可修改紀錄 (Basic1Page)

在這個範例頁面中,展示了如何使用修改 ListView 來源物件的定義,使其具有資料繫結能力,進而可以做到修改紀錄的能力。
可修改紀錄 可修改紀錄
在 XAML 頁面定義與上面的頁面定義一樣,不過,在 ViewModel 的 ListView 要綁定的資料來源定義上,修改為 public ObservableCollection<MyItemViewModel> MyItemList ,也就是,在 ListView 中的每筆紀錄,都會修改成為 MyItemViewModel 類別,而這個類別則有實作了 BindableBase,請參考底下程式碼。因此,原先當使用者按下了修改按鈕所要做的事情,就會立即在 ListView 上顯示出這些更新的欄位。
        private ObservableCollection<MyItemViewModel> myItemList;
        public ObservableCollection<MyItemViewModel> MyItemList
        {
            get { return myItemList; }
            set { SetProperty(ref myItemList, value); }
        }
    public class MyItemViewModel : BindableBase
    {

        #region FirstName
        private string firstName;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public string FirstName
        {
            get { return this.firstName; }
            set { this.SetProperty(ref this.firstName, value); }
        }
        #endregion

        #region LastName
        private string lastName;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public string LastName
        {
            get { return this.lastName; }
            set { this.SetProperty(ref this.lastName, value); }
        }
        #endregion

        #region Age
        private int age;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public int Age
        {
            get { return this.age; }
            set { this.SetProperty(ref this.age, value); }
        }
        #endregion

        public MyItemViewModel()
        {

        }
    }

Basic1Page.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.Basic1Page"
             Title="ListView 基本應用 可修改紀錄">

  <StackLayout>
    <ListView
      ItemsSource="{Binding MyItemList, Mode=TwoWay}"
      SelectedItem="{Binding MyItemListSelected, Mode=TwoWay}"
      Margin="20,0"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="36" />
              </Grid.ColumnDefinitions>
              <Label Grid.Column="0"
                VerticalOptions="Center"
                TextColor="Teal" FontSize="20"
                Text="{Binding FirstName, Mode=TwoWay}" />
              <Label Grid.Column="1"
                VerticalOptions="Center"
                TextColor="Teal" FontSize="20"
                Text="{Binding LastName, Mode=TwoWay}" />
              <Label Grid.Column="2"
                VerticalOptions="Center"
                TextColor="Blue" FontSize="30"
                Text="{Binding Age, Mode=TwoWay}" />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <StackLayout Orientation="Vertical">
      <StackLayout Orientation="Horizontal"
                   HorizontalOptions="Center">
        <Button Text="新增" Command="{Binding 新增Command}" />
        <Button Text="修改" Command="{Binding 修改Command}" />
        <Button Text="刪除" Command="{Binding 刪除Command}" />
      </StackLayout>
    </StackLayout>
  </StackLayout>

</ContentPage>

Basic1PageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class Basic1PageViewModel : BindableBase
    {
        private ObservableCollection<MyItemViewModel> myItemList;
        public ObservableCollection<MyItemViewModel> MyItemList
        {
            get { return myItemList; }
            set { SetProperty(ref myItemList, value); }
        }

        private MyItemViewModel myItemListSelected;
        public MyItemViewModel MyItemListSelected
        {
            get { return myItemListSelected; }
            set { SetProperty(ref myItemListSelected, value); }
        }

        public DelegateCommand 新增Command { get; private set; }
        public DelegateCommand 修改Command { get; private set; }
        public DelegateCommand 刪除Command { get; private set; }
        public Basic1PageViewModel()
        {
            MyItemList = MyItemRepository.SampleViewModel();

            新增Command = new DelegateCommand(新增);
            修改Command = new DelegateCommand(修改);
            刪除Command = new DelegateCommand(刪除);
        }

        private void 刪除()
        {
            var fooItem = MyItemList.FirstOrDefault(x => x.FirstName == myItemListSelected.FirstName &&
            x.LastName == myItemListSelected.LastName && x.Age == myItemListSelected.Age);
            if (fooItem != null)
            {
                MyItemList.Remove(fooItem);
            }
        }

        private void 修改()
        {
            if (myItemListSelected != null)
            {
                #region 直接修改綁定的物件 
                myItemListSelected.FirstName = "艾嬡";
                myItemListSelected.LastName = "林";
                myItemListSelected.Age = 33;
                #endregion

            }
        }

        private void 新增()
        {
            MyItemList.Add(new MyItemViewModel
            {
                FirstName = "文君",
                LastName = "簡",
                Age = 19
            });
        }
    }
}

MyItemViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;

namespace XFListView.ViewModels
{
    public class MyItemViewModel : BindableBase
    {

        #region FirstName
        private string firstName;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public string FirstName
        {
            get { return this.firstName; }
            set { this.SetProperty(ref this.firstName, value); }
        }
        #endregion

        #region LastName
        private string lastName;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public string LastName
        {
            get { return this.lastName; }
            set { this.SetProperty(ref this.lastName, value); }
        }
        #endregion

        #region Age
        private int age;
        /// <summary>
        /// PropertyDescription
        /// </summary>
        public int Age
        {
            get { return this.age; }
            set { this.SetProperty(ref this.age, value); }
        }
        #endregion

        public MyItemViewModel()
        {

        }
    }
}

ListView 固定紀錄列高 (RowHeightPage)

在這個範例頁面中,展示了如何使用指定 ListView 內每筆紀錄要顯示的高度。
  • 要設定每筆紀錄要顯示的高度,可以使用 ListView 的屬性 RowHeight 來指定,例如:RowHeight="60" 就是指定每筆紀錄要顯示的高度是60。
  • 在這個範例中,ViewModel 的每筆紀錄內容都很多,而且有設定高度,因此,多餘的部分,會被裁剪掉。
    固定紀錄列高 

RowHeightPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.RowHeightPage"
             Title="固定紀錄列高"  >

  <StackLayout>
    <ListView x:Name="lv"
      ItemsSource="{Binding MyItemList, Mode=TwoWay}"
      Margin="20,0"
      RowHeight="60"
      SeparatorColor="Red"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <StackLayout>
              <BoxView Color="Transparent" HeightRequest="10" />
              <Label
                TextColor="Teal" FontSize="30"
                Text="{Binding FirstName}"  />
              <BoxView Color="Transparent" HeightRequest="10" />
            </StackLayout>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>


</ContentPage>

RowHeightPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Repositories;
using Prism.Navigation;

namespace XFListView.ViewModels
{
    public class RowHeightPageViewModel : BindableBase, INavigationAware
    {
        private ObservableCollection<MyItemViewModel> myItemList;
        public ObservableCollection<MyItemViewModel> MyItemList
        {
            get { return myItemList; }
            set { SetProperty(ref myItemList, value); }
        }



        private MyItemViewModel myItemListSelected;
        public MyItemViewModel MyItemListSelected
        {
            get { return myItemListSelected; }
            set { SetProperty(ref myItemListSelected, value); }
        }

        public DelegateCommand 可變列高Command { get; private set; } 
        public DelegateCommand 固定列高Command { get; private set; } 
        public RowHeightPageViewModel()
        {
            MyItemList = MyItemRepository.SampleLongViewModel();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

ListView 可變紀錄列高 (VariRowHeithtPage)

在這個範例頁面中,展示了如何根據當時資料顯示的高度,自行調整每列的高度。
  • 要設定每筆紀錄要顯示的高度,可以使用 ListView 的屬性 HasUnevenRows 來指定,例如:RowHeight="60" 就是指定每筆紀錄要顯示高度會依當時資料的高度而定。
 

VariRowHeithtPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             Title="可變紀錄列高"
             x:Class="XFListView.Views.VariRowHeithtPage">

    <StackLayout>
        <ListView x:Name="lv"
      ItemsSource="{Binding MyItemList, Mode=TwoWay}"
      Margin="20,0"
      HasUnevenRows="True"
      SeparatorColor="Red"
      >
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout>
                            <BoxView Color="Transparent" HeightRequest="10" />
                            <Label
                TextColor="Teal" FontSize="30"
                Text="{Binding FirstName}"  />
                            <BoxView Color="Transparent" HeightRequest="10" />
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>

</ContentPage>

VariRowHeithtPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class VariRowHeithtPageViewModel : BindableBase, INavigationAware
    {
        private ObservableCollection<MyItemViewModel> myItemList;
        public ObservableCollection<MyItemViewModel> MyItemList
        {
            get { return myItemList; }
            set { SetProperty(ref myItemList, value); }
        }



        private MyItemViewModel myItemListSelected;
        public MyItemViewModel MyItemListSelected
        {
            get { return myItemListSelected; }
            set { SetProperty(ref myItemListSelected, value); }
        }

        public DelegateCommand 可變列高Command { get; private set; }
        public DelegateCommand 固定列高Command { get; private set; }
        public VariRowHeithtPageViewModel()
        {
            MyItemList = MyItemRepository.SampleLongViewModel();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
        }
    }
}

ListView 客製化資料列與資料按鈕會失效 (CustomButtonPage)

在這個範例頁面中,展示了開發者可以在 ListView 的每筆紀錄上,放置按鈕 View,讓使用者點選,觸發這個按鈕命令或者事件;可是,當您使用底下的方式來宣告 XAML & 定義 ViewModel 的程式碼,卻發現到無法觸發每筆紀錄的按鈕事件。
客製化資料列與資料按鈕會失效 客製化資料列與資料按鈕會失效
在 ListView 的 ItemTemplate 內,確實有宣告一個按鈕與設定Command屬性並資料繫結到 ViewModel 上
<Button Grid.Column="2"
                      Text="進行互動"
                      Command="{Binding 進行互動Command}"
                      CommandParameter="{Binding .}"
                      />
在 ViewModel 內,也確實有宣告 ICommand 物件,即 進行互動Command,和其觸發後要執行的動作。可是,為什麼沒有觸發這個 ICommand 物件呢?
        public CustomButtonPageViewModel(IPageDialogService dialogService)
        {

            _dialogService = dialogService;
            MyData = PersonRepository.SampleViewModel();

            進行互動Command = new DelegateCommand<PersonViewModel>(進行互動);
        }

        private async void 進行互動(PersonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"您選取了 {obj.Name}", "確定");
        }
這是一般在剛開始接觸 MVVM 開發方法的開發者常犯的錯誤,要知道,在這個 ViewModel 內所定義的 進行互動Command ICommand 物件,並不存在於每筆 ListView 的紀錄內,每筆紀錄要顯示的資料是定義在PersonViewModel 這個類別上。雖然其有繼承了 BindableBase類別與實作了 INotifyPropertyChanged 介面,可以,在這個類別內,卻沒有定義任何 ICommand 物件,這將導致 ListView 內的每筆紀錄,沒有可用的 ICommand 物件可用於綁定繫結之用。
    public class PersonViewModel : BindableBase
    {
        #region Name
        private string name;
        /// <summary>
        /// Name
        /// </summary>
        public string Name
        {
            get { return this.name; }
            set { this.SetProperty(ref this.name, value); }
        }
        #endregion


        #region Birthday
        private DateTime birthday;
        /// <summary>
        /// Birthday
        /// </summary>
        public DateTime Birthday
        {
            get { return this.birthday; }
            set { this.SetProperty(ref this.birthday, value); }
        }
        #endregion

        #region FavoriteColor
        private Color favoriteColor;
        /// <summary>
        /// FavoriteColor
        /// </summary>
        public Color FavoriteColor
        {
            get { return this.favoriteColor; }
            set { this.SetProperty(ref this.favoriteColor, value); }
        }
        #endregion


        public PersonViewModel()
        {

        }
    }

CustomButtonPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.CustomButtonPage"
             Title="客製化資料列與資料按鈕會失效"
             >

  <StackLayout>
    <ListView
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="120" />
              </Grid.ColumnDefinitions>
              <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}" 
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
              <StackLayout Grid.Column="1" Orientation="Vertical">
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
              </StackLayout>
              <Button Grid.Column="2"
                      Text="進行互動"
                      Command="{Binding 進行互動Command}"
                      CommandParameter="{Binding .}"
                      />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

</ContentPage>

CustomButtonPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using XFListView.Repositories;
using Prism.Services;

namespace XFListView.ViewModels
{
    public class CustomButtonPageViewModel : BindableBase
    {

        #region MyData
        private ObservableCollection<PersonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion

        #region MyDataSelected
        private PersonViewModel myDataSelected;
        /// <summary>
        /// MyDataSelected
        /// </summary>
        public PersonViewModel MyDataSelected
        {
            get { return this.myDataSelected; }
            set { this.SetProperty(ref this.myDataSelected, value); }
        }
        #endregion


        public readonly IPageDialogService _dialogService;

        public DelegateCommand<PersonViewModel> 進行互動Command { get; private set; }
        public CustomButtonPageViewModel(IPageDialogService dialogService)
        {

            _dialogService = dialogService;
            MyData = PersonRepository.SampleViewModel();

            進行互動Command = new DelegateCommand<PersonViewModel>(進行互動);
        }

        private async void 進行互動(PersonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"您選取了 {obj.Name}", "確定");
        }
    }
}

ListView 客製化資料列與資料按鈕 (CustomButton1Page)

在這個範例頁面中,展示了如何解決上述問題,那就是需要重新修改 PersonViewModel 這個類別。
客製化資料列與資料按鈕 客製化資料列與資料按鈕iOS
在這個範例中,將會把 PersonViewModel 類別,使用這個 PersonWithButtonViewModel 類別來取代,在這個類別中,經會定義了 ICommand 的物件,如下所示,這樣,就解決了使用者可以在每筆 ListView 紀錄上按下按鈕,並且有回應的功能。
using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;

namespace XFListView.ViewModels
{
    public class PersonWithButtonViewModel : BindableBase
    {
        #region Name
        private string name;
        /// <summary>
        /// Name
        /// </summary>
        public string Name
        {
            get { return this.name; }
            set { this.SetProperty(ref this.name, value); }
        }
        #endregion

        #region Birthday
        private DateTime birthday;
        /// <summary>
        /// Birthday
        /// </summary>
        public DateTime Birthday
        {
            get { return this.birthday; }
            set { this.SetProperty(ref this.birthday, value); }
        }
        #endregion

        #region FavoriteColor
        private Color favoriteColor;
        /// <summary>
        /// FavoriteColor
        /// </summary>
        public Color FavoriteColor
        {
            get { return this.favoriteColor; }
            set { this.SetProperty(ref this.favoriteColor, value); }
        }
        #endregion

        public readonly IPageDialogService _dialogService;

        public DelegateCommand<PersonWithButtonViewModel> 進行互動Command { get; private set; }
        public DelegateCommand<PersonWithButtonViewModel> 立即產生Command { get; private set; }
        public DelegateCommand<PersonWithButtonViewModel> 刪除Command { get; private set; }

        public PersonWithButtonViewModel()
        {
            #region 使用 Prism 的容器 IUnityContainer,解析 IPageDialogService 物件
            // 在這裡不使用建構式注入,是因為,PersonRepository.SampleWithButtonViewModel() 要產生的清單物件,不是透過 Unity 所建立的
            // 所以,手動進行物件解析,取得實作的物件
            var fooApp = App.Current as App;
            IUnityContainer fooIUnityContainer = fooApp.Container;
            _dialogService = fooApp.Container.Resolve<IPageDialogService>();
            #endregion

            進行互動Command = new DelegateCommand<PersonWithButtonViewModel>(進行互動);
            立即產生Command = new DelegateCommand<PersonWithButtonViewModel>(立即產生);
            刪除Command = new DelegateCommand<PersonWithButtonViewModel>(刪除);
        }

        private async void 進行互動(PersonWithButtonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"您選取了 {obj.Name}", "確定");
        }

        private async void 刪除(PersonWithButtonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"準備刪除 {obj.Name}", "確定");
        }

        private async void 立即產生(PersonWithButtonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"立即執行 {obj.Name}", "確定");
        }

    }
}

CustomButton1Page.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.CustomButton1Page"
             Title="客製化資料列與資料按鈕"
             >

  <StackLayout>
    <ListView
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="120" />
              </Grid.ColumnDefinitions>
              <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}"
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
              <StackLayout Grid.Column="1" Orientation="Vertical">
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
              </StackLayout>
              <Button Grid.Column="2"
                      Text="進行互動"
                      Command="{Binding 進行互動Command}"
                      CommandParameter="{Binding .}"
                      />
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

</ContentPage>

CustomButton1PageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using XFListView.Repositories;
using Prism.Services;

namespace XFListView.ViewModels
{
    public class CustomButton1PageViewModel : BindableBase
    {

        #region MyData
        private ObservableCollection<PersonWithButtonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonWithButtonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion

        #region MyDataSelected
        private PersonWithButtonViewModel myDataSelected;
        /// <summary>
        /// MyDataSelected
        /// </summary>
        public PersonWithButtonViewModel MyDataSelected
        {
            get { return this.myDataSelected; }
            set { this.SetProperty(ref this.myDataSelected, value); }
        }
        #endregion


        public readonly IPageDialogService _dialogService;

        public DelegateCommand<PersonViewModel> 進行互動Command { get; private set; }
        public CustomButton1PageViewModel(IPageDialogService dialogService)
        {

            _dialogService = dialogService;
            MyData = PersonRepository.SampleWithButtonViewModel();

            進行互動Command = new DelegateCommand<PersonViewModel>(進行互動);
        }

        private async void 進行互動(PersonViewModel obj)
        {
            await _dialogService.DisplayAlertAsync("請確定", $"您選取了 {obj.Name}", "確定");
        }
    }
}

ListView 頁首頁尾、自動捲動、互動功能表 (HeaderFooterPage)

在這個範例頁面中,展示了如何使用頁首與頁尾的UI內容,並且可以使用 ListView 的互動功能表。
頁首頁尾自動捲動互動功能表 頁首頁尾自動捲動互動功能表
您可以使用 ListView.Header & ListView.Footer 來宣告 ListView 的頭與尾要出先甚麼樣貌。
並且在 ViewCell 裡面,使用 ViewCell.ContextActions 來宣告互動功能表的定義,如下所示
                        <ViewCell.ContextActions>
                            <MenuItem Text="立即產生"
                                      Command="{Binding 立即產生Command}" CommandParameter="{Binding .}"  />
                            <MenuItem Text="刪除"
                                      Command="{Binding 刪除Command}" CommandParameter="{Binding .}"
                                      IsDestructive="True" />
                        </ViewCell.ContextActions>

HeaderFooterPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.HeaderFooterPage"
             Title="頁首頁尾、自動捲動、互動功能表"
             >

    <StackLayout>
        <ListView
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      >
            <ListView.Header>
                <StackLayout Orientation="Horizontal">
                    <Label Text="頁首標題"
          TextColor="Green" FontSize="40"
          BackgroundColor="Yellow" />
                </StackLayout>
            </ListView.Header>
            <ListView.Footer>
                <StackLayout Orientation="Horizontal">
                    <Label Text="頁尾標題"
          TextColor="White" FontSize="40"
          BackgroundColor="Gray" />
                </StackLayout>
            </ListView.Footer>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <ViewCell.ContextActions>
                            <MenuItem Text="立即產生"
                                      Command="{Binding 立即產生Command}" CommandParameter="{Binding .}"  />
                            <MenuItem Text="刪除"
                                      Command="{Binding 刪除Command}" CommandParameter="{Binding .}"
                                      IsDestructive="True" />
                        </ViewCell.ContextActions>

                        <ViewCell.View>
                            <Grid
              >
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="70" />
                                    <ColumnDefinition Width="*" />
                                    <ColumnDefinition Width="120" />
                                </Grid.ColumnDefinitions>
                                <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}"
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
                                <StackLayout Grid.Column="1" Orientation="Vertical">
                                    <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                                    <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
                                </StackLayout>
                                <Button Grid.Column="2"
                      Text="進行互動"
                      Command="{Binding 進行互動Command}"
                      CommandParameter="{Binding .}"
                      />
                            </Grid>
                        </ViewCell.View>

                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>

</ContentPage>

HeaderFooterPageViewModel.cs

using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xamarin.Forms;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class HeaderFooterPageViewModel : BindableBase 
    {
        #region MyData
        private ObservableCollection<PersonWithButtonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonWithButtonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion

        #region MyDataSelected
        private PersonWithButtonViewModel myDataSelected;
        /// <summary>
        /// MyDataSelected
        /// </summary>
        public PersonWithButtonViewModel MyDataSelected
        {
            get { return this.myDataSelected; }
            set { this.SetProperty(ref this.myDataSelected, value); }
        }
        #endregion

        public HeaderFooterPageViewModel()
        {
            myData = PersonRepository.SampleWithButtonViewModel();
        }

    }
}

ListView 自動捲動到指定紀錄 (ScrollToPage)

在這個範例頁面中,展示了如何在 ViewModel 來執行 ListView 的自動捲動功能;要做到這樣的功能,需要使用 code-behind 來協助做到。
在 code behind 裡面,使用 listview.ScrollTo(personWithButtonViewModel, ScrollToPosition.Start, true);來做到這樣的需求。

ScrollToPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.ScrollToPage"
             Title="自動捲動到指定紀錄"
             >

  <StackLayout>
    <ListView x:Name="listview"
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}"
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
              <StackLayout Grid.Column="1" Orientation="Vertical">
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
              </StackLayout>
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <StackLayout
      Orientation="Horizontal">
      <Button Text="捲到2Zachary" Command="{Binding 捲到2ZacharyCommand}" />
      <Button Text="捲到Bob" Command="{Binding 捲到BobCommand}" />
    </StackLayout>
  </StackLayout>

</ContentPage>

ScrollToPage.xaml.cs

using Xamarin.Forms;
using XFListView.ViewModels;

namespace XFListView.Views
{
    public partial class ScrollToPage : ContentPage
    {
        public ScrollToPageViewModel fooScrollToPageViewModel;
        public ScrollToPage()
        {
            InitializeComponent();

            fooScrollToPageViewModel = this.BindingContext as ScrollToPageViewModel;
            fooScrollToPageViewModel.自動捲動到 = this.自動捲動到;
        }

        public void 自動捲動到(PersonWithButtonViewModel personWithButtonViewModel)
        {
            listview.ScrollTo(personWithButtonViewModel, ScrollToPosition.Start, true);
        }
    }
}

ScrollToPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class ScrollToPageViewModel : BindableBase
    {
        #region MyData
        private ObservableCollection<PersonWithButtonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonWithButtonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion

        #region MyDataSelected
        private PersonWithButtonViewModel myDataSelected;
        /// <summary>
        /// MyDataSelected
        /// </summary>
        public PersonWithButtonViewModel MyDataSelected
        {
            get { return this.myDataSelected; }
            set { this.SetProperty(ref this.myDataSelected, value); }
        }
        #endregion

        public DelegateCommand 捲到2ZacharyCommand { get; private set; } 
        public DelegateCommand 捲到BobCommand { get; private set; }

        public Action<PersonWithButtonViewModel> 自動捲動到;

        public ScrollToPageViewModel()
        {
            MyData = PersonRepository.SampleWithButtonViewModel();

            捲到2ZacharyCommand = new DelegateCommand(捲到2Zachary);
            捲到BobCommand = new DelegateCommand(捲到Bob);
        }

        private void 捲到Bob()
        {
            if (自動捲動到 != null)
            {
                var fooItem = MyData.FirstOrDefault(x => x.Name == "Bob");
                if (fooItem != null)
                {
                    自動捲動到(fooItem);
                }
            }
        }

        private void 捲到2Zachary()
        {
            if (自動捲動到 != null)
            {
                var fooItem = MyData.FirstOrDefault(x => x.Name == "2Zachary");
                if (fooItem != null)
                {
                    自動捲動到(fooItem);
                }
            }
        }
    }
}

ListView 下拉更新 (PullToRefreshPage)

在這個範例頁面中,展示了如何使用 ListView 的下拉更新的手勢操作,也就是說,當在 ListView 的最上面顯示的是第一筆紀錄,此時,若使用手勢由上往下滑動,此時,就會自動通知 ListView 可以開始進入到資料更新模式,不過,若想要啟動這個功能,您的 ListView 需要設定屬性 IsPullToRefreshEnabled="True"
在 ListView 內,也綁定了 RefreshCommand="{Binding 更新資料Command}" 命令屬性與 IsRefreshing="{Binding IsBusy, Mode=TwoWay}" 屬性;也就是說,當進入到更新模式,更新資料Command 命令就會被呼叫,而 ViewModel 的 IsBusy 屬性會變成 True。
底下為 ViewModel 的 更新資料Command 命令要呼叫的方法,透過非同步的方式取得最新的資料,完成之後,設定 IsBusy 屬性為 True,讓 ListView 控制項不會顯示正在忙碌的視覺。
        private async void 更新資料()
        {
            await 從網路取得最新資料();
            IsBusy = false;
        }

        public async Task 從網路取得最新資料()
        {
            await Task.Delay(3000);
            var fooMyData = PersonRepository.SampleWithButtonViewModel();
            foreach (var item in fooMyData)
            {
                MyData.Add(item);
            }
        }

PullToRefreshPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFListView.Views.PullToRefreshPage"
             Title="下拉更新" >

  <StackLayout>
    <ListView x:Name="listview"
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      IsPullToRefreshEnabled="True"
              RefreshCommand="{Binding 更新資料Command}"
              IsRefreshing="{Binding IsBusy, Mode=TwoWay}"
      >
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}"
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
              <StackLayout Grid.Column="1" Orientation="Vertical">
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
              </StackLayout>
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

</ContentPage>

PullToRefreshPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class PullToRefreshPageViewModel : BindableBase
    {
        #region MyData
        private ObservableCollection<PersonWithButtonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonWithButtonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion


        #region IsBusy 
        private bool isBusy;
        /// <summary>
        /// IsBusy 
        /// </summary>
        public bool IsBusy 
        {
            get { return this.isBusy; }
            set { this.SetProperty(ref this.isBusy, value); }
        }
        #endregion


        public DelegateCommand 更新資料Command { get; private set; } 

        public PullToRefreshPageViewModel()
        {
            MyData = PersonRepository.SampleWithButtonViewModel();
            更新資料Command = new DelegateCommand(更新資料);
        }

        private async void 更新資料()
        {
            await 從網路取得最新資料();
            IsBusy = false;
        }

        public async Task 從網路取得最新資料()
        {
            await Task.Delay(3000);
            var fooMyData = PersonRepository.SampleWithButtonViewModel();
            foreach (var item in fooMyData)
            {
                MyData.Add(item);
            }
        }

    }
}

ListView 點選與取消選取 (ItemClickPage)

在這個範例頁面中,展示了如何設計當使用者點選了某個項目的時候,就會執行相對應的 ICommand 所定義的方法;要達成不使用 Code Behind 來寫程式碼,而透過 ViewModel 來完成這樣的需求,您需要在 XAML 命名空間中引用 xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors" 這個命名空間,並且要在專案內安裝 Behaviors.Forms NuGet 套件。
因此,您可以在 ListView 內,使用底下宣告標記,宣告當 ListView 的 ItemTapped 事件發生的時候,就會執行綁定的 ICommand MyDataClickedCommand 方法;在這裡,請您測試看看,若將EventName="ItemTapped" 改成 EventName="ItemSelected" ,整個執行過程中會有甚麼不同呢?
      <ListView.Behaviors>
        <behaviors:EventHandlerBehavior EventName="ItemTapped">
          <behaviors:InvokeCommandAction Command="{Binding MyDataClickedCommand}"  />
        </behaviors:EventHandlerBehavior>
      </ListView.Behaviors>

ItemClickPage.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"
             prism:ViewModelLocator.AutowireViewModel="True"
             xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
             x:Class="XFListView.Views.ItemClickPage"
             Title="ListView 點選與取消選取">

  <StackLayout>
    <ListView x:Name="listview"
      ItemsSource="{Binding MyData}"
      SelectedItem="{Binding MyDataSelected, Mode=TwoWay}"
      Margin="20,0"
      IsPullToRefreshEnabled="True"
              RefreshCommand="{Binding 更新資料Command}"
              IsRefreshing="{Binding IsBusy, Mode=TwoWay}"
      >
      <ListView.Behaviors>
        <behaviors:EventHandlerBehavior EventName="ItemTapped">
          <behaviors:InvokeCommandAction Command="{Binding MyDataClickedCommand}"  />
        </behaviors:EventHandlerBehavior>
      </ListView.Behaviors>
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <Grid
              >
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>
              <BoxView Grid.Column="0" BackgroundColor="{Binding FavoriteColor}"
                       VerticalOptions="Center" HorizontalOptions="Center"
                       HeightRequest="40" WidthRequest="40"/>
              <StackLayout Grid.Column="1" Orientation="Vertical">
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Name}" />
                <Label
                  TextColor="Teal" FontSize="14"
                  Text="{Binding Birthday, StringFormat='{0:yyyy-MM-dd}'}" />
              </StackLayout>
            </Grid>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

</ContentPage>

ItemClickPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFListView.Repositories;

namespace XFListView.ViewModels
{
    public class ItemClickPageViewModel : BindableBase
    {

        private readonly INavigationService _navigationService;

        #region MyData
        private ObservableCollection<PersonWithButtonViewModel> myData;
        /// <summary>
        /// MyData
        /// </summary>
        public ObservableCollection<PersonWithButtonViewModel> MyData
        {
            get { return this.myData; }
            set { this.SetProperty(ref this.myData, value); }
        }
        #endregion


        #region MyDataSelected
        private PersonWithButtonViewModel myDataSelected;
        /// <summary>
        /// MyDataSelected
        /// </summary>
        public PersonWithButtonViewModel MyDataSelected
        {
            get { return this.myDataSelected; }
            set { this.SetProperty(ref this.myDataSelected, value); }
        }
        #endregion

        public DelegateCommand MyDataClickedCommand { get; private set; }

        public ItemClickPageViewModel(INavigationService navigationService)
        {

            _navigationService = navigationService;
            MyData = PersonRepository.SampleWithButtonViewModel();
            MyDataClickedCommand = new DelegateCommand(MyDataClicked);
        }

        private async void MyDataClicked()
        {
            await _navigationService.NavigateAsync("Basic1Page");
            // 若沒有加入底下這行,則當回到這個頁面的時候,這筆一樣有被選取
            MyDataSelected = null;
        }
    }
}

參考資料

企業之差旅費用 跨平台應用程式開發 (2) 建立專案

在這裡,您已經準備好要來進行開發跨平台 ( Android, iOS, UWP ) 的行動應用程式開發,並且在行動應用程式中,將會呼叫企業後端所提供的 Web API 服務,以便讓使用者可以透過行動裝置,進行差旅費用的申請;這是企業內部很典型的一個應用程式,接下來就是需要開始建立專案與進行相關設定。
在這個階段的練習,您將會學會底下的 Xamarin.Forms 的開發技術:
  1. 使用 Visual Studio 2015 增加 Prism 專案樣版
  2. 建立 Prism for Xamarin.Forms 的專案
  3. 升級與安裝必要的套件(NuGet Package)或者插件(Plug-ins)
  4. 建立專案資料夾
這篇章節的練習專案的原始程式碼將會存放在 GitHubhttps://github.com/vulcanlee/XFAppSample/tree/master/XFDoggy/1.BuildProject 內,在這個目錄下,會有兩個目錄:
  • Starter
    這個目錄將會存放當您要進行練習開發這個專案之前,Visual Studio 專案的原始檔案。
  • Completed
    這個目錄將會存放當您要進行練習開發這個專案完成之後,Visual Studio 專案的原始檔案。

使用 Visual Studio 2015 增加 Prism 專案樣版

若您的 Visual Studio 開發環境中,尚未安裝 Prism Template Pack 擴充功能,請參考底下步驟,進行安裝這個擴充功能。

安裝 Prism Template Pack

  1. 首先,開啟 Visual Studio 2015
  2. 點選功能表 工具 > 擴充功能與更新 選項
  3. 在 擴充功能與更新 對話窗內,點選 線上 > Visual Studio 組件庫
  4. 在右上方文字輸入盒內,輸入 Prism Template Pack ,並且按下 Enter 按鍵
  5. 點選搜尋出來的 Prism Template Pack 項目,點選 下載 按鈕,安裝 Prism Template Pack 組件到您的Visual Studio 2015 內;安裝完成後,您需要點選 立即重新啟動 按鈕,讓您的 Visual Studio 2915 重新啟動之後,這次的安裝才會生效。
    PrismTemplatePack
    PrismTemplatePack下載與安裝

建立 Prism for Xamarin.Forms 的專案

  1. 當 Visual Studio 重新啟動之後,點選功能表 檔案 > 新增 > 專案
  2. 在 新增專案 對話窗內,點選 範本 > Visual C# > Prism > Prism Unity App (Forms),並且在底下名稱欄位中,輸入 XFDoggy 最後,點選 確定 按鈕。
  3. 接著,會出現 Prism for Xamarin.Forms - Project Wizard 這個對話窗,請在這個對話窗中,勾選Android / iOS / UWP 這三個檢查盒,最後點選 Create 按鈕,以便產生這三個類型的專案。
  4. 當專案建立到一半,若您的開發環境還沒有建置與設定完成 Mac 電腦與 Xamarin Studio for Mac 系統,此時會看到 Xamarin Mac Agent Instructions 對話窗出現,這個對話窗是要提醒您進行與 Mac 電腦連線設定,這是因為,您無法在 Windows 作業系統進行 iOS 相關應用程式的建立與設計工作,而是需要透過 Mac 電腦安裝的 XCode 來協助您完成這些 iOS 應用程式的建立工作。不過,這不影響您繼續開發 Xamarin.Forms 的應用程式,只不過此時,您無法編譯與執行 iOS 的應用程式。
  5. 接著會看到 新的通用Windows專案 對話視窗,此時,您只需要按下 確定 按鈕即可,此時,專案精靈會繼續完成相關平台的專案建立工作。
  6. 最後,整個新的 Xamarin.Forms 專案就建立完成了。

升級與安裝必要的套件(NuGet Package)或者插件(Plug-ins)

在這個專案內,將會使用到許多不同的 NuGet 套件 (Package),因此,您需要先在這裡把這些套件安裝到所有的專案內,方便日後開發作業可以更加順利。
  1. 滑鼠右擊方案 XFDoggy ,選擇 管理方案的 NuGet 套件,在 NuGet - 解決方案 頁面中,切換到 瀏覽標籤頁次內,請在文字輸入盒中輸入,Behaviors.Forms 並且開始搜尋這個套件,並且安裝到所有的專案內。
    使用這個套件,可以讓您在宣告 XAML 定義的時候,使用 XAML 提供的行為特性功能,例如,可以綁定當某個事件發生的時候,將會執行某個指定的 ICommand。這項功能對於有在開發 WPF 或者 Windows UWP 的開發者而言,必定不陌生,因為,在這些環境之下,可以使用 Blend SDK 所提供的各種不同行為(Behaviors)功能,來擴增或者豐富您的 XAML 宣告。
    NuGetBehaviors.Forms
  2. 滑鼠右擊方案 XFDoggy ,選擇 管理方案的 NuGet 套件,在 NuGet - 解決方案 頁面中,切換到 瀏覽標籤頁次內,請在文字輸入盒中輸入,Newtonsoft.Json 並且開始搜尋這個套件,並且安裝到所有的專案內。
    'Newtonsoft.Json' 這個套件,可以讓您進行 JSON 物件的 序列化 (Serialization) 或者 反序列化 (Deserialization)) 的操作;這項功能將會提供當從網路取得開發資料的 JSON 定義資料之後,接著,便可以將這些 JSON 定義文字轉換成為 .NET 的物件。
    NuGetNewtonsoft.Json
  3. 滑鼠右擊核心PCL專案 XFDoggy ,選擇 管理 NuGet 套件,在 NuGet - 解決方案 頁面中,切換到 瀏覽 標籤頁次內,請在文字輸入盒中輸入,Microsoft.Net.Http 並且開始搜尋這個套件,並且安裝到核心PCL專案內;在安裝過程中,若看到 接受授權 對話窗,請點選 我接受 按鈕
    接受授權
    由於在撰寫這份文件的時候 (2016.09.19),本機電腦 Visual Studio 的 Xamarin Toolkit 已經升級到 Xamarin 4.2 版,而透過這個Xamarin版本再產生 核心PCL專案 的時候,其 PCL TargetFrameworkProfile 已經採用了 Profile259 (前一個 Xamarin 版本,使用的 Profile111);而在 Profile259 的 PCL專案內,是沒有這個 System.Net.Http 命名空間。
    因此,需要額外安裝這個 Microsoft.Net.Http NuGet 套件,以便可以使用 HttpClient 這樣的類別。
    想要查看您的 PCL專案的 TargetFrameworkProfile 是哪個版本,可以先使用滑鼠右擊 核心PCL XFDoggy 專案,接著選取 卸載專案,再度使用滑鼠右擊 核心PCL XFDoggy 專案,選取 編輯 XFDoggy.csproj 這個選項。
    在下面截圖中,將會看到紅色框線處標示的,就是這個 PCL 的 Profile 版本
    XFDoggy.csproj
    有關更多不同 PCL Profile 資訊,可以參考這個網址http://portablelibraryprofiles.apps.stephencleary.com/

建立專案資料夾

核心 PCL 專案

請在 核心 PCL 專案內依序建立底下的資料夾
  1. 滑鼠右擊 XFDoggy 核心PCL 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Helps
  2. 滑鼠右擊 XFDoggy 核心PCL 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Infrastructure
  3. 滑鼠右擊 XFDoggy 核心PCL 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Models
  4. 滑鼠右擊 XFDoggy 核心PCL 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Services

平台專案

請在每個平台的專案下,依序建立底下資料夾
  1. 滑鼠右擊 XFDoggy.Droid 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Infrastructure
  2. 滑鼠右擊 XFDoggy.iOS 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱Infrastructure
  3. 滑鼠右擊 XFDoggy.UWP 專案節點,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱Infrastructure
  4. 滑鼠右擊 XFDoggy.UWP 專案節點內的 Assets 資料夾,選擇 加入 > 新增資料夾,在新產生的資料夾內,填入資料夾名稱 Images

複製圖片檔案

  1. 請下載底下連結中的圖片檔案到本機檔案總管內的某個資料夾內
  2. 請拖拉這些圖片檔案到 XFDoggy.Droid 專案 > Resources > drawable 目錄下
  3. 請拖拉這些圖片檔案到 XFDoggy.iOS 專案 > Resources 目錄下
  4. 請拖拉這些圖片檔案到 XFDoggy.UWP 專案 > Assets > Images 目錄下

第一次啟動專案測試

  1. 滑鼠右擊 XFDoggy.Droid 專案節點,選擇 設定為起始專案
  2. 按下 F5 按鍵,該使執行這個 Android 專案,看看是否可以正常執行。
  3. 若可以正常編譯,但是執行的時候出現了錯誤訊息:java.lang.OutOfMemoryError
    • 此時請滑鼠雙擊 XFDoggy.Droid 專案 Properties 節點
    • 在 XFDoggy.Droid 頁面,點選 Android Options > Advanced
    • 在 Java Max Heap Size 欄位內輸入 1G
    • 重新執行一次
    AndroidMaxHeap

2016/10/04

企業之差旅費用 跨平台應用程式開發 (1) 專案需求說明

這是一個企業內部行動應用程式的開發範例,透過已經開發好的 Web API,開發出一套跨平台的行動裝置應用程式,可以分別在 Android / iOS / UWP 裝置上執行,讓企業內部使用者透過這個應用程式進行差旅費用的申請。
若要瞭解這些 Web API 如何使用,可以下載這個 PostMan 定義檔案,查看相關範例

2016/10/03

如何啟用 Xamarin.Forms 的 XAML 編輯有 IntelliSense效果

當您在使用 Visual Studio 2105 編輯 .xaml 檔案內的 XAML 標籤宣告語言,Visual Studio 2015 提供了 IntelliSense 的輔助輸入提示功能;不過,若您的 Visual Studio 2015 對於正在編輯的 XAML 檔案,無法正常使用 IntelliSense 的功能,可以參考底下步驟。
XAML沒有IntelliSense
  • 關閉 Visual Studio 2015 所有正在編輯的標籤頁次
  • 離開 Visual Studio
  • 重新使用 Visual Studio 開啟這個專案
  • 使用滑鼠右擊 XAML 檔案,選擇 開啟方式 > 原始程式碼(文字)編輯器,接著點選 設定為預設值 按鈕,最後點選 確定 按鈕
    原始程式碼文字編輯器
在下圖中,您可以看到使用了 原始程式碼(文字)編輯器 方式來開啟 XAML 檔案與沒有的差異,那就是最前面的 <?xml version="1.0" encoding="utf-8" ?> 文字,顯示為藍色的,說明這樣開啟 XAML 檔案的方式,具有 IntelliSense 的效果。
XAML有IntelliSense.png

2016/10/02

Xamarin.Forms 使用 Grial UI Kit 在Android可以執行,在iOS卻會閃退

使用 Grial UI Kit 的專案,發現到 Android 專案可以正常執行,可是,在 iOS 專案下執行,卻會發現到有System.IO.FileNotFoundException: Could not load file or assembly 'UXDivers.Artina.Shared' or one of its dependencies 例外異常錯誤訊息。
當您使用 Xamarin.Forms 搭配 XAML 開發方式,執行 iOS 專案的時候,可能會發生某些組件無法正常被載入的情況。
假使您僅僅在 XAML 中參考這些組件,就會發生這樣的例外異常錯誤。
您可以在 AppDelegate.cs 檔案中,加入底下程式碼,強迫要將這個組件讀入到記憶體中
var workaround = typeof(UXDivers.Artina.Shared.CircleImage);