본문 바로가기

Develop/MAUI 가이드

[Xamarin] BindableProperty 구현하고 관리하는 방법

반응형

Bindable Property
Bindable Property

BindableProperty 구현하고 관리하는 방법

마이크로소프트 공식 문서 에서 BindableProperty 를 구현하는 방법을 소개하고 있습니다. 다만 아쉽게도 공식문서엔 BindalbeProperty 코드를 재사용하거나 관리하는 측면에 대한 디테일한 설명이 없어서 글을 작성하게 되었습니다.

먼저 공식 문서대로 하면 BindableProperty 는 기본적으로 아래와 같이 구현하게 됩니다.

public class MyContentView : ContentView
{
    public static readonly BindableProperty NameProperty =
      BindableProperty.Create ("Name", typeof(string), typeof(MyContentView), null);
    public string Name
    {
      get { return (string)GetValue (NameProperty); }
      set { SetValue (NameProperty, value); }
    }
}

공식 문서와 달리 개인적으로 아래와 같이 구현하는 게 저에게는 더 가독성이 좋습니다.

public class MyContentView : ContentView
{
    public static readonly BindableProperty NameProperty = BindableProperty.Create(
        propertyName: nameof(Name),
        returnType: typeof(string),
        declaringType: typeof(MyContentView),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneWay);
    public string Name
    {
      get => (string)GetValue(NameProperty);
      set => SetValue(NameProperty, value);
    } 
}
  1. BindableProperty.Create 메서드에 입력해야 할 속성이 많다보니 각 속성의 의미가 무엇인지 순서를 외우지 못했다면 위와 같이 작성해야 다른 개발자가 봐도 이해할 수 있습니다.
  2. propertyName 에 단순히 "Name" 이라 적기보다는 nameof 를 활용하면 속성 이름을 변경하면 연동되서 편리합니다.
  3. defaultBindingMode 를 지정하면 Xaml 파일에서 Mode 를 지정하는 코드를 줄일 수 있고, Command 와 같이 한번 바인딩하면 변경하지 않는 속성의 경우 BindingMode.OneTime 을 적용해서 최적화할 수도 있습니다. 다만 다른 개발자에게 소스코드가 공개되지 않는 View 라이브러리일 경우, 기본 바인딩을 BindingMode.OneWay 이라고 가정하기 때문에 변경하지 않는걸 추천합니다.

공식문서를 따라하면 BindableProperty 가 변경되었을 때 대응하는 코드는 아래와 같이 구현하게 됩니다.

public class MyContentView : ContentView
{
    public static readonly BindableProperty NameProperty =
      BindableProperty.Create (
        "Name", typeof(string), typeof(MyContentView), null, propertyChanged: OnNameChanged);

    // ...

    public string Name
    {
      get => (string)GetValue(NameProperty);
      set => SetValue(NameProperty, value);
    }

    // ...

    static void OnNameChanged (BindableObject bindable, object oldValue, object newValue)
    {
      // Property changed implementation goes here
    }
}

저는 아래와 같이 구현하는게 가독성이 좋고 관리하기 편합니다.

public class MyContentView : ContentView
{
    #region Name
    public static readonly BindableProperty NameProperty = BindableProperty.Create(
        propertyName: nameof(Name),
        returnType: typeof(string),
        declaringType: typeof(MyContentView),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneWay,
        propertyChanged: (bindable, oldValue, newValue) => 
        {
            // 1번) 기본적으로 잘못 호출할 일이 없도록 여기서 전부 해결한다.
            if(bindable is MyContentView view && newValue is string newName)
            {
                // Name 속성 변경에 대응하는 코드 ...
            }
            // 2번) 하지만 재사용이 가능한 코드로 해결하고 싶다면 아래와 같이 구현한다.
            (view as MyContentView).OnNameChanged((string)newValue);
        });

    // 2번) 재사용할 메서드
    private void OnNameChanged(string newName)
    {
      // Name 속성 변경에 대응하는 코드 ...
    }

    public string Name
    {
      get => (string)GetValue(NameProperty);
      set => SetValue(NameProperty, value);
    }
    #endregion
}
  1. 굳이 재사용하지 않을 static 메서드를 만들 필요는 없다고 생각하기도 하고, 만약 다른 개발자가 static 메서드를 호출하면 버그가 될 수도 있어서, 저는 BindableProperty 내에 메서드를 정의합니다. 하지만 재사용해야 한다면 2번과 같이 구현하기도 합니다.
  2. 공식 문서나 샘플 코드 또는 Xamarin 소스 코드에서는 BindableProperty 와 연관된 코드들의 위치를 클래스에서 흩어 놓았는데, 저는 한 위치에 모아서 #region 을 지정해 관리하는 걸 선호하는 편입니다. 속성과 관련하여 수정할 일이 있을 때 다같이 수정해야 하는데 위치가 흩어져 있으면 찾아서 수정해야 하는 번거로움을 해결할 수 있어서 모아 놓습니다.

BindableProperty 재사용 패턴 (IElement 패턴)

신기하게도 Xamarin 소스코드에서는 열심히 사용하는 패턴인데 공식문서에서는 언급조차 하지 않는 패턴입니다. 이유는 알 수 없지만 코드를 특정한 방식으로 사용하라고 공식 문서에 남기지 않기 위해 설명을 하지 않은 걸까 추측하고 있습니다.

BindableProperty는 특이한 방법으로 구현하는 속성이다 보니 재사용은 아래와 같이 구현해야 합니다. 예제로는 MVVM 패턴에서 빠질 수 없는 Command 를 재사용 가능한 BindableProperty 로 구현하는 방법을 보여 드리겠습니다.

1. ICommandElement 인터페이스 정의

[EditorBrowsable(EditorBrowsableState.Never)]
public interface ICommandElement
{
    ICommand Command { get; set; }
    void OnCommandChanged(Command newValue);

    object CommandParameter { get; set; }
}

2. CommandElement static 클래스 정의

static class CommandElement
{
    public static readonly BindableProperty CommandProperty
        = BindableProperty.Create(
            propertyName: nameof(ICommandElement.Command),
            returnType: typeof(ICommand),
            declaringType: typeof(ICommandElement),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneTime,
            propertyChanged: );
    private static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue)
        => (bindable as ICommandElement).OnCommandChanged((ICommand)newValue);

    public static readonly BindableProperty CommandParameterProperty
        = BindableProperty.Create(
            propertyName: nameof(ICommandElement.CommandParameter),
            returnType: typeof(object),
            declaringType: typeof(ICommandElement),
            defaultValue: null,
            defaultBindingMode: BindingMode.OneTime);
    }

3. ICommandElement 와 CommandElement 를 활용해 BindableProperty 재사용

public class MyContentView : ContentView, ICommandElement
{
    public static readonly BindableProperty CommandProperty = CommandElement.CommandProperty;
    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public static readonly BindableProperty CommandParameterProperty = CommandElement.CommandParameterProperty;
    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }
}
반응형