전체 페이지뷰

2017년 4월 12일 수요일

Chapter 8. Code and XAML in harmony, part 1

코드와 XAML은 언제나 쌍을 이루어 서로를 보완하며 움직입니다. XAML의 "code-behind" 파일이라고 불리지만 코드는 앱에서 보다 능동적이며 인터액티브한 부분을 다루는데 탁월합니다. 따라서 코드는 XAML에서 정의된 element를 코드 내에서 만들어진 객체만큼 잘 참조할 수 있어야 합니다. 이와 마찬가지로 XAML의 element도 코드 기반의 이벤트 핸들러에서 이벤트를 불러 일으킬 수 있어야 합니다. 이것이 이 챕터에서 다루고자 하는 내용입니다.

그러나 그 전에 XAML 파일에서 객체를 인스턴스화하는 특이한 기술을 몇가지 살펴보고자 합니다.


Passing arguments

XAML로 된 앱을 실행할 때, XAML 파일 내의 각 element는 해당하는 클래스나 구조체의 매개변수 없는 생성자를 통해 인스턴스화 됩니다. 계속해서 property와 attribute가 설정되고 객체가 초기화 됩니다. 이 과정은 합리적이라 생각됩니다. 그러나, XAML 개발자는 때로 정적 생성 메소드나 매개변수 있는 생성자를 사용해야할 때가 있습니다. 이 과정에서 대개 API 자체를 포함시킬 필요는 없지만, 대신 API와 상호 작용하는 XAML 파일이 외부 데이터 클래스를 참조할 필요가 있습니다.

2009 XAML 사양은 이런 경우에 대비해서 x:Arguments element와 x:FactoryMethod attribute를 도입했으며, Xamarin.Forms는 이를 지원합니다. 이런 기술은 일반적인 환경하에서 자주 사용되지는 않지만, 만일의 경우를 대비하여 알아두는게 좋겠습니다.

Constructors with arguments

XAML element의 생성자에 매개 변수를 넘겨주기 위해서, element는 start와 end 태그로 나뉘어져야 합니다. 그 안에 x:Arguments의 start, end 태그가 오고, 다시 그 안에 한 개 이상의 생성자 매개변수가 올수 있습니다.

그렇다면 double이나 int 같은 일반적인 타입에 대한 인수를 여러 개 지정하려면 어떻게 하면 되겠습니까? 쉼표를 찍으면 될까요?

쉼표가 아니고 각 인수는 start, end 태그로 구분되어야 합니다. 다행히도 XAML 2009 사양에서는 기본 형식에 대한 XML element가 정의되어 있습니다. 이렇게 태그를 사용하여 element 타입을 정하고, OnPlatform에서 제네릭 형식을 지정하고, 생성자 인수를 구분하는 일을 할 수 있습니다. 다음은 Xamarin.Forms에서 지원하는 전체 기본 타입입니다. C# 형식 이름이 아닌 .NET 형식 이름을 따릅니다.


  • x:Object 
  • x:Boolean 
  • x:Byte 
  • x:Int16 
  • x:Int32 
  • x:Int64 
  • x:Single 
  • x:Double 
  • x:Decimal 
  • x:Char 
  • x:String 
  • x:TimeSpan 
  • x:Array 
  • x:DateTime (XAML 2009 specification이 아닌 Xamarin.Forms에서 지원)
이 모든 기능을 사용하기는 어려울 테지만 일부 기능은 유용하게 사용할 수 있을 것입니다.

ParameteredConstructorDemo 샘플은 x:Arguments의 사용법을 보여주는데, Color 구조체의 생성자 세 종류를 사용하는 x:Double 태그로 구분됩니다. 3 개의 파라미터를 가지는 생성자에는, 0으로부터 1까지의 범위의 빨강, 초록, 파랑의 값이 필요하고, 4 개의 파라미터를 가지는 생성자에서는 4번째의 파라미터로서 알파 채널을 추가하며(0.5로 설정), 1개의 파라미터를 가지는 생성자는 0(검정색)에서 1(흰색)까지 그레이 쉐이드 값이 필요합니다.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ParameteredConstructorDemo"
             x:Class="ParameteredConstructorDemo.MainPage">
    <StackLayout>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color>
                    <x:Arguments>
                        <x:Double>1</x:Double>
                        <x:Double>0</x:Double>
                        <x:Double>0</x:Double>
                    </x:Arguments>
                </Color>
            </BoxView.Color>            
        </BoxView>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color>
                    <x:Arguments>
                        <x:Double>0</x:Double>
                        <x:Double>0</x:Double>
                        <x:Double>1</x:Double>
                        <x:Double>0.5</x:Double>
                    </x:Arguments>
                </Color>
            </BoxView.Color>
        </BoxView>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color>
                    <x:Arguments>
                        <x:Double>0.5</x:Double>                        
                    </x:Arguments>
                </Color>
            </BoxView.Color>
        </BoxView>
    </StackLayout>
</ContentPage>
cs

x:Arguments 태그 내의 element 수와 이 element의 타입은 클래스 또는 구조체의 생성자 중 하나와 일치해야 합니다. 결과는 다음과 같습니다.


파란색 BoxView는 50%의 투명도를 지녀 배경색을 비춰보이게 하므로, 밝은 배경에서와 어두운 배경에서의 색이 다릅니다.

Can I call methods from XAML?

한 때, 이 질문에 대한 대답은 "장난하나"였지만 지금은 "예"라고 말해도 좋습니다. 그러나, 너무 흥분하진 마십시오. XAML에서 호출 할 수있는 유일한 메소드는, 그 메소드를 정의하는 클래스 (또는 구조체)와 같은 형식의 개체 또는 값을 반환하는 메소드입니다. 이 메소드는 public 또는 static이어야 합니다. 이것들을 creation method 또는 factory method라고 부르기도 합니다. x:FactoryMethod attribute를 사용하여 메소드 이름을 지정하고 x:Arguments element를 사용하여 인수를 지정해서  메소드를 호출하고 XAML의 element를 인스턴스화 할 수 있습니다.

Color 구조체에는 7개의 static 메소드가 있고, 반환 형식이 Color이므로 이 요건에 들어 맞습니다. 아래 XAML 파일의 그 중 세 개를 사용합니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:FactoryMethodDemo"
             x:Class="FactoryMethodDemo.MainPage">
    <StackLayout>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color x:FactoryMethod="FromRgb">
                    <x:Arguments>
                        <x:Int32>255</x:Int32>
                        <x:Int32>0</x:Int32>
                        <x:Int32>0</x:Int32>
                    </x:Arguments>
                </Color>
            </BoxView.Color>
        </BoxView>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color x:FactoryMethod="FromRgb">
                    <x:Arguments>
                        <x:Double>0</x:Double>
                        <x:Double>1.0</x:Double>
                        <x:Double>0</x:Double>
                    </x:Arguments>
                </Color>
            </BoxView.Color>
        </BoxView>
        <BoxView WidthRequest="100"
                 HeightRequest="100"
                 HorizontalOptions="Center"
                 VerticalOptions="CenterAndExpand">
            <BoxView.Color>
                <Color x:FactoryMethod="FromHsla">
                    <x:Arguments>
                        <x:Double>0.67</x:Double>
                        <x:Double>1.0</x:Double>
                        <x:Double>0.5</x:Double>
                        <x:Double>1.0</x:Double>
                    </x:Arguments>
                </Color>
            </BoxView.Color>
        </BoxView>
    </StackLayout>
</ContentPage>
cs

첫 두개의 static 메소드명은  Color.FromRgb로 같습니다만 하나는 0~255의 Int32, 다른 하나는 0~1의 Double을 인자로 갖습니다. 마지막은 Color.FormHsla로 hue, saturation, luminosity, alpha를 인자로 가집니다. 흥미로운 점은, 이것이 XAML파일에서 Xamarin.Forms API를 사용하여 HSL 값으로부터 Color 값을 정의하는 유일한 방법이라는 점입니다. 결과는 다음과 같습니다.



The x:Name attribute

대부분의 실제 어플리케이션에서 code-behind 파일은 XAML 파일에 정의된 element를 참조해야 합니다. 이미 지난 장의 CodePlusXaml 프로그램에서 한가지 방법을 보셨습니다. 만약에 code-behind 파일이 XAML 파일에 정의된 시각적 트리에 대한 정보를 알고 있다면, root element(page 자체)로부터 시작하여 트리 구조 내에 element들을 위치시킬 수 있을 겁니다. 이 과정을 "walking the tree"라고 부르며, element를 페이지에 위치시키는데 유용합니다.

일반적으로, XAML 파일의 element명을 변수 이름과 유사하게 짓는 것이 더 좋은 방법입니다. 이를 위해서 Name이라는 XAML 고유의 attribute를 사용합니다. XAML attribute를 사용할때 접두사 x를 붙이는 것이 룰처럼 되어 있어서 Name attribute도 x:Name이라고 부릅니다.

XamlClock 프로젝트로 x:Name을 소개하겠습니다. 이 XamlClockPage.xaml은 timeLabeldateLabel의 두가지 Label 컨트롤을 가지고 있습니다.

x:Name을 만드는 규칙은 C# 변수명 만드는 것과 같습니다. 이름은 문자 또는 밑줄로 시작해야하며 문자, 밑줄 및 숫자 만 사용할 수 있습니다. 5 장의 시계 프로그램과 마찬가지로 XamlClockDevice.StartTimer를 사용하여 시간과 날짜를 업데이트하는 주기적인 이벤트를 발생시킵니다. 아래에 XamlClockPage의 code-behind 파일이 있습니다.

namespace XamlClock
{
    public partial class XamlClockPage
    {
        public XamlClockPage()
        {
            InitializeComponent();
            Device.StartTimer(TimeSpan.FromSeconds(1), OnTimerTick);
        }
        bool OnTimerTick()
        {
            DateTime dt = DateTime.Now;
            timeLabel.Text = dt.ToString("T");
            dateLabel.Text = dt.ToString("D");
            return true;
        }
    }
}
cs

1초마다 타이머 메소드가 호출되고, 타이머를 계속하기 위해 true를 반환합니다. 만약 false를 반환하면 타이머가 멈추게 되고, 재시작 하려면 Device.StartTimer를 다시 호출해야 합니다.

콜백 메소드는 정상적인 변수 인 것처럼 timeLabel dateLabel을 참조하고 각각의 Text 프로퍼티를 설정합니다.

code-behind 파일이 x:Name으로 식별되는 element를 참조할 수 있는 방법은 무엇입니까? 마술인가요? 그건 물론 아니죠. 프로젝트를 빌드 시 XAML 파서가 생성하는 XamlClockPage.xaml.g.cs 파일을 살펴보면 메커니즘을 알수 있습니다.
//------------------------------------------------------------------------------
// <auto-generated>
//     이 코드는 도구를 사용하여 생성되었습니다.
//     런타임 버전:4.0.30319.42000
//
//     파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
//     이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
namespace XamlClock
{
    using System;
    using Xamarin.Forms;
    using Xamarin.Forms.Xaml;
    public partial class XamlClockPage : global::Xamarin.Forms.ContentPage {
        [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG"
            "0.0.0.0")]
        private global::Xamarin.Forms.Label timeLabel;
        [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG"
            "0.0.0.0")]
        private global::Xamarin.Forms.Label dateLabel;
        [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG"
            "0.0.0.0")]
        private void InitializeComponent()
        {
            this.LoadFromXaml(typeof(XamlClockPage));
            timeLabel = this.FindByName<global::Xamarin.Forms.Label>("timeLabel");
            dateLabel = this.FindByName<global::Xamarin.Forms.Label>("dateLabel");
        }
    }
}
cs

attribute들과 풀네임으로 쓰여진 타입들 때문에 조금 알아보기가 어려울 수 있겠지만, 빌드 타임에 파서가 XAML 파일을 쿼리하고 나면 x:Name attribute는 자동 생성코드의 private 필도로 변환됩니다. 이렇게 해서 code-behind 파일의 코드가 이 이름을 마치 정상 필드인 것처럼 참조할 수 있게 됩니다. 그러나 필드의 초기값은 null입니다. 런타임에 InitializeComponent가 호출되었을 때에야 NameScopeExtensions 클래스에 정의 된 FindByName 메서드를 통해 두 필드가 설정됩니다. 만약 그보다 먼저 필드를 참조하려고 한다면 null 값을 가지게 될 것입니다.

아주 당연하지만 정확하게 표현하지는 않는 x:Name의 규칙이 또 있는데, 그것은 code-behind 파일에 정의된 필드, 프로퍼티의 이름을 똑같이 써서는 안 된다는 것입니다.

이것들은 private 필드이므로 code-behind 파일에서만 접근 가능하며, 다른 클래스에선 접근 불가합니다. 만약 ContentPage로부터 파생된 클래스가 다른 클래스에 public 필드나 프로퍼티를 노출해야할 필요가 있다면 사용자가 직접 클래스를 정의해야 할 것입니다.

x:Name 값은 XAML 페이지 내에서 고유해야 하는데,XAML 파일에서 OnPlatform을 사용하는 경우 이것이 가끔 문제가 되는 경우가 있습니다. 예를 들어 다음은 OnPlatformiOS, Android, WinPhone 프로퍼티를 property-element로 나타내고, 세 가지 Label  뷰 중 하나를 선택하도록 하는 XAML 파일입니다.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="PlatformSpecificLabels.PlatformSpecificLabelsPage">
    
    <OnPlatform x:TypeArguments="View">
        <OnPlatform.iOS>
            <Label Text="This is an iOS device" 
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.iOS>
        
        <OnPlatform.Android>
            <Label Text="This is an Android device" 
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.Android>
        
        <OnPlatform.WinPhone>
            <Label Text="This is an Windows device"
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.WinPhone>
    </OnPlatform>
</ContentPage>
cs

OnPlatformx:TypeArguments attribute는 대상 프로퍼티의 형식과 정확히 일치해야 합니다. 이 OnPlatform element는 ContentPageContent property에 암묵적으로 설정되어 있으며 이 Content 프로퍼티는 View 타입이므로, OnPlatformx:TypeArguments 특성에서 View를 지정해줘야 합니다. 그러나 OnPlatform의 property는 해당 유형에서 파생되는 모든 클래스에 설정될 수 있습니다. iOS, Android, WinPhone 프로퍼티로 설정된 객체는 모두 View에서 파생되었지만 실제로는 다른 유형일 수 있습니다.

위의 프로그램은 작동하긴 하지만 엄밀하게 말해서 적절하지는 않습니다. 세 개의 Label이 모두 초기화, 인스턴스화 되지만 그 중 하나만이 Content property로 설정됩니다. 이런 방식의 문제점은 code-behind 파일에서 Label을 참조해서 각각에 똑같은 이름을 부여해야 하는 아래와 같은 경우에 발생합니다.

아래 파일은 작동하지 않습니다!

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="PlatformSpecificLabels.PlatformSpecificLabelsPage">
    
    <OnPlatform x:TypeArguments="View">
        <OnPlatform.iOS>
            <Label x:Name="deviceLabel" 
                   Text="This is an iOS device" 
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.iOS>
        
        <OnPlatform.Android>
            <Label x:Name="deviceLabel" 
                   Text="This is an Android device" 
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.Android>
        
        <OnPlatform.WinPhone>
            <Label x:Name="deviceLabel" 
                   Text="This is a Windows device" 
                   HorizontalOptions="Center" 
                   VerticalOptions="Center" />
        </OnPlatform.WinPhone>
        
    </OnPlatform>
</ContentPage>
cs

다수의 element가 같은 이름을 가질 수 없어서 이 파일은 작동하지 않습니다.

다른 이름들을 지어주고 code-behind 파일에서 Device.OnPlatform을 사용하여 이름들을 다룰 수도 있습니다만, 가급적이면 플랫폼 독립적인 마크업을 최소화하는 편이 좋습니다. 이 예에서는 Label의 프로퍼티가 Text만 빼고 모두 일치하므로, Text만 플랫폼 독립적으로 만드는 것이 좋습니다. 다음은 이 장의 샘플 코드에 포함 된 PlatformSpecificLabels 프로그램입니다. 이 프로그램에는 하나의 Label만 있고 Text를 제외하면 모두 플랫폼과 무관합니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="PlatformSpecificLabels.PlatformSpecificLabelsPage">
    
    <Label x:Name="deviceLabel" 
           HorizontalOptions="Center" 
           VerticalOptions="Center">
        <Label.Text>
            <OnPlatform x:TypeArguments="x:String" 
                        iOS="This is an iOS device" 
                        Android="This is an Android device" 
                        WinPhone="This is a Windows device" />
        </Label.Text>
    </Label>
</ContentPage>
cs


Text 프로퍼티는 content 프로퍼티이며, 생략 가능하다고 했으므로 Label.Text 태그는 필요치 않습니다. 따라서 다음도 잘 작동됩니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="PlatformSpecificLabels.PlatformSpecificLabelsPage">
    
    <Label x:Name="deviceLabel" 
           HorizontalOptions="Center" 
           VerticalOptions="Center">
        
        <OnPlatform x:TypeArguments="x:String" 
                    iOS="This is an iOS device" 
                    Android="This is an Android device" 
                    WinPhone="This is a Windows device" />
    </Label>
</ContentPage>
cs


댓글 없음:

댓글 쓰기