2017/8/23

Xamarin.Forms 教學系列文(二十三.貳)XAML 觸發程式 - DataTrigger & MultiTrigger



學習目標
  • DataTrigger - Binding 資料觸發
  • MultiTrigger - 多條件式 Trigger
  • MltiTrigger 利用 隱藏Switch 做中介轉換 

上一小節已經學會,針對 特定屬性 或 特定事件 更改時去觸發 Trigger 。

DataTrigger 的寫法其實跟 Trigger 很像,只是差在將 Property 改成 Binding

DataTrigger 的功能為
偵測到 databiding 物件 的值是否更改,再決定觸發 Trigger 動作。

而 databinding 可以搭配 MVVM 或是其他 Visual Element 物件。

拿 19 章的程式碼來修改,加入 Trigger 更改字顏色的功能,

當資料是男生時,將字的顏色改成藍色。
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
                  xmlns:school="clr-namespace:SchoolOfFineArt;assembly=SchoolOfFineArt" 
                  x:Class="GenderColors.GenderColorsPage">

    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" />
    </ContentPage.Padding>

    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>

    <StackLayout BindingContext="{Binding StudentBody}">
        <Label Text="{Binding School}" FontSize="Large" 
               FontAttributes="Bold" 
               HorizontalTextAlignment="Center" />

        <ListView ItemsSource="{Binding Students}" 
                  VerticalOptions="FillAndExpand">
            <ListView.RowHeight>
                <OnPlatform x:TypeArguments="x:Int32" iOS="70" Android="70" WinPhone="100" />
            </ListView.RowHeight>

            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Grid Padding="0, 5">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="80" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>

                            <Image Grid.Column="0" Source="{Binding PhotoFilename}" 
                                   VerticalOptions="Center" />

                            <StackLayout Grid.Column="1" VerticalOptions="Center">
                                <Label Text="{Binding FullName}"
                                       FontSize="22" 
                                       TextColor="Pink">

                                    <!--重點就底下這七行,當 Sex 是 Male 時,字的顏色改為藍色-->
                                    <Label.Triggers>
                                        <DataTrigger TargetType="Label" 
                                                     Binding="{Binding Sex}" 
                                                     Value="Male">
                                            <Setter Property="TextColor" Value="#8080FF" />
                                        </DataTrigger>
                                    </Label.Triggers>

                                </Label>
                                <Label Text="{Binding GradePointAverage, StringFormat='G.P.A. = {0:F2}'}" 
                                       FontSize="16" />
                            </StackLayout>

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

執行結果:

痾...就是這麼簡單...

我們也可以用 DataTrigger 去偵測另一個 Visual Element 物件屬性是否符合我們的條件,再做改變。

底下程式實作當 Entry 沒輸入值時,Button 是不能按的效果。

*要記得將 Entry 的 Text 值設為空字串,不然初始預設為 null,Text.Length 會失效。
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="ButtonEnabler.ButtonEnablerPage" 
             Padding="20, 50">

    <StackLayout Spacing="20">
        <Entry x:Name="entry" 
               Text=""
               Keyboard="Url" 
               Placeholder="enter filename" />

        <Button Text="Save" 
                FontSize="Large" 
                HorizontalOptions="Center">

            <!--設定 Button 的 Trigger-->
            <Button.Triggers>
                <DataTrigger TargetType="Button" 
                             Binding="{Binding Source={x:Reference entry}, Path=Text.Length}" 
                             Value="0">

                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Button.Triggers>
        </Button>
    </StackLayout>
</ContentPage>

執行結果:

所以看起來~ 我們能用 Trigger 達到一些簡易的必填驗證!


多條件 Trigger - MultiTrigger

不論 Trigger 或是 DataTrigger,會執行都是因為設定的條件為真而去觸發。

身為一個程式設計師,你會思考是否能同時加入多個條件,即使真的能多個條件,也要決定這些條件是 AND OR 的邏輯組合。

這裡介紹 Trigger 家族的最後一個,MultiTrigger,定義了兩個屬性:
  • Conditions of type IList<Condition>
  • Setters of type IList<Setter>

Condition 是一個抽象類別,底下有兩個子類別:
  • PropertyCondition
  • BindingCondition

你可以將不同的 PorpertyCondition BindingCondition 物件混在一起使用,當所有條件成真時,就會執行 Setter 內所有的更改。

來看範例:
畫面上有四個 Switch 和一個藍色的 BoxView,

當所有 Switch 打開後,BoxView 變成紅色

要達成這件事,只需要寫 XAML 就好了喔~ 超神奇~

在 MultiTrigger 內有四個 Condition ,分別綁定四個 Switch。
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="AndConditions.AndConditionsPage">

    <StackLayout>
        <Grid VerticalOptions="CenterAndExpand">
            <Switch x:Name="switch1" Grid.Column="0" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch2" Grid.Column="1" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch3" Grid.Column="2" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch4" Grid.Column="3" 
                    HorizontalOptions="Center" />
        </Grid>

        <BoxView WidthRequest="100" 
                 HeightRequest="100" 
                 VerticalOptions="CenterAndExpand" 
                 HorizontalOptions="Center" 
                 Color="Blue">

            <BoxView.Triggers>
                <MultiTrigger TargetType="BoxView">
                    <!--綁定四個 Switch,當四個都 Toogled,BoxView 才變成紅色-->
                    <MultiTrigger.Conditions>
                        <BindingCondition Binding="{Binding Source={x:Reference switch1}, Path=IsToggled}" 
                                          Value="True" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch2}, Path=IsToggled}" 
                                          Value="True" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch3}, Path=IsToggled}"
                                          Value="True" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch4}, Path=IsToggled}" 
                                          Value="True" />
                    </MultiTrigger.Conditions>

                    <Setter Property="Color"  Value="Red" />
                </MultiTrigger>
            </BoxView.Triggers>
        </BoxView>
    </StackLayout>
</ContentPage>

看完範例,大家應該會發現,Condition 為 AND 的邏輯。

那如果我要 OR 呢 ???
有一個 Switch 打開 BoxView 就變紅色呢 ?

MultiTrigger 並沒有提供屬性讓妳設定是 AND 或 OR,目前只能自己用邏輯的方式來解決...

先了解一下底下這邏輯,
𝐴 | 𝐵 == !(!𝐴 & !𝐵)
依照這個邏輯我們來改寫範例:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="AndConditions.AndConditionsPage">

    <StackLayout>
        <Grid VerticalOptions="CenterAndExpand">
            <Switch x:Name="switch1" Grid.Column="0" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch2" Grid.Column="1" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch3" Grid.Column="2" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch4" Grid.Column="3" 
                    HorizontalOptions="Center" />
        </Grid>

        <!--先將初始顏色設為 紅-->
        <BoxView WidthRequest="100" 
                 HeightRequest="100" 
                 VerticalOptions="CenterAndExpand"
                 HorizontalOptions="Center" 
                 Color="Red">

            <BoxView.Triggers>
                <MultiTrigger TargetType="BoxView">
                    <MultiTrigger.Conditions>
                        <!--將判斷值都改設為 False-->
                        <BindingCondition Binding="{Binding Source={x:Reference switch1}, Path=IsToggled}" 
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch2}, Path=IsToggled}" 
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch3}, Path=IsToggled}"
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch4}, Path=IsToggled}" 
                                          Value="False" />
                    </MultiTrigger.Conditions>
                    
                    <Setter Property="Color" Value="Blue" />
                </MultiTrigger>
            </BoxView.Triggers>
        </BoxView>

    </StackLayout>
</ContentPage>



最後提一個常見狀況,假設你有兩個 Entry,當兩個 Entry 都有資料時,Button 才能按。

但由於 Value 條件不能設定為 > 0 …

所以我們反過來想,當兩個 Entry 都沒有字時,Button 就不能按,CODE 如下:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="AndConditions.AndConditionsPage">

    <StackLayout>
        <Grid VerticalOptions="CenterAndExpand">
            <Switch x:Name="switch1" Grid.Column="0" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch2" Grid.Column="1" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch3" Grid.Column="2" 
                    HorizontalOptions="Center" />

            <Switch x:Name="switch4" Grid.Column="3" 
                    HorizontalOptions="Center" />
        </Grid>

        <!--先將初始顏色設為 紅-->
        <BoxView WidthRequest="100" 
                 HeightRequest="100" 
                 VerticalOptions="CenterAndExpand"
                 HorizontalOptions="Center" 
                 Color="Red">

            <BoxView.Triggers>
                <MultiTrigger TargetType="BoxView">
                    <MultiTrigger.Conditions>
                        <!--將判斷值都改設為 False-->
                        <BindingCondition Binding="{Binding Source={x:Reference switch1}, Path=IsToggled}" 
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch2}, Path=IsToggled}" 
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch3}, Path=IsToggled}"
                                          Value="False" />

                        <BindingCondition Binding="{Binding Source={x:Reference switch4}, Path=IsToggled}" 
                                          Value="False" />
                    </MultiTrigger.Conditions>
                    
                    <Setter Property="Color" Value="Blue" />
                </MultiTrigger>
            </BoxView.Triggers>
        </BoxView>

    </StackLayout>
</ContentPage>


但其實這樣很不好思考… 每件邏輯都要自己反過來…

書上提供一個神奇的方法…
在每個 Entry 底下設置一個隱藏的 Switch,並先和 Entry 綁定,
Swtich 的 DataTrigger 判斷 Text.Length 是否有值,並更改 IsToogled 屬性

接著直接將 Condition 和 Switch 做綁定,
雖然中間多加了一層 Switch ,但讓 Condition 的思考更為直觀,Condition 只要面對處理過後的 Switch 資料就好。
<StackLayout>
    <Entry x:Name="entry1" Text="" />
    <!--隱藏 Switch-->
    <Switch x:Name="switch1" IsVisible="False">
        <Switch.Triggers>
            <!--若 Entry 無值,則 IsToggled 為 True-->
            <DataTrigger TargetType="Switch" Binding="{Binding Source={x:Reference entry1}, Path=Text.Length}" 
                         Value="0">
                <Setter Property="IsToggled" Value="True" />
            </DataTrigger>
        </Switch.Triggers>
    </Switch>

    <Entry x:Name="entry2" Text="" />
    <!--隱藏 Switch2-->
    <Switch x:Name="switch2" IsVisible="False">
        <Switch.Triggers>
            <DataTrigger TargetType="Switch" Binding="{Binding Source={x:Reference entry2}, Path=Text.Length}"
                         Value="0">
                <Setter Property="IsToggled" Value="True" />
            </DataTrigger>
        </Switch.Triggers>
    </Switch>

    <Button Text="Send" IsEnabled="False">
        <Button.Triggers>
            <MultiTrigger TargetType="Button">
                <MultiTrigger.Conditions>
                    <!--若 Switch 的 IsToogle 都為 False 時(表示 Entry 都有值),按鈕就能按下-->
                    <BindingCondition Binding="{Binding Source={x:Reference switch1}, Path=IsToggled}" 
                                      Value="False" />

                    <BindingCondition Binding="{Binding Source={x:Reference switch2}, Path=IsToggled}" 
                                      Value="False" />

                </MultiTrigger.Conditions>
                <Setter Property="IsEnabled" Value="True" />
            </MultiTrigger>
        </Button.Triggers>
    </Button>
</StackLayout>


Trigger 最大的好處跟 MVVM 一樣,就是將一些要寫在 C# 的繁瑣事件處理,搬移到 XAML 上,並維持著重複使用的特性。

舉例來說,
上一小節一開始提到的,當 Entry  IsFocused = True 時將物件放大,反之則縮小。

若沒有 Trigger 的幫忙,我們就必須寫一個 Focused 事件,去控制 Entry 的放大,並再寫一個 UnFocused 事件去處理 Entry 的縮小...

雖然 Trigger 很方便,
但有些物件如 Frame 或是 Image,是沒有事件可以觸發 Trigger 的,他們只有 Tapped 事件,而 Tapped 事件並不在 Triggers Collection 內…

若要做到同樣的效果,就要靠下一小節的 Behavior 了。




作者外出取材


沒有留言:

張貼留言