XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

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

2016/11/15

Xamarin.Forms 水平捲動式的 ListView

在 Xamarin.Forms 所提供的內建控制項中,僅能夠提供垂直捲動的方式來呈現出清單資料,不過,有很多時候,我們需要使用能夠水平捲動的 ListView 功能,這個時候,Xamarin.Forms內建的控制項就無法幫助您做到這樣的需求。
不過,在網路上找到一篇文章
他提供了相當不錯的水平捲動的客製化控制項的原始碼,我在這裡進行一些簡化與修正,做到如下圖的效果;不過,若您對這樣的Custom Control 有興趣的話,您首先需要對於 BindableProperty 要有所認識與知道如何實作。

專案原始碼

客製化水平捲動 ListView 控制項

其實,這個客製化控制項相當的簡單,那就是繼承了 ScrollView 這個 Xamarin.Forms 的控制項,並且設定使用的是水平捲動的方式,而要顯示的內容,則是透過了一個可綁定屬性 ItemsSourceProperty 讓開發者可以在 XAML 中來綁定這個屬性,設定要顯示的資料清單來源。在這個 ItemsSourceProperty 可綁定屬性中,會訂閱 INotifyCollectionChanged 這個事件,其用於告知接收程式發生動態變更,例如當加入和移除項目時,或重新整理整份清單時。因此,當資料有異動的時候,就會同步將 ScrollView 內的 StackLayout 裡面的項目,進行新增或者刪除。
這個客製化控制項,可以使用 DataTemplate 來在 XAML 中自行擴充與定義想要每筆資料長成甚麼樣子。
原作者的有些內容,在這裡我將其移除,因為,我用不到,有興趣的人,可以自行參考原作者的文章。
原始碼如下
    public class HorizontalListView : ScrollView
    {
        readonly StackLayout _imageStack;

        public HorizontalListView()
        {
            this.Orientation = ScrollOrientation.Horizontal;

            _imageStack = new StackLayout
            {
                Orientation = StackOrientation.Horizontal
            };

            this.Content = _imageStack;
        }

        public IList<View> Children
        {
            get
            {
                return _imageStack.Children;
            }
        }

        // http://dotnetbyexample.blogspot.tw/2016/03/xamarin-form-21-upgradesome-surprises.html
        public static readonly BindableProperty ItemsSourceProperty =
            BindableProperty.Create(nameof(ItemsSource), // 屬性名稱 
                typeof(IList),  // 回傳類型
                typeof(HorizontalListView),  // 宣告類型
                default(IList), // 預設值 
                BindingMode.TwoWay,  // 預設資料繫結模式
                propertyChanging: (bindableObject, oldValue, newValue) =>
                {
                    ((HorizontalListView)bindableObject).ItemsSourceChanging();
                },
                propertyChanged: (bindableObject, oldValue, newValue) =>
                {
                    ((HorizontalListView)bindableObject).ItemsSourceChanged(bindableObject, oldValue as IList, newValue as IList);
                }
       );

        public IList ItemsSource
        {
            get
            {
                return (IList)GetValue(ItemsSourceProperty);
            }
            set
            {

                SetValue(ItemsSourceProperty, value);
            }
        }

        void ItemsSourceChanging()
        {
            if (ItemsSource == null)
                return;
        }

        void ItemsSourceChanged(BindableObject bindable, IList oldValue, IList newValue)
        {
            if (ItemsSource == null)
                return;

            var notifyCollection = newValue as INotifyCollectionChanged;
            if (notifyCollection != null)
            {
                notifyCollection.CollectionChanged += (sender, args) =>
                {
                    if (args.NewItems != null)
                    {
                        foreach (var newItem in args.NewItems)
                        {

                            var view = (View)ItemTemplate.CreateContent();
                            var bindableObject = view as BindableObject;
                            if (bindableObject != null)
                                bindableObject.BindingContext = newItem;
                            _imageStack.Children.Add(view);
                        }
                    }
                    if (args.OldItems != null)
                    {
                        // not supported
                        _imageStack.Children.RemoveAt(args.OldStartingIndex);
                    }
                };
            }

        }

        public DataTemplate ItemTemplate
        {
            get;
            set;
        }

    }

如何使用客製化水平捲動 ListView 控制項

由於這個是自訂控制項,因此,想要在頁面中使用,當然必須先要自訂一個命名空間,在這裡,定義了一個 xmlns:Controls="clr-namespace:XFHorListView.Controls" 指向了這個自訂控制項。
這個客製化水平捲動 ListView 控制項支援了 DataTemplate 功能,可以讓開發者自行透過 XAML 來描述資料需要以甚麼方式來呈現;另外,在這個控制項內,若想要設定當使用者點選某個項目的時候,需要執行 ViewModel 內的某個 DelegatedCommand 命令,可以在 DataTemplate 內,設定 View.GestureRecognizers 使用 TapGestureRecognizer 來觸發 ViewModel 內的命令;在範例中,所要觸發的命令將會定義在頁面 ViewModel 內,因此,若要綁定這個命令,需要在進行資料綁定的時候,重新指定綁定資料來源:
Command="{Binding Path=BindingContext.使用者點選Command, Source={x:Reference ThisPage}}"
在這裡,當要進行資料繫結的時候,會重新設定綁定資料來源為根結點,也就是這個頁面,接著使用 Path 來指定要綁定的來源是頁面的 BindingContext 下的使用者點選Command
<?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:Controls="clr-namespace:XFHorListView.Controls"
             xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
             x:Class="XFHorListView.Views.MainPage"
             Title="MainPage"
             x:Name="ThisPage">

    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding Title}" />
        <Controls:HorizontalListView
                ItemsSource="{Binding MyDatas}"
            HorizontalOptions="Fill"
                >
            <Controls:HorizontalListView.ItemTemplate>
                <DataTemplate>
                    <Grid BackgroundColor="Transparent">
                        <Image
                            Source="{Binding ImageUrl}"
                            Aspect="AspectFit">
                            <Image.GestureRecognizers>
                                <TapGestureRecognizer
                                    Command="{Binding Path=BindingContext.使用者點選Command, Source={x:Reference ThisPage}}"
                                    CommandParameter="{Binding}" />
                            </Image.GestureRecognizers>
                        </Image>
                        <BoxView 
                            HeightRequest="30"
                            VerticalOptions="End"
                            Color="{Binding Color}" />
                        <Label
                            Margin="0,0,0,10"
                            VerticalOptions="End"
                            TextColor="White"
                            Text="{Binding Title}" />
                    </Grid>
                </DataTemplate>
            </Controls:HorizontalListView.ItemTemplate>
        </Controls:HorizontalListView>
        <ListView
            ItemsSource="{Binding MyDatas}"
            SelectedItem="{Binding 使用者點選項目}"
            >
            <ListView.Behaviors>
                <behaviors:EventHandlerBehavior EventName="ItemTapped">
                    <behaviors:InvokeCommandAction Command="{Binding ListView使用者點選Command}"  />
                </behaviors:EventHandlerBehavior>
            </ListView.Behaviors>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="100" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Image 
                                Grid.Column="0"
                                Source="{Binding ImageUrl}" Aspect="AspectFit" />
                            <BoxView 
                                Grid.Column="1"
                                Color="{Binding Color}" />
                            <Label 
                                Grid.Column="1"
                                TextColor="White"
                                Text="{Binding Title}" />
                        </Grid>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

2016/11/14

Xamarin.Forms 有園角或圓形的使用

Xamarin.Forms 內的 BoxView 僅能夠指定背景顏色,並且繪製出有背景顏色地舉行;若想要繪製出有園角或者圓形的圖樣,這個時候,可以借助第三方套件 RoundedBoxView 來完成。
不過,找到一個可用圓角的BoxView套件,但是,也許作者太久沒維護了,導致不支援 PCL Profile259,因此,無法用於專案內。
為此,只好根據該套件的原始碼,自行將這些原始碼放到測試專案內,這樣就可以顯示圓角矩形控制項。
XFRoundedBoxView
範例專案

2016/11/13

Xamarin.Forms 使用具有 GridView 特性圖片畫廊

傳統上 Xamarin.Forms 並沒有提供類似 GridView 這樣的控制項,也就是可以使用多 Column 的方式來顯示集合資料,但是其他的行為與 ListView 一樣。
在這份筆記中,將會測試與使用 DLToolkit.Forms.Controls.FlowListView 這個 NuGet 套件,就可以達成使用 GridView 控制項的圖片畫廊效果。
要使用這個功能,您需要在方案中加入這個 DLToolkit.Forms.Controls.FlowListView & Xamarin.FFImageLoading.Forms 這兩個 NuGet 套件到所有的專案內;下面螢幕截圖為實際執行結果。
XFHListView
範例專案

需要安裝 NuGet 套件

要完成這個筆記中的效果,你需要在方案中安裝底下的 NuGet 套件
Xamarin.FFImageLoading.Forms
DLToolkit.Forms.Controls.FlowListView

範例說明

首先,需要定義這個集合的資料模型。
在這個,建立了一個 SampleItems1 資料模型,裡面有 Title / Color / ImageUrl 這三個屬性。
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;

namespace XFHListView.Models
{
    public class SampleItems1 : BindableBase
    {

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


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


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

        public SampleItems1()
        {

        }
    }
}
接著需要在 ViewModel 內定義所有資料綁定的集合資料,並且要進行集合資料的初始化,可以參考底下程式碼。
在這個 ViewModel 內,集合資料將會使用 ObservableCollection<SampleItems1> 型別,並且儲存在 MyDatas 這個物件內。
在建構式內,呼叫了 Init() 方法,針對 MyDatas 進行產生實際物件並且進行資料初始化。
在進行 MyDatas 物件初始化的過程中,會透過一個靜態屬性 RandomColor.PickColor 來得到動態的顏色物件。而圖片的 URL 則是會透過一個 images 陣列來取得
另外,也定義了一個命令 使用者點選Command,定義了當使用者點選某個特定項目之後,就會顯示出他選取的項目內容。
using Prism.Commands;
using Prism.Mvvm;
using Prism.Services;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using XFHListView.Helper;
using XFHListView.Models;

namespace XFHListView.ViewModels
{
    public class SimpleGalleryPageViewModel : BindableBase
    {
        string[] images = {
                "https://farm9.staticflickr.com/8625/15806486058_7005d77438.jpg",
                "https://farm5.staticflickr.com/4011/4308181244_5ac3f8239b.jpg",
                "https://farm8.staticflickr.com/7423/8729135907_79599de8d8.jpg",
                "https://farm3.staticflickr.com/2475/4058009019_ecf305f546.jpg",
                "https://farm6.staticflickr.com/5117/14045101350_113edbe20b.jpg",
                "https://farm2.staticflickr.com/1227/1116750115_b66dc3830e.jpg",
                "https://farm8.staticflickr.com/7351/16355627795_204bf423e9.jpg",
                "https://farm1.staticflickr.com/44/117598011_250aa8ffb1.jpg",
                "https://farm8.staticflickr.com/7524/15620725287_3357e9db03.jpg",
                "https://farm9.staticflickr.com/8351/8299022203_de0cb894b0.jpg",
            };

        #region MyDatas
        private ObservableCollection<SampleItems1> _MyDatas;
        /// <summary>
        /// MyDatas
        /// </summary>
        public ObservableCollection<SampleItems1> MyDatas
        {
            get { return _MyDatas; }
            set { SetProperty(ref _MyDatas, value); }
        }
        #endregion

        #region 使用者點選項目
        private SampleItems1 _使用者點選項目;
        /// <summary>
        /// 使用者點選項目
        /// </summary>
        public SampleItems1 使用者點選項目
        {
            get { return this._使用者點選項目; }
            set { this.SetProperty(ref this._使用者點選項目, value); }
        }
        #endregion

        public DelegateCommand 使用者點選Command { get; set; }

        public readonly IPageDialogService _dialogService;
        public SimpleGalleryPageViewModel(IPageDialogService dialogService)
        {

            _dialogService = dialogService;
            Init();

            使用者點選Command = new DelegateCommand(() =>
            {
                _dialogService.DisplayAlertAsync("訊息", $"你選取的是 {使用者點選項目.Title}", "OK");
            });
        }

        public void Init()
        {
            MyDatas = new ObservableCollection<SampleItems1>();

            var howMany = new Random().Next(100, 500);

            for (int i = 0; i < howMany; i++)
            {
                var foo = i % 10;
                MyDatas.Add(new SampleItems1() {
                    Title = string.Format("項目 {0}", i),
                    ImageUrl = images[foo],
                    Color = RandomColor.PickColor
                });
            }
        }
    }
}
有的 Model / ViewModel,接下來,就是要定義 View,可以參考底下 XAML 。
這個頁面 XAML 內容相當的簡單,根項目為 ContentPage,並且其兒子為 FlowListView;不過,由於這個 FlowListView 控制項並不是 Xamarin.Forms 預設命名空間所提供的,因此,需要在跟項目內另外定義一個命名空間 xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView",這樣,就可以使用前置詞 flv 來引用 FlowListView 這個控制項。
FlowListView 這個控制項內,使用了 FlowColumnCount="3" 宣告了這個 GridView 效果共有三個 Column,並且每筆紀錄並沒有使用分隔線,這是使用了這個宣告:SeparatorVisibility="None"
另外,使用了 HasUnevenRows="True" 設定依據當時記錄本身顯示的高度作為高度;當使用者點選了清單內的某個項目,可以使用 FlowItemTappedCommand 來綁定 ViewModel 內的某個 DelegateCommand ,就可以在 ViewModel 內撰寫相關點擊後的處理邏輯;而使用者點選後的項目,也會透過 FlowLastTappedItem 這個屬性,將其綁定到 ViewModel 內的某個屬性,也就是說,當使用者點選某個項目,就可以透過該 ViewModel 內的屬性,取得使用者當時點選的項目內容;最後,該 FlowListView 要顯示的資料清單內容,將會透過 FlowItemsSource 屬性來進行資料綁定。
最後,與 ListView 相同,每筆資料,可以透過 FlowListView.FlowColumnTemplate 來設定這些資料項目要使用何種格式來顯示。
關於在每個紀錄上要顯示圖片的時候,不使用 Xamarin.Forms 的 Image 檢視,而是使用了 CachedImage,要使用這個檢視,必須要在根項目內定義新的前置詞與命名空間(FFImageLoading) 這樣才能夠在該頁面中引用。這個 ffimageloading:CachedImage 檢視增加了許多功能,例如, LoadingPlaceholder & ErrorPlaceholder 則是可以放置一個圖片名稱,用於當圖片尚未被載入到頁面的時候與讀取發生錯誤的時候要顯示的圖片;RetryCount 這個屬性用於當取得圖片發生錯誤的時候,需要重新再度讀取的次數;CacheDuration 則是設定這個圖片需要快取的保存期限;DownsampleHeight & DownsampleWidth 若有設定的話,會自動依據這兩個屬性設定值,縮放成為其定義大小,不過,若 DownsampleUseDipUnits 設定為 False,則表示要縮放的單為 Pixel,若設定為 True,則表示要縮放的單位為 DIP。DownsampleToViewSize若設定為 True,則會自動縮放大小成為當時 View 的大小。
<?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:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView"
             xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
             x:Class="XFHListView.Views.SimpleGalleryPage"
             Title="圖片畫廊">

    <flv:FlowListView
        FlowColumnCount="3"
        SeparatorVisibility="None"
        HasUnevenRows="True"
        FlowItemTappedCommand="{Binding 使用者點選Command}"
        FlowLastTappedItem="{Binding 使用者點選項目}"
        FlowItemsSource="{Binding MyDatas}"
        FlowColumnMinWidth="110"
        >
        <flv:FlowListView.FlowColumnTemplate>
            <DataTemplate>
                <Grid Padding="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>

                    <ffimageloading:CachedImage 
                        HeightRequest="100" 
                        Aspect="AspectFill"
                        DownsampleHeight="100" DownsampleUseDipUnits="false" 
                        LoadingPlaceholder="image_loading.png" ErrorPlaceholder="image_error.png"
                        Source="{Binding ImageUrl}"/>

                    <Label x:Name="Label" HorizontalOptions="Fill" HorizontalTextAlignment="Center" VerticalOptions="End"
                        BackgroundColor="Black" Opacity="0.5" 
                           TextColor="{Binding Color}"
                           Text="{Binding Title}"/>
                </Grid>
            </DataTemplate>
        </flv:FlowListView.FlowColumnTemplate>

    </flv:FlowListView>
</ContentPage>

2016/11/12

Xamarin.Forms 動態產生檢視項目的 WrapView

在現在的 Xamarin.Forms 內建的控制項中,並沒有辦法針對動態產生的控制項,使用 XAML & ViewModel 來實現這樣的效果,因此,在這裡,參考了 XLabs 內提供的 WrapLayout,將其相關原始碼抽取出來,就可以實踐這樣的效果。
範例專案原始碼
XFWrapView
在這個範例中,第1個 Row 中,將會顯示 View Model 內的35五筆資料,並且使用 DataTemplate 方式定義其要顯示的樣貌;不過,在這裡將會使用水平排列的方式來顯示。
在這個範例中,第2個 Row 中,將會顯示 View Model 內的35五筆資料,並且使用 DataTemplate 方式定義其要顯示的樣貌;不過,在這裡將會使用垂直排列的方式來顯示。
<?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"
             xmlns:local="clr-namespace:XFWrapLayout"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFWrapLayout.Views.MainPage"
             Title="MainPage">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <local:WrapView Grid.Row="0" ItemsSource="{Binding MyItems}" Orientation="Horizontal">
            <local:WrapView.ItemTemplate>
                <DataTemplate>
                    <StackLayout BackgroundColor="Blue" WidthRequest="40" HeightRequest="20" VerticalOptions="Fill" Padding="6, 6, 6, 6">
                        <Label Text="{Binding Name}" FontSize="Small" XAlign="Start" TextColor="White" VerticalOptions="EndAndExpand"/>
                    </StackLayout>
                </DataTemplate>
            </local:WrapView.ItemTemplate>
        </local:WrapView>
        <local:WrapView Grid.Row="1" ItemsSource="{Binding MyItems}" Orientation="Vertical">
            <local:WrapView.ItemTemplate>
                <DataTemplate>
                    <StackLayout BackgroundColor="Red" WidthRequest="40" HeightRequest="20" VerticalOptions="Fill" Padding="6, 6, 6, 6">
                        <Label Text="{Binding Name}" FontSize="Small" XAlign="Start" TextColor="White" VerticalOptions="EndAndExpand"/>
                    </StackLayout>
                </DataTemplate>
            </local:WrapView.ItemTemplate>
        </local:WrapView>
    </Grid>
</ContentPage>