2016년 10월 10일 월요일

Xamarin.Forms XAML Basics Part 4, Data Binding Basics

Xamarin.Forms에서의 XAML 사용이 궁금해서 아래 링크를 보며 대충 필요한 것만 정리함.
https://developer.xamarin.com/guides/xamarin-forms/xaml/

Part 4. Data Binding Basics


Data binding은 source, target 두 객체의 property들을 연결한다. 

이를 위해 두단계의 과정이 필요하다.
먼저 target의 BidningContext property가 source로 지정되어야 하고
SetBinding method을 target에서 호출 하여
source의 property와 binding 해야 한다. (뭔소리임?)

즉 Target property는 bindable property 여야 하는데 이를 위해 target은 BindableObject를 상속 받아야 한다.

XAML에서는 Binding markup extension이 SetBinding call과 Binding class를 대신한다는 것을 제외 하고 동일핟.
다만 BindingContext를 지정하는 방법이 있는게 아니라 code-behind file(XAML의 cs파일)에서 지정 하는 방법이나 StaticResource, x:Static markup extension을 사용하는 방법, BindingContext property-element tag를 사용하는 방법을 사용할 수 있다.


View-to-View Bindings
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_binding_basics/#View-to-View_Bindings

동일 page 내에서 view들을 binding하고자 할 경우 target object에 BindingContext를 x:Reference markup extension을 사용하여 지정할 수 있다.

아래는 Slider와 두개의 Label가 binding 되어 있는 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"
             x:Class="XamlSamples.SliderBindingsPage"
             Title="Slider Bindings Page">

  <StackLayout>
    <Label Text="ROTATION"
           BindingContext="{x:Reference Name=slider}"
           Rotation="{Binding Path=Value}"
           FontAttributes="Bold"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="CenterAndExpand" />

    <Slider x:Name="slider"
            Maximum="360"
            VerticalOptions="CenterAndExpand" />

    <Label BindingContext="{x:Reference slider}"
          Text="{Binding Value,
                          StringFormat='The angle is {0:F0} degrees'}"
          FontAttributes="Bold"
          FontSize="Large"
          HorizontalOptions="Center"
          VerticalOptions="CenterAndExpand" />
  </StackLayout>
</ContentPage>

BindingContext에서 x:Refernce가 slider로만 되어 있는데 이는 대상 object의 x:Name이다.

BindingContext="{x:Reference Name=slider}"
BindingContext="{x:Reference slider}"

Binding markup extension은 BindingBase, Binding과 같은 여러 property를 가지고 있다.
Binding 의 ContentProperty로 지정된 proeprty는 Path이므로 Binding markup extension에서 처음 item일 경우 "Path=" 을 명시적으로 지정하지 않아도 된다.

Rotation="{Binding Path=Value}"
Text="{Binding Value,
               StringFormat='The angle is {0:F0} degrees'}"

함께 StringFormat이 사용되어 있는데 이는 Xamarin.Forms에서 implicit type conversions을 수행하지 않기 때문에 non-string인 Value 값을 string으로 변환하기 위해 사용되었다.

함께 알고 있어야 할 것은 StringFormat은 static String.Format method를 사용하는데 내부적으로 {}을 사용하고 있어 XAML parser에서의 혼동과 같은 문제를 야기할 수 있으므로 string formatting시 single quotation marks('')을 사용하여 표시해야 한다.

Text="{Binding Value,
               StringFormat='The angle is {0:F0} degrees'}"

Backwards Bindings
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_binding_basics/#Backwards_Bindings

하나의 view는 여러 property들에 대해서 data binding이 가능하다. 하지만 각 view는 하나의 BindingContext을 가지므로 multiple data binding 시 동일 object에 대해서 여러 reference를 가져야 한다.

이 같은 제약을 해결하기 위해서 종종 OneWayToSource나 TwoWay mode를 사용하여 view-to-view binding을 사용하기도 한다.

4개의 Slider를 사용하여 Label의 Scale, Rotate, RotateX, RotateY를 조절하고자 할 경우 Label의 BindingContext가 하나이므로 Label에서 각 Slider로 binding하는 것은 어렵다. 이러한 점을 회피하고자 binding을 반대로 Slider들의 BindingContext를 Label로 지정하고 Slider의 Value property를 binding하는 방법을 사용한다.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SliderTransformsPage"
             Title="Slider Transforms Page">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <StackLayout Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">

      <!-- Scaled and rotated Label -->
      <Label x:Name="label"
             Text="TEXT"
             HorizontalOptions="Center"
             VerticalOptions="CenterAndExpand" />

    </StackLayout>

    <!-- Slider and identifying Label for Scale -->
    <Slider x:Name="scaleSlider"
            BindingContext="{x:Reference label}"
            Grid.Row="1" Grid.Column="1"
            Maximum="10"
            Value="{Binding Scale, Mode=TwoWay}" />

    <Label BindingContext="{x:Reference scaleSlider}"
           Text="{Binding Value, StringFormat='Scale = {0:F1}'}"
           Grid.Row="1" Grid.Column="0"
           VerticalTextAlignment="Center" />

    <!-- Slider and identifying Label for Rotation -->
    <Slider x:Name="rotationSlider"
            BindingContext="{x:Reference label}"
            Grid.Row="2" Grid.Column="1"
            Maximum="360"
            Value="{Binding Rotation, Mode=OneWayToSource}" />

    <Label BindingContext="{x:Reference rotationSlider}"
           Text="{Binding Value, StringFormat='Rotation = {0:F0}'}"
           Grid.Row="2" Grid.Column="0"
           VerticalTextAlignment="Center" />

    <!-- Slider and identifying Label for RotationX -->
    <Slider x:Name="rotationXSlider"
            BindingContext="{x:Reference label}"
            Grid.Row="3" Grid.Column="1"
            Maximum="360"
            Value="{Binding RotationX, Mode=OneWayToSource}" />

    <Label BindingContext="{x:Reference rotationXSlider}"
           Text="{Binding Value, StringFormat='RotationX = {0:F0}'}"
           Grid.Row="3" Grid.Column="0"
           VerticalTextAlignment="Center" />

    <!-- Slider and identifying Label for RotationY -->
    <Slider x:Name="rotationYSlider"
            BindingContext="{x:Reference label}"
            Grid.Row="4" Grid.Column="1"
            Maximum="360"
            Value="{Binding RotationY, Mode=OneWayToSource}" />

    <Label BindingContext="{x:Reference rotationYSlider}"
           Text="{Binding Value, StringFormat='RotationY = {0:F0}'}"
           Grid.Row="4" Grid.Column="0"
           VerticalTextAlignment="Center" />
  </Grid>
</ContentPage>

초기 예제는 Label들에서 slider의 Value property로 binding 하였으나
Label  --Value--   slider    --Value--   Label

아래와 같이
label의 Scale, Rotation, RotationX, RotationY를 각 slider에서 binding하고
각 slider들의 Value property를 각 Label들에서 binding하는 형태이다.

label <--  Scale  -->   scaleSlider       --Value--   Label
      <--Rotation--     rotationSlider    --Value--   Label
      <--RotationX--   rotationXSlider   --Value--   Label
      <--RotationY--   rotationYSlider   --Value--   Label

Scale property는 Twoway로 binding되는데 이는 Scale property의 초기값이 1인 관계로 이를 scaleSlider에도 반영하기 위해서 이다. OneWayToSource로 binding하게 되면 Scale property는 Slider의 기본 값인 0으로 설정되어 Label이 보이지 않게 된다.

또한 명세에서 보면 column 0에 Label을 column 1에 Slider를 배치하였지만 작성 순서는 Slider를 먼저 그다음에 Label을 배치하였음 이유는 Slider의 값을 Label에서 OneWay binding 하고 있어 Slider가 우선 정의 되어야 Label에서 값을 참조할 수 있음.


Bindings and Collections
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_binding_basics/#Bindings_and_Collections


Templated ListView를 사용할 때 XAML과 data binding feature가 상당히 유용하다.
ListView는 IEnumerable를 구현하고 있는 ItemSource property를 가지고 있어 이를 item을 표시할 때 사용한다. ListView collection은 Cell을 상속한 template를 사용함으로 써 사용자가 원하는 형태로 보여줄 수 있다. Template는 ListView의 개별 item들을 위해 clone되고 각 clone들을 설정하기 위해 data를 binding 한다.

주로 Custom Cell을 만들기 위해 ViewCell class를 사용하여 coding하는 방법이 꽤나 지저분하지만 XAML에서는 상당히 간단하다.

아래 sample project에서는 NamedColor class를 include한다. NamedColor는 Name과 FriendlyName, color를 가지고 있고 내부적으로 static read-only color 값의 목록을 포함하고 있다.

<?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:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

  <ListView ItemsSource="{x:Static local:NamedColor.All}" />

</ContentPage>

Item의 template을 지정하려면 ItemTemplate property에 ViewCell을 포함한 DataTemplate를 지정해야 한다.

<ListView ItemsSource="{x:Static local:NamedColor.All}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <ViewCell.View>
            <Label Text="{Binding FriendlyName}" />
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>

좀 더 꾸미기 위해 page의 resource dictionary를 정의해서 사용할 수 있다.

<?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:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

  <ContentPage.Resources>
    <ResourceDictionary>
      <OnPlatform x:Key="boxSize"
                  x:TypeArguments="x:Double"
                  iOS="50"
                  Android="50"
                  WinPhone="75" />

      <!-- This is only an issue on the iPhone; Android and
           WinPhone auto size the row height to the contents. -->
      <OnPlatform x:Key="rowHeight"
                  x:TypeArguments="x:Int32"
                  iOS="60"
                  Android="60"
                  WinPhone="85" />

      <local:DoubleToIntConverter x:Key="intConverter" />

    </ResourceDictionary>
  </ContentPage.Resources>

  <ListView ItemsSource="{x:Static local:NamedColorGroup.All}"
            RowHeight="{StaticResource rowHeight}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <ViewCell.View>
            <StackLayout Padding="5, 5, 0, 5"
                         Orientation="Horizontal"
                         Spacing="15">

              <BoxView WidthRequest="{StaticResource boxSize}"
                       HeightRequest="{StaticResource boxSize}"
                       Color="{Binding Color}" />

              <StackLayout Padding="5, 0, 0, 0"
                           VerticalOptions="Center">

                <Label Text="{Binding FriendlyName}"
                       FontAttributes="Bold"
                       FontSize="Medium" />

                <StackLayout Orientation="Horizontal"
                             Spacing="0">
                  <Label Text="{Binding Color.R,
                                   Converter={StaticResource intConverter},
                                   ConverterParameter=255,
                                   StringFormat='R={0:X2}'}" />
                  <Label Text="{Binding Color.G,
                                   Converter={StaticResource intConverter},
                                   ConverterParameter=255,
                                   StringFormat=', G={0:X2}'}" />
                  <Label Text="{Binding Color.B,
                                   Converter={StaticResource intConverter},
                                   ConverterParameter=255,
                                   StringFormat=', B={0:X2}'}" />
                </StackLayout>
              </StackLayout>
            </StackLayout>
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

Xamarin.Forms의 Color의 type은 0-1값을 가지는 double형이므로 int형으로 형변환이 필요하고 또한 Anroid RGB color 범위에 맞춰서 값 변환이 필요하다.

이는 외부 binding converter를 통해서 가능하다.

using System;
using System.Globalization;
using Xamarin.Forms;

namespace XamlSamples
{
    class DoubleToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,
                              object parameter, CultureInfo culture)
        {
            double multiplier;

            if (!Double.TryParse(parameter as string, out multiplier))
                multiplier = 1;

            return (int)Math.Round(multiplier * (double)value);
        }

        public object ConvertBack(object value, Type targetType,
                                  object parameter, CultureInfo culture)
        {
            double divider;

            if (!Double.TryParse(parameter as string, out divider))
                divider = 1;

            return ((double)(int)value) / divider;
        }
    }
}

ListView의 item이 동적으로 변경되는 것을 처리하기 위해서는 INotifyCollectionChanged interface를 구현하고 있는 ObservableCollection을 사용해서 CollectionChanged event handler를 통해서 처리 가능하다.
또한 property들이 변경되는 것은 INotifyPropertyChanged interface를 구현해서 PropertyChanged event handler를 통해서 처리 가능함.


댓글 없음:

댓글 쓰기