MixedCode

안녕하세요. 믹스드코드입니다.

이번 세션은 크로스플랫폼 앱 개발 기술 자마린폼즈의 뷰에 대한 부분을 좀더  자세히 들여다 보고 뷰를 담당하고 있는 XAML 페이지에서 사용 가능한 각종 확장 기능들을 하나씩 알아가 보도록 하겠습니다.

1.MVVM in XAMARIN - View 심화
데스크톱 응용 프로그램 개발 기술인 WPF에서도 XAML 언어를 표현해 화면을 표현하는데요.
XAML을 통해 화면을 표현하고 사용자와의 상호작용 및 XAML요소의 기능을 확장 시켜 줄수 있는 기능을 제공하는 XAML의 핵심 기술 삼총사들에 대해 알아보도록 하겠습니다.
그 주인공들은 바로 Trigger,Converter,Behavior 입니다.

1) Trigger?
트리거는 말그대로 XAML의 특정요소에서 어떤 변화(이벤트,속성값의변화,데이터값)가 발생하면 XAML코드로 미리 설정된 기능이 작동되게 XAML코드로 해당 기능을 정의하는 것을 말합니다.
트리거는 XAML코드상에서 관련 기능이 구현되며 C# 언어를 이용해 코드비하인드에서 이벤트 핸들러등을 이용하는 방법이 아니라는 거죠.

트리거의 종류는 크게 아래와 같이 Property Trigger, DataTrigger,Event Trigger 3가지가 종류가 존재합니다.

1.1 Property Trigger
- 특정 XAML 요소의 지정된 속성값이 특정 조건으로 변경되면 해당 요소의 다른 속성값을 변경처리합니다.  
- 스타일을 사용하여 특정요소 유형들의 공통 스타일을  지정하는데도 유용하게 사용됩니다.
- 속성트리거는 주로 뷰내의 특정 요소의 속성값이 변경되면 동일 요소의 속성값을 변경처리하는데 주로 사용됩니다.

ㅁ실습 시나리오1: 텍스트 박스에 현재 커서 포커싱이 되어 있으면 텍스트 박스의 배경색 속성값을 파란색으로 변경해보겠습니다.

-in MainPage.xaml

<Entry Placeholder="enter name">
    <Entry.Triggers>
        <Trigger TargetType="Entry"
             Property="IsFocused" Value="True">
            <Setter Property="BackgroundColor" Value="Red" />
        </Trigger>
    </Entry.Triggers>

</Entry>


ㅁ실습 시나리오2: 전역 스타일에 텍스트박스의  스타일 트리거를 지정해 사용자 액션에 따른 기본적인 요소의 작동 스타일 적용할수 있습니다
-이건 마치 HTML기반의 CSS3에서 제공하는 기능과도 유사합니다.

-in App.XAML

         <Style x:Key="EntryNormalStyle" TargetType="Entry">
                <Setter Property="HorizontalOptions" Value="Fill" />

                <Style.Triggers>
                    <Trigger TargetType="Entry" Property="IsFocused" Value="False">
                        <Setter Property="BackgroundColor" Value="Gray" />
                    </Trigger>

                    <Trigger TargetType="Entry" Property="IsFocused" Value="True">
                        <Setter Property="BackgroundColor" Value="White" />
                    </Trigger>
                </Style.Triggers>

          </Style>


ㅁ배포결과: 배포결과 화면을 보면 HTML CSS처럼 스타일이 전역으로 설정되어 있어도 특정 페이지에 지정된 스타일이 있으면 해당 스타일이 우선적용되는것을 볼수 있죠.
CSS 스타일 적용하는것처럼 XAML 스타일도 요소내의 인라인 스타일 => 페이지스타일 => 전역스타일 우선순위로 스타일및 트리거 적용이 작동됩니다.



1.2 Data Trigger
- 데이터 트리거는 지정한(Target) 요소의 바인딩된 속성값을 기반으로 현재 요소의 속성을 변경할수있는 기능을 기본적으로 제공합니다.
- 데이터 트리거는 Target요소를 지정하고 타겟요소이 데이터 바인딩된 속성값을 기반으로 본인요소의 속성을 제어합니다.
- Data Trigger는 데이터 바인딩과 밀접하게 연관되어 있으며 뷰모델 또는 바인딩 가능한 데이터 모델의 속성들과 바인딩된 XAML 요소의 바인딩된 특정 속성값이 지정된 값으로 변경되면 자동감지하여 요소 자신의 속성값을 변경해주는 기능을 제공합니다.
- 데이터 트리거는 뷰모델의 특정 데이터 속성이 지정된 값으로 변경되면 뷰의 XAML 요소들의 보여지는 모습을 제어하는 수단으로 유용하게 많이 사용됩니다.

ㅁ실습시나리오1:  REGIST버튼은 반드시 사용자 아이디 텍스트 속성값이 비어있지 않아야 활성화 되어야 합니다.

-in MainPage.xaml

<Button x:Name="btnRegist" Text="Regist" Command="{Binding RegistCommand}" Style="{StaticResource NormalButtonStyle}">
 <Button.Triggers>
  <DataTrigger TargetType="Button" Binding="{Binding Source={x:Reference txtUserID},Path=Text.Length}" Value="0">
   <Setter Property="IsEnabled" Value="False" />
  </DataTrigger>
 </Button.Triggers>

</Button>

-추가로 Data Trigger 여러 조건식을 사용해 트리거링이 가능한 Muti Data Trigger 기능도 제공합니다.
멀티 트리거는 별개의 트리거로 보이지만 사실 DataTrigger의 확장기능 기능으로 보시면됩니다.
데이터 조건을 다중건으로 필터링하여 좀더 섬세하게 조건별 제어가 가능합니다.


sample) 멀티트리거 샘플
<MultiTrigger TargetType="Button">
    <MultiTrigger.Conditions>
        <BindingCondition Binding="{Binding Source={x:Reference email},Path=Text.Length}" Value="0" />
        <BindingCondition Binding="{Binding Source={x:Reference phone},Path=Text.Length}" Value="0" />
    </MultiTrigger.Conditions>

  <Setter Property="IsEnabled" Value="False" />
</MultiTrigger>

1.3 Event Trigger
- XAML 요소에서 지정된 이벤트(라우티드이벤트)가 발생하면 특정 작업(유효성검사) 을 수행하게 하는것입니다.
- 이벤트 트리거는 요소의 지정된 이벤트가 발생했을때 기타 트리거들처럼 특정요소의 속성값을 바꾸는것보다는 일반적으로 특정기능을 수행하는것에 포커싱이 되어 있습니다.
- 자마린폼즈의 이벤트 트리거는 WPF의 이벤트 트리거와는 다소 다르게 사용법과 제공기능에 아직 한계가 많아 보입니다.
- Xaml 요소에서 지정된 이벤트가 발생하면 액션메소드를 실행시켜 유효성검사등의 로직을 처리한더거나 하는 기능은 WPF의 기본기능으로
제공되는것과 동일하지만 사용법에 있어 아직은 확장성이 떨어지는듯합니다.

ㅁ실습시나리오 1: 전화번호를 숫자로만 입력받고 싶어용.
-PCL프로젝트에 Triggers폴더를 하나 생성합니다.
-해당 폴더안에 아래 클래스를 추가합니다.

- in Trigger

   public class NumericValidationTriggerAction : TriggerAction<Entry>
    {
        protected override void Invoke(Entry entry)
        {
            double result;
            bool isValid = Double.TryParse(entry.Text, out result);
            entry.TextColor = isValid ? Color.Default : Color.Red;
        }
    }

-in Xaml
-MainPage.xaml 상단에 아래 네임스페이스를 추가하고 전화번호 입력 텍스트박스에 이벤트 트리거를 추가합니다.

    xmlns:trigger="clr-namespace:XamarinMVVMDemo.Triggers"

    <Entry Grid.Row="4" Grid.Column="1" Text="{Binding Telephone,Mode=TwoWay}" Style="{StaticResource EntryNormalStyle}">
            <Entry.Triggers>
                <EventTrigger Event="TextChanged">
                    <trigger:NumericValidationTriggerAction />
                </EventTrigger>
            </Entry.Triggers>

     </Entry>


2) Converter
-컨버터는  데이터 바인딩시 바인딩 된 원본 데이터를  XAML요소의 속성으로 바로 표시하지 않고 원본 데이터를 특정 로직을 적용해 데이터를 변경하거나 데이터 포맷을 변경해 요소 속성에  표출하는 방법을 제공합니다.
-일종의 C# 으로 정의되는 각종 유틸리티 메소드 등으로 XAML상에서 호출해서 사용할수 있는게 특징입니다.
-주로 표시 포맷을 변경하거나 로직을 적용하고 결과값을 반환하고자 할때 사용합니다.
-PCL 프로젝트에 Converter 폴더를 하나만들고 날짜변환 컨버트를 하나 생성해보겠습니다.
 

ㅁ실습 시나리오1 : 날짜시간 포맷 데이터를 날짜만 문자열로 표기합니다.

-In Converter:

    public class DatetimeToStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,object parameter, CultureInfo culture)
        {
            return ((DateTime)value).ToString("yyyy-MM-dd");        
        }

        public object ConvertBack(object value, Type targetType,object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }


ㅁ실습 시나리오2: 사용자가 입력한 데이터 길이가 5자리 이하이면 REGIST 버튼을 비활성화 해주세요.

-바인딩 텍스트 값과 필수  입력 길이값을 컨버터 파라메터로 전달받아 Boolean형으로 데이터를 반환받습니다.

-In Converter:

    public class TextLengthValidationConverter : IValueConverter
    {
      
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if ((int)value < (int)parameter)
                return false;
            else
                return true;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }



-in Xaml
ㄴXAML 페이지 상단에 네임스페이스를 정의합니다.
ㄴ리소스에 해당 컨버터에 대해 키를 지정하고 필요한 요소의 바인딩 속성에서 컨버터 키를 이용해 접근해 사용합니다.

    xmlns:converter="clr-namespace:XamarinMVVMDemo.Converter"

    <ContentPage.Resources>
        <converter:DatetimeToStringConverter x:Key="datetimeToStringConverter" />
        <converter:TextLengthValidationConverter x:Key="TextLengthValidationConverter" />
    </ContentPage.Resources
>

  <Entry Grid.Row="5" Grid.Column="1" Text="{Binding RegistDate,Mode=TwoWay,Converter={StaticResource}}" Style="{StaticResource EntryNormalStyle}"></Entry>


-in MainPage.xaml
ㄴ데이터 트리거와 Converter를 이용한 지정한 길이 이하로 입력하면 버튼을 비활성화 처리합니다.

      <Button x:Name="btnRegist" Text="Regist" Command="{Binding RegistCommand}" Style="{StaticResource NormalButtonStyle}">
                <Button.Triggers>
                    <DataTrigger TargetType="Button" Binding="{Binding Source={x:Reference txtUserID},Path=Text.Length,Converter={StaticResource textLengthValidationConverter},ConverterParameter=5}" Value="false">
                        <Setter Property="IsEnabled" Value="False" />
                    </DataTrigger>

                </Button.Triggers>
         </Button>


3)Behaviors
- 비헤이비어의 사용 목적은 XAML요소에 새로운 개발자 정의 속성 및 기능을 제공하기 위해서 사용됩니다.
- Label 요소에 내가 정의한 바인딩 가능한 속성을 하나 확장하거나 특정 기능을 추가로 구현하고 싶을 때 보통은 해당 라벨 클래스를 상속받아 구현한 개발자정의 클래스를 통해
추가적인 기능을 구현하지만 비헤이비어를 이용하면 쉽게 기존 요소에 새로운 기능을 추가할수 있습니다.

-XAML 요소에서 지정된 이벤트가 발생하면 특정 커맨드를 실행시키는 기능으로도 많이 사용됩니다.(비헤이비어를 통해 기능을 확장해야함.)
- 즉 뷰에서 XAML요소이 이벤트가 발생하면 XAML 요소에 애니메이션 효과를 준다거나 또는 뷰모델의 특정 컨맨드를 실행시켜 뷰모델과 
상호작용을 지시한다거나 하는 기능을 제공하는것이죠.



**MVVM 이 좋다는것은 모두 알지만 실제 현업에서 MVVM 패턴을 적용해보면 컨트롤을 코드 비하인드 처럼 쉽게 제어할수 없다고 판단되거나
컨트롤 기능을 확장하기가 어렵다고 판단되어 패턴을 무시하고 코드비하인드코드를 믹스해 사용하는 사례가 많은데 이럴때는 주로
비헤이비어나 Attached Property 기능을 이용해 코드 비하인드에서 하려고 했던 내용들을 해당 비헤이비어 클래스에서 구현하시면 됩니다.

** 비헤이비어는 코드 비하인드에서 할수 있는 모든 일 이상을 할수 있으므로 UI관련 신규 기능을 구현하거나 확장해야한다면 비헤이비어와 많이 친해지셔야 합니다.

https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/behaviors/


3.1 Behavior 유형
비헤이비어는 구현방법에 따라  크게 Attached Behavior, Behavior(Xamarin.Forms Behavior),기능확장에 포커스된 Reusable Behavior 3종류로 나뉩니다. 

ㅁAttached Behavior: XAML요소에 개발자정의 속성을 동적으로 추가하고 해당 요소를 코드비하인드에서처럼 쉽게 제어할수 있습니다.
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/behaviors/attached/

public static class NumericValidationBehavior
{
    public static readonly BindableProperty AttachBehaviorProperty =
        BindableProperty.CreateAttached (
            "AttachBehavior",
            typeof(bool),
            typeof(NumericValidationBehavior),
            false,
            propertyChanged:OnAttachBehaviorChanged);

    public static bool GetAttachBehavior (BindableObject view)
    {
        return (bool)view.GetValue (AttachBehaviorProperty);
    }

    public static void SetAttachBehavior (BindableObject view, bool value)
    {
        view.SetValue (AttachBehaviorProperty, value);
    }

    static void OnAttachBehaviorChanged (BindableObject view, object oldValue, object newValue)
    {
        var entry = view as Entry;
        if (entry == null) {
            return;
        }

        bool attachBehavior = (bool)newValue;
        if (attachBehavior) {
            entry.TextChanged += OnEntryTextChanged;
        } else {
            entry.TextChanged -= OnEntryTextChanged;
        }
    }

    static void OnEntryTextChanged (object sender, TextChangedEventArgs args)
    {
        double result;
        bool isValid = double.TryParse (args.NewTextValue, out result);
        ((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
    }
}


ㅁXamarin.Forms Behaviors : Behavior 또는 Behavior <T> 클래스를 상속받아 구현하며 해당 요소에 대해 비헤이비어가 Attach/Detach 될때 해당 요소의 각종 이벤트 기반 처리가 가능해집니다.
ㄴhttps://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/behaviors/creating/


public class NumericValidationBehavior : Behavior<Entry>
{
    protected override void OnAttachedTo(Entry entry)
    {
        entry.TextChanged += OnEntryTextChanged;
        base.OnAttachedTo(entry);
    }

    protected override void OnDetachingFrom(Entry entry)
    {
        entry.TextChanged -= OnEntryTextChanged;
        base.OnDetachingFrom(entry);
    }

    void OnEntryTextChanged(object sender, TextChangedEventArgs args)
    {
        double result;
        bool isValid = double.TryParse (args.NewTextValue, out result);
        ((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
    }
}


ㅁ Reusable Behaviors : 재사용 가능한 비헤이비어
ㄴ해당 요소에 효과등의 추가적인 기능을 비헤이비어를 통해 적용해 재사용이 가능합니다.
ㄴ요소자체에 요소의 특정 이벤트가 발생하면 뷰모델의 커맨드를 바로 바인딩하고 커맨드 매개변수를 전달할수 있는 요소의 커맨드 확장 기능을 만들고 재사용이 가능합니다.

ㄴhttps://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/behaviors/reusable/


public class EffectBehavior : Behavior<View>
{
  ...
  void AddEffect (View view)
  {
    var effect = GetEffect ();
    if (effect != null) {
      view.Effects.Add (GetEffect ());
    }
  }

  void RemoveEffect (View view)
  {
    var effect = GetEffect ();
    if (effect != null) {
      view.Effects.Remove (GetEffect ());
    }
  }

  Effect GetEffect ()
  {
    if (!string.IsNullOrWhiteSpace (Group) && !string.IsNullOrWhiteSpace (Name)) {
      return Effect.Resolve (string.Format ("{0}.{1}", Group, Name));
    }
    return null;
  }
}


public class EventToCommandBehavior : BehaviorBase<View>
{
  public static readonly BindableProperty EventNameProperty =
    BindableProperty.Create ("EventName", typeof(string), typeof(EventToCommandBehavior), null, propertyChanged: OnEventNameChanged);
  public static readonly BindableProperty CommandProperty =
    BindableProperty.Create ("Command", typeof(ICommand), typeof(EventToCommandBehavior), null);
  public static readonly BindableProperty CommandParameterProperty =
    BindableProperty.Create ("CommandParameter", typeof(object), typeof(EventToCommandBehavior), null);
  public static readonly BindableProperty InputConverterProperty =
    BindableProperty.Create ("Converter", typeof(IValueConverter), typeof(EventToCommandBehavior), null);


  public string EventName { ... }
  public ICommand Command { ... }
  public object CommandParameter { ... }
  public IValueConverter Converter { ...  }
  ...
}


지금 까지 XAML 3총사를 기준으로 XAML 기반의 뷰에 대한 기능을 확장하는 주요 방법들에 대해 알아보았습니다.
특히 Behavior 사용을 잘하시면 코드 비하인드 없는 깔끔한 MVVM 기반 APP개발이 가능하오니 이점 유념해주시고 멋진 MVVM 프로젝트를 한번 진행해보시기 바랍니다.
감사합니다.








Comments

  1. 지현명

    올려주신 강의 첫번째거부터 잘 따라왔는데 Convert부분 코드 입력하니 에러는 안나는데 화면에 화이트 스크린 나오면서 진행이 안됩니다.
    DatetimeToStringConverter 하신 부분에 Converter={StaticResource datetimeToStringConverter} "datetimeToStringConverter" 요거가 빠져 있네요.

    xaml하면서 디버깅도 안되고 에러나도 잡히지도 않고 편하게 비하인드 코드로 할까 하다가도 포기하기 싫어서
    여기 강의는 끝까지 따라가고 싶었는데 좌절이네요 ㅠㅠ

    혹시 이거 Full소스 받을 수 있을까요? Convert하고 Behavior 2개 남았는데
    제 메일 주소 입니다. gwise@naver.com

    감사합니다.

  2. 믹스드코드

    안녕하세요. 지현명님
    개발 샘플을 올려드리던지 했어야 했는데 불편을 드려 죄송합니다. ^^
    말씀하신 대로 해당 폴더명으로 네임스페이스 참조가 이루어져 합니다.
    "xmlns:trigger="clr-namespace:XamarinMVVMDemo.Trigger" => "xmlns:trigger="clr-namespace:XamarinMVVMDemo.Triggers"
    많은 분들이 고생하셨을듯한데 다시 한번 소스점검후 샘플을 첨부토록 하겠습니다.
    댓글 주셔서 정말 감사드립니다.

  3. 지현명

    좋은글 감사합니다. 그러나 오타
    1.3 Event Trigger 설명하실때 "PCL프로젝트에 Triggers폴더를 하나 생성합니다." Triggers 폴더라고 하고
    밑에는 "xmlns:trigger="clr-namespace:XamarinMVVMDemo.Trigger"로 되어 있어서 왜 안되는지 한참을 찾았습니다.
    자마린 폼즈는 왜 이럴때 에러라고 표시가 안될까요?

    Triggers 폴더명이 맞다는 전제하에 xmlns:trigger="clr-namespace:XamarinMVVMDemo.Triggers 로 해야 정상으로 동작합니다.

  4. 강창훈

    안녕하세요. 김은지님.
    한동안 노느라 댓글을 이제야 확인했네요. 죄송하구요.

    문의주신 내용은 txtUserID라는 사용자아이디 입력 엔트리 박스의 고유 아이디값이 해당 컨틀롤에 지정되어야 하는데 지정이 안되어 있어 그렇지 않을지요?

    지정 방법은 해당 엔트리 컨트롤에
    x:Name="txtUserID 요렇게 하시면 됩니다.

  5. 김은지

    안녕하세요! 1.2 DataTrigger에 Regist 버튼 비활성화 하는 부분을 실습했는데 왜 잘 동작하지 않을까요? ㅠㅠ 조언 부탁 드립니다

  6. 임영택

    좋은글 감사합니다!

Leave a Reply

*