전체 페이지뷰

2017년 5월 26일 금요일

Chapter 11. The bindable infrastructure, part 1

C #의 가장 기본적인 언어 구문 중 하나는 프로퍼티로 알려진 클래스 멤버입니다. C #을 처음 접했을 때 우리는 모두 프로퍼티를 정의하는 법을 배우곤 합니다. 이 프로퍼티는 때로 private 필드에 의해 뒷받침되며 private 필드를 참조하고 새 값으로 무언가를 수행하는 set, get 접근자를 가집니다.


public class MyClass 
    … 
    double quality; 
    public double Quality 
    { 
        set 
        { 
            quality = value; 
            // Do something with the new value 
        } 
        get 
        { 
            return quality; 
        }
    } 
   … 
}
cs

프로퍼티는 때로 smart field라고도 부릅니다. 문법적으로 프로퍼티에 액세스하는 코드는 필드에 액세스하는 코드와 유사합니다. 그러나 프로퍼티는 액세스할 때 자체 코드를 실행할 수 있습니다.

프로퍼티는 또한 메소드와도 유사합니다. 실제로 C # 코드는 set_Qualityget_Quality라는 한 쌍의 메서드를 가지는 Quality와 같은 프로퍼티를 구현하는 intermediate language로 컴파일됩니다. 그러나 프로퍼티와 set,get 메소드 간의 기능적 유사점에도 불구하고, 코드말고 마크업의 관점에서 볼 때 프로퍼티 구문이 훨씬 더 적합하다는 것을 알 수 있습니다. 프로퍼티가 들어가지 않은 API를 기반으로 XAML을 작성하는 것은 상상하기 어려운 일입니다.

따라서 Xamarin.Forms가 C# 프로퍼티를 기반으로 하여 향상시킨 프로퍼티 정의를 구현한다는 사실을 놀랍기까지 합니다. Microsoft의 XAML 기반 플랫폼에 대한 경험이 있으면 그리 놀랍지 않겠지만 말입니다.

위에 표시된 속성 정의는 .NET 공용 언어 런타임(common language runtime)에서 지원되므로 CLR 프로퍼티로 알려져 있습니다. Xamarin.Forms의 향상된 프로퍼티 정의는 CLR 프로퍼티를 기반으로 하며, BindableProperty 클래스로 캡슐화되고 BindableObject 클래스에 의해 지원되므로 bindable 프로퍼티라고 부릅니다.


The Xamarin.Forms class hierarchy


이 중요한 BindableObject 클래스의 세부 사항을 살펴보기 전에, BindableObject가 어떻게 클래스 계층 구조를 구성하여 Xamarin.Forms 전체 구조에 적용되는지부터 알아 보겠습니다.

Xamarin.Forms와 같은 객체 지향 프로그래밍 프레임워크에서 클래스 계층 구조를 보면 환경의 중요한 내부 구조를 알 수 있습니다. 클래스 계층 구조로부터 다양한 클래스가 서로 관련되는 방법과 그 클래스들이 공유하는 프로퍼티, 메소드, 이벤트들을 알 수 있게 되고 마찬가지로 bindable 프로퍼티가 지원되는 방식도 알 수 있습니다.

온라인 문서를 공부하고 어떤 클래스에서 어떤 클래스가 파생되는가를 기록하여, 힘들지만 이런 클래스 계층 구조를 구성해 볼 수 있습니다. 그게 싫다면 Xamarin.Forms 프로그램을 작성하여 당신 대신해서 전화기의 클래스 계층 구조를 출력해 볼 수도 있습니다. 그런 프로그램은 .NET 리플렉션을 사용하여 Xamarin.Forms.CoreXamarin.Forms.Xaml 어셈블리의 모든 공용 클래스, 구조체, 열거형을 가져 와서 트리 형태로 정렬해 줍니다. ClassHierarchy라는 프로그램으로 보여 드릴 겁니다.

평상시처럼 ClassHierarchy 프로젝트에는 ContentPage에서 파생된 MainPage라는 클래스가 들어 있고, 추가로 TypeInformation, ClassAndSubclasses라는 두 개의 클래스가 포함되어 있습니다.

이 프로그램은 Xamarin.Forms.Core, Xamarin.Forms.Xaml 어셈블리와 Xamarin.Forms 클래스의 기본 클래스 역할을 하는 모든 .NET 클래스의 public 클래스(구조체, 열거형까지)에 대해 하나의 TypeInformation 인스턴스를 만드는데 단, Object는 제외됩니다. 이 .NET 클래스는 Attribute, Delegate, Enum, EventArgs, Exception, MulticastDelegate, ValueType 등을 말합니다. TypeInformation 생성자에는 타입을 식별하는 Type 객체가 필요하고, 약간의 또 다른 정보도 필요로 합니다.

using System;
using System.Reflection;
namespace ClassHierachy
{
    class TypeInformation
    {
        bool isBaseGenericType;
        Type baseGenericTypeDef;
        public TypeInformation(Type type,bool isXamarinForms)
        {
            Type = type;
            IsXamarinForms = isXamarinForms;
            TypeInfo typeInfo = type.GetTypeInfo();
            BaseType = typeInfo.BaseType;
            if (BaseType != null)
            {
                TypeInfo baseTypeInfo = BaseType.GetTypeInfo();
                isBaseGenericType = baseTypeInfo.IsGenericType;
                if (isBaseGenericType)
                {
                    baseGenericTypeDef = baseTypeInfo.GetGenericTypeDefinition();
                }
            }
        }
        public Type Type { private set; get; }
        public Type BaseType { private set; get; }
        public bool IsXamarinForms { private set; get; }
        public bool IsDerivedDirectlyFrom(Type parentType)
        {
            if(BaseType!=null && isBaseGenericType)
            {
                if (baseGenericTypeDef == parentType)
                {
                    return true;
                }                
            }
            else if (BaseType == parentType)
            {
                return true;
            }
            return false;
        }
    }
}
cs

이 클래스의 중요한 부분은 IsDerivedDirectlyFrom 메소드입니다. 이 메서드는 현재 형식의 기본 형식인 인수가 전달되면 true를 반환합니다. 제너릭 클래스가 관련되어 있을 때 결정이 복잡하며, 그 문제는 클래스의 복잡성을 설명해 줍니다.

ClassAndSubclasses 클래스는 상당히 짧습니다.

using System;
using System.Collections.Generic;
namespace ClassHierachy
{
    class ClassAndSubclasses
    {
        public ClassAndSubclasses(Type parent, bool isXamarinForms)
        {
            Type = parent;
            IsXamarinForms = isXamarinForms;
            Subclasses = new List<ClassAndSubclasses>();
        }
        public Type Type { private set; get; }
        public bool IsXamarinForms { private set; get; }
        public List<ClassAndSubclasses> Subclasses { private set; get; }
    }
}
cs

이 프로그램은 클래스 계층 구조에 표시된 모든 Type에 대해 인스턴스 하나를 생성하는데, Object를 포함하므로 TypeInformation 인스턴스 수보다 하나 많은 ClassAndSubclasses 인스턴스를 만듭니다. Object와 연관된 ClassAndSubclasses 인스턴스는 Object에서 직접 파생된 모든 클래스의 컬렉션을 가지며, 각 ClassAndSubclasses 인스턴스는 나머지 클래스 트리에 대해 파생되는 모든 클래스의 컬렉션을 포함합니다.

ClassHierarchyPage 클래스는 XAML 파일과 코드 숨김 파일로 구성되는데, XAML 파일에 Label element를 위해 스크롤 가능한 StackLayout이 들어 있습니다.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ClassHierachy"
             x:Class="ClassHierachy.MainPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="5,20,0,0"
                    Android="5,0,0,0"
                    WinPhone="5,0,0,0" />
    </ContentPage.Padding>
    <ScrollView>
        <StackLayout x:Name="stackLayout"
                     Spacing="0" />
    </ScrollView>
</ContentPage>
cs

코드 숨김 파일은 두 개의 Xamarin.Forms Assembly 객체에 대한 참조를 가져온 다음, 모든 public 클래스, 구조체 및 열거형을 classList 컬렉션에 더합니다. 그런 다음 .NET 어셈블리의 기본 클래스를 포함할 필요가 있는지를 확인하고 결과를 정렬한 다음 AddChildrenToParentAddItemToStackLayout이라는 두 재귀 메서드를 호출합니다.

using System;
using System.Collections.Generic;
using Xamarin.Forms;
using System.Reflection;
using Xamarin.Forms.Xaml;
namespace ClassHierachy
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            List<TypeInformation> classList = new List<TypeInformation>();
            // Gets types in Xamarin.Forms.Core assembly.
            GetPublicTypes(typeof(View).GetTypeInfo().Assembly, classList);
            // Get types in Xamarin.Forms.Xaml assembly.
            GetPublicTypes(typeof(Extensions).GetTypeInfo().Assembly, classList);
            // Ensure that all classes have a base type in the list.
            //  (i.e., add Attribute, ValueType, Enum, EventArgs, etc.)
            int index = 0;
            // Watch out! Loops through expanding classList!
            do
            {
                // get a child type from the list.
                TypeInformation childType = classList[index];
                if (childType.Type != typeof(Object))
                {
                    bool hasBaseType = false;
                    // Loop through the list looking for a base type
                    foreach (TypeInformation parentType in classList)
                    {
                        if (childType.IsDerivedDirectlyFrom(parentType.Type))
                        {
                            hasBaseType = true;
                        }
                    }
                    // If there is no base type, add it.
                    if (!hasBaseType && childType.BaseType != typeof(Object))
                    {
                        classList.Add(new TypeInformation(childType.BaseType, false));
                    }
                }
                index++;
            }
            while (index < classList.Count);
            // Now sort the list.
            classList.Sort((t1, t2) =>
            {
                return String.Compare(t1.Type.Name, t2.Type.Name);
            });
            // Start the display with System.Object.
            ClassAndSubclasses rootClass = new ClassAndSubclasses(typeof(Object), false);
            // Recursive method to build the hierarchy tree. 
            AddChildrenToParent(rootClass, classList);
            // Recursive method for adding items to StackLayout. 
            AddItemToStackLayout(rootClass, 0);
        }
        void GetPublicTypes(Assembly assembly, List<TypeInformation> classList)
        {
            // Loop through all types
            foreach(Type type in assembly.ExportedTypes)
            {
                TypeInfo typeInfo = type.GetTypeInfo();
                // public types only but exclude interfaces.
                if(typeInfo.IsPublic || !typeInfo.IsInterface)
                {
                    // Add type to list.
                    classList.Add(new TypeInformation(type, true));
                }
            }
        }
        void AddChildrenToParent(ClassAndSubclasses parentClass, 
            List<TypeInformation> classList)
        {
            foreach(TypeInformation typeInformation in classList)
            {
                if (typeInformation.IsDerivedDirectlyFrom(parentClass.Type))
                {
                    ClassAndSubclasses subClass = 
                        new ClassAndSubclasses(typeInformation.Type, typeInformation.IsXamarinForms);
                    parentClass.Subclasses.Add(subClass);
                    AddChildrenToParent(subClass, classList);
                }
            }
        }
        void AddItemToStackLayout(ClassAndSubclasses parentClass, int level)
        {
            // If assembly is not Xamarin.Forms, display full name.
            string name = parentClass.IsXamarinForms ?
                parentClass.Type.Name : parentClass.Type.FullName;
            TypeInfo typeInfo = parentClass.Type.GetTypeInfo();
            // If generic, display angle brackets and parameters.
            if (typeInfo.IsGenericType)
            {
                Type[] parameters = typeInfo.GenericTypeParameters;
                name = name.Substring(0, name.Length - 2);
                name += "<";
                for (int i = 0; i < parameters.Length; i++)
                {
                    name += parameters[i].Name;
                    if (i < parameters.Length - 1)
                    {
                        name += ", ";
                    }
                }
                name += ">";
            }
            // Create Label and add to StackLayout.
            Label label = new Label
            {
                Text = String.Format("{0}{1}"new string(' '4 * level), name),
                TextColor = parentClass.Type.GetTypeInfo().IsAbstract ? Color.Accent : Color.Default
            };
            stackLayout.Children.Add(label);
          
            // now display the nested types.
            foreach(ClassAndSubclasses subClass in parentClass.Subclasses)
            {
                AddItemToStackLayout(subClass, level + 1);  
            }
        }
    }
}
cs

AddChildrenToParent라는 재귀 메서드는 classList 컬렉션으로부터 연결된 ClassAndSubclasses 인스턴스의 목록을 어셈블합니다. AddItemToStackLayout 메서드는 들여 쓰기용의 공백을 가지는 Label 뷰를 만들어 StackLayout 객체에 ClassesAndSubclasses 리스트를 재귀적으로 추가합니다. 이 메서드는 Xamarin.Forms 타입은 클래스 이름만을 표시하지만 .NET 형식은 구분 가능하도록 전체 이름을 표시합니다.

전반적으로, Xamarin.Forms visual element에는 다음과 같은 일반 계층 구조가 있음을 알 수 있습니다.

System.Object 
    BindableObject 
        Element VisualElement 
            View 
                ... 
                Layout 
                    ... 
                    Layout<T> 
                        ...
            Page 
                ...

(사실 저의 경우 여기서 ArgumentNullException이 나서 결과를 얻지 못했습니다. 코드는 아무리 살펴봐도 다른 것이 없었는데 왜 그런지 모르겠습니다. 물론 어떻게 코드를 다르게 써서 동작할 수 있도록 바꿀 수 있겠습니다만, 지금은 그게 목표가 아니기 때문에 그냥 넘어가도록 하겠습니다. 시간을 많이 허비했네요...ㅠㅠ)

Object를 제외하고, 이 클래스 계층 구조상의 모든 클래스는 Xamarin.Forms.Core.dll 어셈블리에 구현되고 Xamarin.Forms의 네임 스페이스와 연결됩니다.

중요한 클래스들 몇몇을 좀 더 자세히 알아보겠습니다.

BindableObject 클래스는 이름에서도 알 수 있듯이 데이터 바인딩을 지원합니다. 즉, 두 객체의 두 프로퍼티를 연결하여 동일한 값을 유지하게 하는 것입니다. 그러나 BindableObject는 style과 DynamicResource 마크업 확장도 지원합니다. 이 작업은 두 가지 방법으로 수행되는데 하나는 BindableProperty 객체 형식의 BindableObject 프로퍼티 정의를 통해서이고, 다른 하나는 .NET INotifyPropertyChanged 인터페이스를 구현을 통해서입니다. 이것들은 차차 더 깊이 논의하게 될 것입니다.

계층 구조를 따라 내려가 봅시다: 앞서 보았듯이, Xamarin.Forms의 UI 객체는 페이지에 부모-자식 계층으로 정렬되고, Element 클래스는 부모, 자식 관계를 지원 합니다.

VisualElement는 Xamarin.Forms에서 아주 중요한 클래스입니다. 자마린에서 스크린 상에 공간을 점유하는 모든 것을 visual element라고 부릅니다. VisualElement 클래스는 28개의 public 프로퍼티를 정의하고 있는데, size, location, background color, 그리고 IsEnabledIsVisible같은 시각적 혹은 기능적 성질과 관련된 것들입니다.

Xamarin.Forms에서 view라고 하면 버튼, 슬라이더, 텍스트 입력상자와 같은 개개의 시각적 객체를 칭하지만, 레이아웃 클래스의 부모 역시 View입니다. 흥미롭게도 ViewVisualElement에서 상속받은 멤버에 세 개의 public 멤버만 추가합니다. 그것들은 HorizontalOptions ,VerticalOptions(이것들은 당연히 Page에는 적용되지 않습니다),그리고 GestureRecognizers(터치 입력 지원을 위한)입니다.

Layout의 자식은 자식 뷰를 가질 수 있습니다. 하위 뷰는 부모의 경계 내에서 시각적으로 화면에 표시됩니다. Layout에서 파생되는 클래스는 View 타입의 자식 하나만을 가질 수 있지만 제너릭 Layout<T> 클래스는 Children 프로퍼티를 가지고 있어서, 다수의 자식 뷰의 컬렉션(다른 레이아웃 포함)을 가질 수 있습니다. 여러분은 이미 자식을 수평 혹은 수직으로 정렬하는 StackLayout을 보았습니다. Layout 클래스는 View에서 파생 되었지만 Xamarin.Forms에서 매우 중요하므로 레이아웃 자체를 하나의 카테고리로 간주하기도 합니다.

ClassHierarchyXamarin.Forms.CoreXamarin.Forms.Xaml 어셈블리에 정의된 모든 public 클래스, 구조체, 열거형을 보여주지만 인터페이스는 제외합니다. 인터페이스도 중요하므로 직접 살펴보시거나 프로그램을 수정하여 인터페이스까지 나타내도록 해 보시기 바랍니다.

ClassHierarchy는 다양한 플랫폼에서 Xamarin.Forms를 구현하는데 도움이 되는 public 클래스들 중 다수도 표시하지 않습니다. 이 책의 마지막 챕터에서 그것을 보시게 될 겁니다.


A peek into BindableObject and BindableProperty


BindableObject, BindableProperty라는 클래스의 존재는 처음에는 조금 혼란스럽습니다. BindableObject는 Xamarin.Forms API 전체에 대한 기본 클래스, 특히 Element와 더 나아가 VisualElement의 기본 클래스 역할을 한다는 점에서 Object와 매우 흡사합니다.

BindableObjectBindableProperty 형식의 객체를 지원합니다. BindableProperty 객체는 CLR 프로퍼티를 확장합니다. Bindable property를 잘 이해하려면 직접 만들어 보는 것이 가장 좋을 것이지만(이 챕터가 끝나기 전에 직접 해 보시게 될 겁니다) 이미 존재하는 것들을 살펴 보는 것도 한 방법입니다.

7장  "XAML vs. code"에서 두 개의 버튼이 하나는 C# 3.0 객체 초기화 구문으로, 다른 하나는 XAML로 만들어 진 것을 기억하실 겁니다.

다음은 두 개의 버튼을 서로 다른 방법으로 만드는 PropertySettings라는 프로그램인데, 다른 것이 있다면 코드 전용이라는 점입니다. 첫 번째 Label의 프로퍼티는 구식 방식으로, 두 번째 Label의 프로퍼티는 좀 더 세련된 방법으로 설정됩니다.

namespace PropertySettings
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            Label label1 = new Label();
            label1.Text = "Text with CLR properties";
            label1.IsVisible = true;
            label1.Opacity = 0.75;
            label1.HorizontalTextAlignment = TextAlignment.Center;
            label1.VerticalOptions = LayoutOptions.CenterAndExpand;
            label1.TextColor = Color.Blue;
            label1.BackgroundColor = Color.FromRgb(255128128);
            label1.FontSize = Device.GetNamedSize(NamedSize.Medium, new Label());
            label1.FontAttributes = FontAttributes.Bold | FontAttributes.Italic;
            Label label2 = new Label();
            label2.SetValue(Label.TextProperty, "Text with bindable properties");
            label2.SetValue(Label.IsVisibleProperty, true);
            label2.SetValue(Label.OpacityProperty, 0.75);
            label2.SetValue(Label.HorizontalTextAlignmentProperty, TextAlignment.Center);
            label2.SetValue(Label.VerticalOptionsProperty, LayoutOptions.CenterAndExpand);
            label2.SetValue(Label.TextColorProperty, Color.Blue);
            label2.SetValue(Label.BackgroundColorProperty, Color.FromRgb(255128128));
            label2.SetValue(Label.FontSizeProperty, 
                Device.GetNamedSize(NamedSize.Medium, new Label()));
            label2.SetValue(Label.FontAttributesProperty, 
                FontAttributes.Bold | FontAttributes.Italic);
            Content = new StackLayout
            {
                Children =
                {
                    label1,
                    label2
                }
            };
        }
    }
}
cs

두 방법 모두 프로퍼티를 잘 설정했습니다.

그러나 구문은 좀 괴상합니다. 예를 들어,

label2.SetValue(Label.TextProperty, "Text with bindable properties");
cs

SetValue 메소드가 사용되었는데, 이것은 BindableObject에 정의되어 있어서 모든 시각 객체에서 사용될 수 있습니다. 당연히 GetValue 메소드도 있습니다.

SetValue에 대한 첫 번째 인수는 Label.TextProperty이고 static임을 알 수 있습니다. 그러나 그 이름에도 불구하고 이것은 프로퍼티가 아니고, Label 클래스의 static 필드입니다. TextProperty는 읽기 전용이며, Label 클래스 내에 다음과 같이 정의됩니다.

public static readonly BindableProperty TextProperty;
cs

보시다시피 BindableProperty 타입의 객체입니다. 물론, 필드가 TextProperty라는 이름을 가지는게 혼동을 줄지도  모르겠습니다. 그러나 static이기 때문에 다른 Label 객체가 있다 해도 독립적입니다.

Label 클래스의 문서를 살펴보면 Text, TextColor, FontSize, FontAttributes 등 10 개의 프로퍼티를 정의하고 있음을 알 수 있습니다. 또한 그에 상응하는 TextProperty, TextColorProperty, FontSizeProperty, FontAttributesProperty 등의 이름을 가진 BindableProperty 타입의 public static 읽기전용 필드도 볼 수 있습니다.

이 필드와 프로퍼티는 서로 연관되어 있습니다. 실제로 Label 클래스의 내부에서 Text CLR 프로퍼티는 TextProperty 객체를 참조하기 위해 아래와 같이 정의됩니다.

public string Text 
    set { SetValue(Label.TextProperty, value); }
    get { return (string)GetValue(Label.TextProperty); } 
}
cs

따라서 Label.TextProperty 인수를 써서 SetValue를 호출하는 응용 프로그램이 Text 프로퍼티를 직접 설정하는 것과 정확히 같고, 속도면에서 아주 약간 빨라진 것이라는 것을 알 수 있습니다..

Label의 Text 프로퍼티가 내부적으로 정의된 방식은 비밀스런 것이 아니라 그저 표준적인 코드에 불과합니다. 어떤 클래스도 BindableProperty 객체를 정의할 수 있지만 BindableObject에서 파생된 클래스만이 그 클래스의 프로퍼티를 실제로 구현하는 SetValue, GetValue 메서드를 호출할 수 있습니다. 반환값이 object이므로 GetValue메소드는 캐스팅이 필요합니다.

Text 프로퍼티를 관리하는 것과 관련된 모든 작업은 실제로 SetValue ,GetValue 호출로 이루어집니다. BindableObjectBindableProperty 객체는 표준 CLR 프로퍼티의 기능을 효과적으로 확장하여 아래 기능을 수행하는 체계적인 방법을 제공합니다.


  • 프로퍼티 정의
  • 프로퍼티에 디폴트값 주기
  • 프로퍼티의 현재 값 저장
  • 프로퍼티 값 유효성을 검사하는 메카니즘 제공
  • 한개의 클래스 내에서 연관된 프로퍼티들 사이에 일관성 유지
  • 프로퍼티 값 변화에 반응
  • 속성 변경시 알림
  • 데이터 바인딩 지원
  • style 지원
  • dynamic resource 지원

Text라는 프로퍼티와 TextProperty라는 BindableProperty간의 관계는 좀 혼동스러울 수 있습니다. 그러나, 이점을 생각해 보시기 바랍니다. TextPropertyText에 대한 인프라 지원을 제공하기 때문에 프로그래머들은 Text 프로퍼티가 TextProperty라는 이름의 BindableProperty에 의해 "지원"되었다고 말합니다. 이것을 더 짧게 Text 자체가 "bindable property"라고 말하면서도 아무도 혼란에 빠지지 않는 것과 같은 방식으로 둘 간의 관계를 이해하시면 되겠습니다.

Xamarin.Forms의 모든 프로퍼티가 bindable property는 아닙니다. ContentPageContent 프로퍼티나 Layout<T>Children 프로퍼티는 bindable property가 아닙니다. VisualElement에서 정의한 28개의 프로퍼티 중 BoundsResources를 제외한 26개가 bindable property의 지원을 받습니다.

FormattedString과 연결된 Span 클래스는 BindableObject에서 파생되는 것이 아닙니다. 따라서 SpanSetValueGetValue 메서드를 상속하지 않으며 BindableProperty 개체를 구현할 수 없습니다.

다시 말해, LabelText 속성은 bindable property에 의해 지원되지만 SpanText 프로퍼티는 그렇지 않다는 뜻입니다. 그건 어떤 차이가 있는 것일까요?

이전 챕터에서 나온 DynamicVsStatic 프로그램을 생각해 봅시다. 거기서 우리는 DynamicResourceLabelText 프로퍼티에서는 작동하지만 SpanText 프로퍼티에서는 작동하지 않는다는 것을 발견했습니다. DynamicResource는 bindable property에서만 작동할 수 있는 것일까요?

Element에 정의된 다음 public 메소드의 정의를 보면 그것을 확인할 수 있습니다.

public void SetDynamicResource(BindableProperty property, string key);
cs

프로퍼티가 DynamicResource 마크업 확장의 대상일 때, 사전 키와 element의 특정 프로퍼티는 이렇게 연결되는 것입니다.

SetDynamicResource 메소드를 사용하면 코드에서 프로퍼티에 dynamic resource 링크를 설정할 수도 있습니다. 다음은 DynamicVsStatic의 코드 전용 버전인 DynamicVsStaticCode의 페이지 클래스입니다. FormattedStringSpan 객체를 사용하지 않아 약간 단순화되었지만 XAML 파일을 파싱법과 XAML 파서에서 Label element의 Text 프러퍼티를 설정하는 방법을 매우 정확하게 모방하고 있습니다.

namespace DynamicVsStaticCode
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
 
            Padding = new Thickness(50);
 
            // Create resource dictionary and add item.
            Resources = new ResourceDictionary
            {
                {"currentDateTime","Not actually a DateTime" }
            };
 
            Content = new StackLayout
            {
                Children =
                {
                    new Label
                    {
                        Text="StaticResource on Label.Text:",
                        VerticalOptions=LayoutOptions.EndAndExpand,
                        FontSize=Device.GetNamedSize(NamedSize.Medium,typeof(Label))
                    },
 
                    new Label
                    {
                        Text=(string)Resources["currentDateTime"],
                        VerticalOptions=LayoutOptions.StartAndExpand,
                        HorizontalTextAlignment=TextAlignment.Center,
                        FontSize=Device.GetNamedSize(NamedSize.Medium,typeof(Label))
                    },
 
                    new Label
                    {
                        Text="DynamicResource on Label.Text:",
                        VerticalOptions=LayoutOptions.EndAndExpand,
                        FontSize=Device.GetNamedSize(NamedSize.Medium,typeof(Label))
                    }
                }
            };
 
            // Create the final Label with the dynamic resource.
            Label label = new Label
            {
                VerticalOptions = LayoutOptions.StartAndExpand,
                HorizontalTextAlignment = TextAlignment.Center,
                FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label))
            };
 
            label.SetDynamicResource(Label.TextProperty, "currentDateTime");
 
            ((StackLayout)Content).Children.Add(label);
 
            // Start the timer going.
            Device.StartTimer(TimeSpan.FromSeconds(1),
                () =>
                {
                    Resources["currentDateTime"= DateTime.Now.ToString();
                    return true;
                });
        }
    }
}
cs


두 번째 레이블의 Text 프로퍼티는 딕셔너리로부터 직접 설정되는데, 이 맥락 내에서 딕셔너리를 사용하는 것은 조금 취지에 어긋납니다. 그러나 마지막 LabelText 프로퍼티는 SetDynamicResource 호출을 통해 딕셔너리 키에 바인딩되고, 긱셔너리 내용이 변하면 프로퍼티가 갱신되도록 해줍니다.


이 점을 생각해 봅시다: BindableProperty 객체를 사용하여 프로퍼티를 참조할 수 없다면 대체 이 SetDynamicResource 메소드의 용도는 무엇입니까? 메소드를 호출하여 프로퍼티 을 참조하기는 쉽지만 프로퍼티 자체를 참조하는 것은 어렵습니다. 그렇게 하기 위해서는 System.Reflection 네임스페이스의 PropertyInfo 클래스나 LINQ Expression 객체를 사용하는 두 가지 방법이 있습니다. 그러나 BindableProperty 객체는 이 목적을 위해 특별히 설계되었으며 프로퍼티와 딕셔너리 키 간의 기본 링크를 처리하는데 필수적입니다.

비슷하게, 다음 장에서 style을 살펴보면, 스타일과 함께 사용되는 Setter 클래스가 있습니다. SetterBindableProperty 타입의 Property라는 프로퍼티를 정의하고 있는데, 이 프로퍼티는 style로 타겟팅되는 모든 프로퍼티가 bindable property에 의해 백업되도록 위임합니다. 이렇게 해야 style이 타겟팅하는 element에 앞서 style을 정의할 수 있게 됩니다.

데이터 바인딩도 비슷합니다. BindableObject 클래스는 Element에 정의된 SetDynamicResource 메서드와 매우 유사한 SetBinding 메서드를 정의하고 있습니다.

public void SetBinding(BindableProperty targetProperty, BindingBase binding);
cs

다시 첫 번째 인수의 타입을 확인하십시오. 데이터 바인딩이 대상으로하는 모든 프로퍼티는 bindable property에 의해 지원되어야 합니다.

이런 이유로 커스텀 뷰를 만들고 public 프로퍼티를 정의해야 할 때는, 기본적으로 bindable 프로퍼티로 정의하도록 해야할 것입니다. 신중하게 고려해보고 필요치 않다고 판단되거나, 프로퍼티가 스타일이나 데이터 바인딩의 대상이 되는 것이 적절하지 않다고 판단한 경우에만 보통의 CLR 프로퍼티로 정의하도록 합니다.

따라서 BindableObject에서 파생된 클래스를 만들 때마다 클래스에 입력해야 하는 첫 번째 코드 중 하나는 "public static readonly BindableProperty"(Xamarin.Forms 프로그래밍에 있어 가장 특징적인 시퀀스)가 될 것입니다.

댓글 없음:

댓글 쓰기