2017/5/26

Xamarin.Forms 教學系列文(十六.貳) BindingMode & StringFormat & BindingConverter


學習目標
  • BindingMode 三種綁定方向
  • StringFormat 資料顯示的格式
  • BindingConverter 客製化資料轉換工具

這篇篇幅會較長一點,主要是延續上一小節 DataBinding,需要學習的周邊知識。

The Binding Mode

正常來說若有一個 Label 的字體大小要隨著 Slider 更改,綁定會像這樣寫:
<Label FontSize="{Binding Source={x:Reference slider}, Path=Value}" />
<Slider x:Name="slider" Maximum="100" />

但我們將 Target 和 Source 反過來綁定看看:
<Label x:Name="label" /> 
<Slider Maximum="100" Value="{Binding Source={x:Reference label}, Path=FontSize}" />
看起來好像沒啥意義的寫法,但這段是可以成功執行的,Slider 可以更改 Label 字的大小。


為什麼???

這就是我們接著要談的 Binding Mode -


做 Data Binding 時可以選擇綁定模式,更改 Target 和 Source 影響的方向。

Binding Mode 共有四種:
  • Default
  • OneWay - 單向綁定 (Source 影響 Target)
  • OneWayToSource - 反向綁定 (Target 影響 Source)
  • TwoWay - 雙向綁定 (Target Source 互相影響)

大部分的 Bindable Mode 預設都是 OneWay,

但底下這些是例外,預設為 TwoWay

Class
Property that is TwoWay
Slider
Value
Stepper
Value
Switch
IsToggled
Entry
Text
Editor
Text
SearchBar
Text
DatePicker
Date
TimePicker
Time

回到本小節一開始的例子,就是因為 Slider 預設為 TwoWay

所以即使我們反過來將 Lable 設為 Source,還是能執行更改字體大小的功能。

來看 Binding Mode 的寫法:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BindingModes.BindingModesPage"
             Padding="10, 0">
    
    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="StackLayout">
                <Setter Property="VerticalOptions" Value="CenterAndExpand" />
            </Style>
            <Style TargetType="Label">
                <Setter Property="HorizontalOptions" Value="Center" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout VerticalOptions="Fill">
        <StackLayout>
            <!--Default-->
            <Label Text="Default"
                   FontSize="{Binding Source={x:Reference slider1},
                                      Path=Value}" />
            
            <Slider x:Name="slider1"
                    Maximum="50" />
        </StackLayout>
        
        <StackLayout>
            <!--OneWay-->
            <Label Text="OneWay"
                   FontSize="{Binding Source={x:Reference slider2},
                                      Path=Value,
                                      Mode=OneWay}" />
            <Slider x:Name="slider2"
                    Maximum="50" />
        </StackLayout>
        
        <StackLayout>
            <!--OneWayToSource-->
            <Label Text="OneWayToSource"
                   FontSize="{Binding Source={x:Reference slider3},
                                      Path=Value,
                                      Mode=OneWayToSource}" />
            <Slider x:Name="slider3"
                    Maximum="50" />
        </StackLayout>
        
        <StackLayout>
            <!--TwoWay-->
            <Label Text="TwoWay"
                   FontSize="{Binding Source={x:Reference slider4},
                                      Path=Value,
                                      Mode=TwoWay}" />
            <Slider x:Name="slider4"
                    Maximum="50" />
        </StackLayout>
        
    </StackLayout>
</ContentPage>

當然第三個 OneWayToSource 字大小是不會動的:


要動起來可以寫成:
    <Label x:Name="label3" 
           Text="OneWayToSource" />
    
    <Slider Maximum="50" Value="{Binding Source={x:Reference label3},
                                         Path=FontSize,
                                         Mode=OneWayToSource}" />
OneWayToSource,當更改 Target (Slider) 時,會去更動 Source (Label)。



String Formatting

在 Data binding 時,資料格式除了在 C# 處理外,XAML 也提供了簡易的寫法:

舉例來說,假設資料格式是 System.DateTime,但前端需要顯示 簡短日期  (yyyy/MM/dd):
        <Label Text="{Binding Source={x:Reference datepicker}, 
                              Path=Date, 
                              StringFormat='Today is {0:d}'}" />
        
        <DatePicker x:Name="datepicker" />


為什麼是 "Path"?

Data Binding 學到這裡可能有個疑惑,會什麼值的來源要叫做 Path 而不是 Property?

原因是,來源值並非只能為 Property,直接看底下範例就能理解,Path 的寫法可以相當多樣化。

但,這麼寫會增加維護上的難度… 所以 Path 還是 盡量保持為來源的 Property 就好。

*Oplatform 寫法已更改,請參考此篇文章
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:globe="clr-namespace:System.Globalization;assembly=mscorlib" 
             x:Class="BindingPathDemos.BindingPathDemosPage" 
             x:Name="page">

    <!--Padding-->
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" 
                    iOS="10, 20, 10, 0" 
                    Android="10, 0"
                    WinPhone="10, 0" />
    </ContentPage.Padding>
   
    <!--Resource-->
    <ContentPage.Resources>
        <ResourceDictionary>
            <Style x:Key="baseStyle" 
                   TargetType="View">

                <Setter Property="VerticalOptions"
                        Value="CenterAndExpand" />
            </Style>

            <Style TargetType="Label" BasedOn="{StaticResource baseStyle}">
                <Setter Property="FontSize" 
                        Value="Large" />

                <Setter Property="HorizontalTextAlignment" 
                        Value="Center" />
            </Style>

            <Style TargetType="Slider"
                   BasedOn="{StaticResource baseStyle}" />
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout BindingContext="{x:Reference page}">
        <!--取得 page 的 Top padding 值-->
        <Label Text="{Binding Path=Padding.Top, 
            StringFormat='The top padding is {0}'}" />
        
        <!--利用 Tree Trace 取值-->
        <Label Text="{Binding Path=Content.Children[4].Value,
            StringFormat='The Slider value is {0:F2}'}" />

        
        <Label Text="{Binding Source={x:Static globe:CultureInfo.CurrentCulture}, 
            Path=DateTimeFormat.DayNames[3], StringFormat='The middle day of the week is {0}'}" />

        <Label Text="{Binding Path=Content.Children[2].Text.Length, 
            StringFormat='The preceding Label has {0} characters'}" />
        <Slider />
    </StackLayout>
</ContentPage>


Binding Converter

若是來源到目標的轉型較為複雜,或是夾雜著自己寫的運算式,

我們就能利用 IValueConverter 來客製化轉型小工具。

IValueConverter 包含兩個方法,Convert ConvertBack,和一些固定的參數,
The ConvertBack method is called only for TwoWay or OneWayToSource bindings。

底下範例為,當 Entry 有輸入值時,Button 才能按下 (有點類似必填功能):

要準備的 Converter 就是 int (Entry.Text.Length) => bool (Button.IsEnabled):
using System;
using System.Globalization;
using Xamarin.Forms;

namespace Xamarin.FormsBook.Toolkit
{
    public class IntToBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (int)value != 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? 1 : 0;
        }
    }
}

XAML:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:toolkit= "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit" 
             x:Class="ButtonEnabler.ButtonEnablerPage" 
             Padding="10, 50, 10, 0">

    <ContentPage.Resources>
        <ResourceDictionary>
            <toolkit:IntToBoolConverter x:Key="intToBool" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout Spacing="20">
        <Entry x:Name="entry" 
               Text=""
               Placeholder="text to enable button" />

        <!--注意 Converter 的用法,這邊先將 Converter 做成資源檔再使用-->
        <Button Text="Save or Send (or something)"
                FontSize="Medium" 
                HorizontalOptions="Center" 
                IsEnabled="{Binding Source={x:Reference entry}, 
                Path=Text.Length, 
                Converter={StaticResource intToBool}}" />
    </StackLayout>
</ContentPage>

執行結果:



兩個小技巧:
1. 如果 Converter 只用到一次,就不用寫資源檔了
        <Button Text="Save or Send (or something)"
                FontSize="Large" 
                HorizontalOptions="Center">
            
            <Button.IsEnabled>
                <Binding Source="{x:Reference entry}" 
                         Path="Text.Length">
                    <Binding.Converter>
                        <toolkit:IntToBoolConverter />
                    </Binding.Converter>
                </Binding>
            </Button.IsEnabled>
        </Button>


2. 更加彈性的 Converter

假設我們要將 Switch 的值顯示出的 "Let's do it" or "Not now",而不是預設的 "True" or "False",

我們可以在 Converter 放入自訂屬性,XAML 使用時去設定其值:
namespace Xamarin.FormsBook.Toolkit
{
    public class BoolToStringConverter : IValueConverter
    {
        //自訂屬性
        public string TrueText { set; get; }
        public string FalseText { set; get; }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? TrueText : FalseText;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return false;
        }
    }
}

<toolkit:BoolToStringConverter 
                x:Key="boolToString" 
                TrueText="Let's do it" 
                FalseText="Not now" />













2 則留言:

  1. 你好 我到這段有個小小的疑問

    Style這個標籤 不會自動跳出來耶

    都要自己打全部...屬性標籤也是

    但其他的會出現也可以選

    回覆刪除
    回覆
    1. VS針對XAML的Intelisense似乎還沒有很完整...
      但目前2.5已經比上一版本好多了,
      只能期待Xamarin慢慢改進...

      刪除