Defining bindable properties
포인트 단위로 글꼴 크기를 지정할 수 있는 Label 클래스가 있다고 가정해 봅시다. 이 클래스를 "alternative Label"이라는 뜻으로 AltLabel이라고 부르기로 합니다. 이 클래스는 Label에서 파생되며 PointSize라는 새 프로퍼티를 가집니다.
PointSize는 bindable property의 지원을 받아야 합니까? 물론이죠(그렇게 하는 것의 실제 이점은 나중에 보여드릴 것입니다)!
코드 전용 AltLabel 클래스는 Xamarin.FormsBook.Toolkit 라이브러리에 포함시킬 것이므로 여러 응용 프로그램에 액세스 할 수 있습니다. 새 PointSize 프로퍼티는 PointSizeProperty라는 BindableProperty 객체와 그것을 참조하는 PointSize라는 CLR 프로퍼티로 구현됩니다.
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty ...;
...
public double PointSize
{
set { SetValue(PointSizeProperty, value); }
get { return (double)GetValue(PointSizeProperty); }
}
...
}
| cs |
필드와 프로퍼티 모두 public이어야 합니다.
PointSizeProperty는 static readonly로 정의되었으므로 정적 생성자 또는 필드 정의와 함께 할당해야 하며 그 후에는 변경할 수 없습니다. 일반적으로는 BindableProperty 객체는 정적 BindableProperty.Create 메서드를 사용하여 필드 정의시에 지정됩니다. 그리고, 네 개의 인수가 필요합니다 (인수 이름과 함께 아래에 표시):
- propertyName 프로퍼티의 text 이름(이 경우 "PointSize")
- returnType 프로퍼티의 타입(이 경우 double)
- declaringType 프로퍼티를 정의하는 클래스의 타입(AltLabel)
- defaultValue 디폴트값(8 포인트로 합니다)
두번째, 세번째 인수는 보통 typeof 구문과 함께 사용됩니다. 다음은 BindableProperty.Create에 이 네 개의 인수를 전달하는 할당문입니다.
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty =
BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType
typeof(AltLabel), // declaringType
8.0 // defalutValue
...);
...
}
| cs |
디폴트 값은 8이 아니라 8.0 입니다. BindableProperty.Create는 모든 타입의 프로퍼티를 처리하도록 설계되었으므로 defaultValue 매개변수는 object로 정의됩니다. C# 컴파일러가 해당 인수로 8을 만나면, 8이 int이고 메서드에 int를 전달한다고 가정합니다. 런타임까지는 문제가 드러나지 않지만, BindableProperty.Create 메서드는 기본값이 double 타입이어야 하므로 TypeInitializationException이 발생합니다.
기본값으로 지정할 값의 타입도 명시해야 합니다. Bindable property를 사용할 때 기본값을 지정하지 않는 것은 매우 흔한 실수입니다.
BindableProperty.Create에는 6 개의 옵션 인수가 더 있습니다. 아래에 인수 이름과 목적을 보여 드립니다.
- defaultBindingMode 데이터 바인딩 시 사용
- validateValue 유효값 체크
- propertyChanged 프로퍼티가 변했는지를 알려주는 콜백
- propertyChanging 프로퍼티가 변하려고 할 때 알려주는 콜백
- coerceValue 설정 값을 다른 값으로 강요하는 콜백(예를 들어 값의 범위를 제한하는 것 같은)
- defaultValueCreator 디폴트값을 만드는 콜백. 이것은 일반적으로 클래스의 모든 인스턴스간에 공유할 수 없는 디폴트 객체를 인스턴스화하는데 사용됩니다(예를 들어, List나 Dictionary와 같은 콜렉션 객체).
유효성 검사(validation), 강제 변환(coercion), 프로퍼티 변경(property-changed) 처리를 CLR 프로퍼티로 수행하지 마시기 바랍니다. CLR 프로퍼티는 SetValue, GetValue 호출에 국한되어야 합니다. 다른 모든 작업은 bindable property 기반으로 수행되어야 합니다.
BindableProperty.Create 호출 시 이 모든 선택적 인수가 필요한 것은 매우 드문 일입니다. 그런 이유로 선택적 인수 사용시에는 C# 4.0에서 도입된 명명된 인수 방법을 사용합니다. 특정 선택 인수를 사용할 때는 아래의 예처럼 인수이름과 콜론을 사용합니다.
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty =
BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType
typeof(AltLabel), // declaringType
8.0 // defalutValue
propertyChanged:OnPointSizeChanged);
...
}
| cs |
두말할 것도 없이 propertyChanged는 선택적 인수 가운데서 가장 중요한데, 프로퍼티가 변경될 때 SetValue를 직접 호출하거나 CLR 프로퍼티를 통해 이 콜백으로 알림을 받도록 하기 때문입니다.
이 예제에서 property-changed 핸들러는 OnPointSizeChanged입니다. 이 핸들러는 프로퍼티가 진짜로 변했을 때만 호출되며 같은 값으로 다시 셋되었을 때에는 호출되지 않습니다. OnPointSizeChanged는 정적 필드에서 참조되므로 메서드 자체도 정적이어야 합니다.
public class AltLabel : Label
{
...
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
...
}
...
}
| cs |
좀 이상하군요. 한 개의 프로그램 내에 여러 개의 AltLabel 인스턴스가 존재할 수 있는데, 그 중 어떤 인스턴스의 프로퍼티가 변화해도 똑같은 정적 메소드가 호출됩니다. 그렇다면 어떤 AltLabel 인스턴스가 변화했는지 알수가 있을까요?
핸들러의 첫번째 인수로 넘겨지는 것이 바로 변화한 인스턴스이므로 핸들러는 어떤 인스턴스가 변화했는지 알 수 있습니다. 이 첫 번째 인수는 BindableObject로 정의되어 있지만, 이 경우 실제로는 AltLabel 타입이며 어떤 AltLabel 인스턴스의 프로퍼티가 변경되었는지 나타냅니다. 따라서 이 첫번째 인수는 안심하고 AltLabel로 캐스팅해도 좋습니다.
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
AltLabel altLabel = (AltLabel)bindable;
...
}
| cs |
그런 다음 속성이 변경된 AltLabel의 인스턴스 어느 것이나 참조할 수 있습니다. 두번째, 세번째 인수는 이 경우 모두 double 값이며, 이전 값과 바뀐 값을 의미합니다.
간혹 인수를 실제의 타입으로 변환하여 메소드를 호출하는 것이 편리할 때가 있습니다.
public class AltLabel : Label
{
...
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
((AltLabel)bindable).OnPointSizeChanged((double)oldValue, (double)newValue);
}
void OnPointSizeChanged(double oldValue, double newValue)
{
....
}
}
| cs |
이제 인스턴스 메서드는 기본 클래스의 인스턴스 프로퍼티나 메서드를 일반적으로 사용하는 것처럼 사용할 수 있습니다.
이 클래스의 경우 OnPointSizeChanged 메서드는 새 포인트 크기와 변환 팩터를 기반으로 FontSize 프로퍼티를 설정해야 합니다. 덧붙여 생성자는 디폴트 PointSize 값을 기반으로 FontSize 프로퍼티를 초기화해야 합니다. 이 작업은 간단한 SetLabelFontSize 메서드를 통해 수행됩니다. 다음은 완성된 클래스입니다.
public class AltLabel : Label
{
public static readonly BindableProperty PointSizeProperty =
BindableProperty.Create("PointSize", // propertyName
typeof(double), // returnType
typeof(AltLabel), // declaringType
8.0, // defalutValue
propertyChanged:OnPointSizeChanged);
public AltLabel()
{
SetLabelFontSize((double)PointSizeProperty.DefaultValue);
}
public double PointSize
{
set { SetValue(PointSizeProperty, value); }
get { return (double)GetValue(PointSizeProperty); }
}
static void OnPointSizeChanged(BindableObject bindable, object oldValue, object newValue)
{
((AltLabel)bindable).OnPointSizeChanged((double)oldValue, (double)newValue);
}
void OnPointSizeChanged(double oldValue, double newValue)
{
SetLabelFontSize(newValue);
}
void SetLabelFontSize(double pointSize)
{
FontSize = 160 * pointSize / 72;
}
}
| cs |
인스턴스 OnPointSizeChanged 프로퍼티가 newValue를 사용하는 대신 PointSize 프로퍼티에 직접 액세스 할 수도 있습니다. property-changed 핸들러가 호출될 때에는 기본 프로퍼티 값이 이미 변경된 이후가 됩니다. 그러나 private 필드가 CLR 프로퍼티를 백업할 때처럼 기본 값에 직접 액세스 할 수는 없습니다. 이 기본 값은 BindableObject에 private이며 GetValue 호출을 통해서만 액세스 할 수 있습니다.
물론, AltLabel을 사용하는 코드가 FontSize 프로퍼티를 설정하고 PointSize 설정을 오버라이드 것을 막을 수는 없지만, 그런 점을 알고 코드가 작성되어야 할 것입니다. 다음은 AltLabel을 사용하여 4에서 12까지의 포인트 크기를 표시하는 PointSizedText라는 프로그램입니다.
<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"
xmlns:local="clr-namespace:PointSizedText"
x:Class="PointSizedText.MainPage">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="5,20,0,0"
Android="5,0,0,0"
WinPhone="5,0,0,0" />
</ContentPage.Padding>
<StackLayout x:Name="stackLayout">
<toolkit:AltLabel Text="Text of 4 points" PointSize="4" />
<toolkit:AltLabel Text="Text of 5 points" PointSize="5" />
<toolkit:AltLabel Text="Text of 6 points" PointSize="6" />
<toolkit:AltLabel Text="Text of 7 points" PointSize="7" />
<toolkit:AltLabel Text="Text of 8 points" PointSize="8" />
<toolkit:AltLabel Text="Text of 9 points" PointSize="9" />
<toolkit:AltLabel Text="Text of 10 points" PointSize="10" />
<toolkit:AltLabel Text="Text of 11 points" PointSize="11" />
<toolkit:AltLabel Text="Text of 12 points" PointSize="12" />
</StackLayout>
</ContentPage>
| cs |
public MainPage()
{
// Instantiate sometehing in library so it can be used in XAML.
var unused = new Xamarin.FormsBook.Toolkit.AltLabel();
InitializeComponent();
}
| cs |
스크린샷은 아래와 같습니다.
The read-only bindable property
텍스트의 단어 수를 세서 Label로 보여주는 애플리케이션을 가정해 보겠습니다. Label로부터 파생되는 클래스에 바로 이 기능을 구현하려고 합니다. 이 클래스를 CountedLabel이라고 명명합니다.
이 시점에서 가장 먼저 생각해야할 것은 WordCountProperty라는 BindableProperty 객체와, 그에 상응하는 WordCount라는 CLR 프로퍼티를 정의하는 것입니다.
주의할 것은 WordCount 프로퍼티는 CountedLabel 클래스 내에서 설정되어야 의미가 있다는 점입니다. 그것은 WordCount CLR 프로퍼티가 public set 접근자여서는 안 된다는 뜻입니다. 그래서 아래와 같이 정의됩니다.
public int WordCount
{
private set { SetValue(WordCountProperty, value); }
get { return (double)GetValue(WordCountProperty); }
}
| cs |
get 접근자는 여전히 public이지만 set은 private입니다. 이러면 충분한 것일까요?
정확히 말하자면 그렇지는 않습니다. CLR 프로퍼티의 public set 접근자에도 불구하고 CountedLabel의 외부 코드는 CountableLabel.WordCountProperty라는 bindable property 객체를 사용하여 SetValue를 호출할 수 있습니다. 그런 형태의 프로퍼티 설정도 금지되어야 하겠습니다. 그러나 WordCountProperty 객체가 public인 경우 그것이 가능한 일일까요?
그 해결책은 BindableProperty.CreateReadOnly 메서드를 사용하여 읽기 전용 bindable property를 만드는 것입니다. Xamarin.Forms API 자체로 여러 가지 읽기 전용 bindable property를 정의합니다. 예를 들자면 VisualElement에서 정의한 Width 및 Height 프로퍼티같은 것들입니다.
이제 우리 것을 만들어볼 차례입니다.
첫 번째 단계는 BindableProperty.CreateReadOnly를 BindableProperty.Create에서와 동일한 인수로 호출하는 것입니다. 그러나 CreateReadOnly 메서드는 BindableProperty가 아닌 BindablePropertyKey 객체를 반환합니다. BindableProperty와 마찬가지로 이 객체를 static readonly로 정의하는데 다만 클래스에 private으로 만듭니다.
static readonly BindablePropertyKey WordCountKey =
BindableProperty.CreateReadOnly("WordCount", // propertyName
typeof(int), // returnType
typeof(CountedLabel), // declaringType
0); // defaultValue
| cs |
이 BindablePropertyKey 객체는 암호화 키 같은 것으로 여기지 마십시오. 훨씬 단순하게 그냥 클래스에 private한 객체일 뿐입니다.
두 번째 단계는 BindablePropertyKey의 BindableProperty 프로퍼티를 사용하여 public BindableProperty 객체를 만드는 것입니다.
public class CountedLabel : Label
{
…
public static readonly BindableProperty WordCountProperty = WordCountKey.BindableProperty;
…
}
| cs |
이 BindableProperty 객체는 public입니다만 특별한 종류의 것이어서 SetValue 호출에 쓰일 수 없습니다. 시도시 InvalidOperationException을 일으킬 것입니다.
그러나 BindablePropertyKey 객체를 받아들이는 SetValue 메서드가 오버로드되어 있습니다. CLR set 접근자는 이 객체를 사용하여 SetValue를 호출 할 수 있지만, 이 set 접근자는 private으로 해 주어 외부에서 설정하지 못하도록 합니다.
public class CountedLabel : Label
{
…
public int WordCount
{
private set { SetValue(WordCountProperty, value); }
get { return (int)GetValue(WordCountProperty); }
}
…
}
| cs |
이제 WordCount 프로퍼티는 CountedLabel 내에서 설정할 수 있게 됩니다. 그렇다면 이 클래스가 프로퍼티를 설정해야 하는 때는 언제입니까? 이 CountedLabel 클래스는 Text 프로퍼티가 변경되면 이를 감지할 필요가 있습니다.
안타깝게도 Label에 TextChanged 이벤트가 따로 있지는 않습니다. 그러나 BindableObject는 INotifyPropertyChanged 인터페이스를 구현합니다. 이는 특히 MVVM (Model-View-ViewModel) 아키텍처를 구현하는 어플리케이션의 경우 매우 중요한 .NET 인터페이스입니다. 여러분은 18장에서 사용하는 법을 배우게 되실 겁니다.
INotifyPropertyChanged 인터페이스는 System.ComponentModel 네임스페이스에 다음과 같이 정의되어 있습니다.
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
| cs |
BindableObject에서 파생된 모든 클래스는 BindableProperty가 지원하는 프로퍼티가 변경될 때마다 PropertyChanged 이벤트를 자동으로 발생시킵니다. 이 이벤트와 동반되는 PropertyChangedEventArgs 객체는 변경된 프로퍼티를 식별하는 string 타입의 PropertyName이라는 프로퍼티를 가지고 있습니다.
따라서 CountedLabel은 PropertyChanged 이벤트에 대한 핸들러를 연결하고 "Text"라는 프로퍼티 이름을 확인해야 합니다. 바로 그 시점에서 단어 수를 계산하는 기술을 사용하게 됩니다. 여기서는 이 이벤트에 람다 함수를 사용할 것입니다. 핸들러는 Split을 호출하여 문자열을 단어로 분리하고 몇 개인지 확인합니다. Split 메서드는 공백, 대시, em 대시 (Unicode \u2014)를 기반으로 텍스트를 분할합니다.
namespace Xamarin.FormsBook.Toolkit
{
public class CountedLabel : Label
{
static readonly BindablePropertyKey WordCountKey =
BindableProperty.CreateReadOnly("WordCount", // propertyName
typeof(int), // returnType
typeof(CountedLabel), // declaringType
0); // defaultValue
public static readonly BindableProperty WordCountProperty =
WordCountKey.BindableProperty;
public CountedLabel()
{
// Set the WordCount property when the Text property changes.
PropertyChanged += (object sender, PropertyChangedEventArgs args) =>
{
if (args.PropertyName == "Text")
{
WordCount = 0;
}
else
{
WordCount = Text.Split(' ', '-', '\u2014').Length;
}
};
}
public int WordCount
{
private set { SetValue(WordCountProperty, value); }
get { return (int)GetValue(WordCountProperty); }
}
}
}
| cs |
이 클래스에는 핸들러에 사용될 PropertyChangedEventArgs 인수를 위해 System.ComponentModel 네임스페이스가 using 지시문을 통해 포함되어 있습니다. Xamarin.Forms에는 PropertyChangingEventArgs(현재형)라는 클래스도 정의되어 있습니다. PropertyChanged 핸들러에는 PropertyChangedEventArgs(과거형)이 필요합니다.
(작성 중)
댓글 없음:
댓글 쓰기