2016년 10월 26일 수요일

Xamarin.Forms XAML Basics Part 5, From Data Bindings to MVVM

Xamarin.Forms에서의 XAML 사용이 궁금해서 아래 링크를 보며 대충 필요한 것만 정리함.
https://developer.xamarin.com/guides/xamarin-forms/xaml/

Bindings to MVVM

: https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_bindings_to_mvvm/

Model-View-ViewModel(MVVM) architectural pattern은 XAML과 함께 정의 되었음.
MVVM pattern은 XAML UI (the View)를 data (the Model)에서 분리하고 중간 매개 역할의 View and Model (the ViewModel)을 통해 접근하는 방법임.
View와 ViewModel은 XAML에 정의된 data binding을 통해서 연결되고 일반적으로 View의 BindingContext는 ViewModel의 instance로 볼 수 있다.

A Simple ViewModel
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_bindings_to_mvvm/#A_Simple_ViewModel

먼저 System namespace를 사용하기 위해 다음과 같이 XML namespace를 정의 함.
xmlns:sys="clr-namespace:System;assembly=mscorlib"
현재 날짜와 시간을 위해 staic DateTime.Now property를 binding한다.
<StackLayout BindingContext="{x:Static sys:DateTime.Now}">
BindingContext는 좀 특별한 property라서 하위 자식들이 상속되어 StackLayout의 하위 자식들이 동일한 BindingContext를 사용할 수 있음.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

  <StackLayout BindingContext="{x:Static sys:DateTime.Now}"
               HorizontalOptions="Center"
               VerticalOptions="Center">

    <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
    <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
    <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
    <Label Text="{Binding StringFormat='The time is {0:T}'}" />

  </StackLayout>
</ContentPage>
MVVM 단어를 생각해 보면 Model과 ViewModel은 class이므로 전반적으로 code로 짜여야 할 것 같아 보이고 View는 주로 XAML 파일로 구성되어 ViewModel의 property를 data-binding으로 참조할 것으로 유추할 수 있다.

잘 작성된 Model은 ViewModel를 모르고 잘 작성된 ViewModel은 View를 알 수 없어야 한다. 하지만 대부분의 개발자들은 ViewMode에서 data type을 노출하고 해당 data type은 UI와 밀접한 관게를 가지도록 구성한다. 예를 들면 Model은 ASCII 문자열을 가지고 있는 Database를 접근하고 ViewModel에서는 이 ASCII string을 UI에서 필요한 Unicode string으로 변환해야하는 상황을 들 수 있다. (??)

아래 예제에서는 Model이 없이 View와 ViewModel간의 data binding을 보여줄 것이다.
using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    class ClockViewModel : INotifyPropertyChanged
    {
        DateTime dateTime;

        public event PropertyChangedEventHandler PropertyChanged;

        public ClockViewModel()
        {
            this.DateTime = DateTime.Now;

            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
                {
                    this.DateTime = DateTime.Now;
                    return true;
                });
        }

        public DateTime DateTime
        {
            set
            {
                if (dateTime != value)
                {
                    dateTime = value;

                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this,
                            new PropertyChangedEventArgs("DateTime"));
                    }
                }
            }
            get
            {
                return dateTime;
            }
        }
    }
}
ViewModel은 일반적으로 INotifyPropertyChanged interface를 상속 받는다. 그리고 class에서는 property가 변경 될 때 마다 PropertyChanged event를 발생 시킨다. 그럼 Xamarin.Forms의 data binding 은 해당 event를 위해 handler를 붙여 변경사항을 추적하여 새로운 값으로 업데이트되도록 한다.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">

  <Label Text="{Binding DateTime,
                        StringFormat='{0:T}'}"
         FontSize="Large"
         HorizontalOptions="Center"
         VerticalOptions="Center">
    <Label.BindingContext>
      <local:ClockViewModel />
    </Label.BindingContext>
  </Label>
</ContentPage>
Lavel에서 BindingContext로 ClockViewModel을 지정하고 있다. 다른 방법으로 ClockViewModel을 Resource collection으로 명시 하여 StaticResource markup extension을 사용하여 BindingContext로 지정할 수 있고 아니면 code-behind file이 VieModel을 정의 할 수 도 있다.


Interactive MVVM
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_bindings_to_mvvm/#Interactive_MVVM

MVVM은 주로 interactive view를 위해 two-way data binding과 함께 사용된다.
using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    public class HslViewModel : INotifyPropertyChanged
    {
        double hue, saturation, luminosity;
        Color color;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Hue
        {
            set
            {
                if (hue != value)
                {
                    hue = value;
                    OnPropertyChanged("Hue");
                    SetNewColor();
                }
            }
            get
            {
                return hue;
            }
        }

        public double Saturation
        {
            set
            {
                if (saturation != value)
                {
                    saturation = value;
                    OnPropertyChanged("Saturation");
                    SetNewColor();
                }
            }
            get
            {
                return saturation;
            }
        }

        public double Luminosity
        {
            set
            {
                if (luminosity != value)
                {
                    luminosity = value;
                    OnPropertyChanged("Luminosity");
                    SetNewColor();
                }
            }
            get
            {
                return luminosity;
            }
        }

        public Color Color
        {
            set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");

                    this.Hue = value.Hue;
                    this.Saturation = value.Saturation;
                    this.Luminosity = value.Luminosity;
                }
            }
            get
            {
                return color;
            }
        }

        void SetNewColor()
        {
            this.Color = Color.FromHsla(this.Hue,
                                        this.Saturation,
                                        this.Luminosity);
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
  <ContentPage.BindingContext>
    <local:HslViewModel Color="Aqua" />
  </ContentPage.BindingContext>

  <StackLayout Padding="10, 0">
    <BoxView Color="{Binding Color}"
             VerticalOptions="FillAndExpand" />

    <Label Text="{Binding Hue,
                      StringFormat='Hue = {0:F2}'}"
           HorizontalOptions="Center" />

    <Slider Value="{Binding Hue, Mode=TwoWay}" />

    <Label Text="{Binding Saturation,
                      StringFormat='Saturation = {0:F2}'}"
           HorizontalOptions="Center" />

    <Slider Value="{Binding Saturation, Mode=TwoWay}" />

    <Label Text="{Binding Luminosity,
                      StringFormat='Luminosity = {0:F2}'}"
           HorizontalOptions="Center" />

    <Slider Value="{Binding Luminosity, Mode=TwoWay}" />
  </StackLayout>
</ContentPage>
Label들은 값을 표시하기 때문에 기본적으로 binding 방법은 OneWay이다. 하지만 Slider는 초기값에 따라 Slider가 설정되어야 하므로 TwoWay binding을 해야 한다.


Commanding with ViewModels
https://developer.xamarin.com/guides/xamarin-forms/xaml/xaml-basics/data_bindings_to_mvvm/#Commanding_with_ViewModels

많은 경우 MVVM pattern은 ViewModel의 View parallel data object이고 UI object인 data item을 직접 조작하는 것을 제한하고 있다.
종종 View는 다양한 동작들을 처리할 수 있는 button을 ViewModel의 button을 필요로할 수 있지만 ViewModel은 특정 UI와 강결합 될 수 있는 Clicked handler들을 포함해서는 안된다.
ViewModel에서 특정 UI object와 독립적이고 ViewModel에서 호출 할 수 있도록 하는 방법은 command interface를 사용하는 것이다. 이 command interface를 지원하는 Xamarin.Forms element들은 다음과 같다.


  • Button
  • MenuItem
  • ToolbarItem
  • SearchBar
  • TextCell (and hence also ImageCell )
  • ListView
  • TapGestureRecognizer

SearchBar, ListView element는 예외적으로 다음 두가지 property를 가진다.


  • Command of type System.Windows.Input.ICommand
  • CommandParameter of type Object

비슷하게 SearchBar는 SearchCommand와 SearchCommandParameter property들을 정의하고 있고 ListView는 ICommand type의 RefershCommand property를 정의하고 있다.

ICommand interface는 다음 두개의 method와 한개의 event를 정의하고 있다.


  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged


ViewModel은 하나 이상의 ICommand type의 property들을 가지고 있다. 이 property들은 각 Button의 Command property에 바인딩되어 있다. CommandParameter property는 부가적으로 ViewModel property에 바인딩된 Button을 확인하기 위해 사용된다. Button은 사용자가 Button을 터치할 때 마다 CommandParameter와 함께 Execute를 호출한다.

CanExecute method와 CanExecuteChanged event는 Button이 사용가능한지 아닌지 확인할 때 사용된다. 초기에 Command property가 설정되고 CanExecuteChanged event가 발생될 때 Button은 CanExecute method를 호출한다.

ViewModel에 command를 추가할 때 ICommand: Command와 Command<T>를 구현해야 한다.These two classes define a bunch of constructors plus a ChangeCanExecute method that the ViewModel can call to force the Command object to fire the CanExecuteChanged event.

다음은 전화번호를 입력하기 위한 simple keypad의 ViewModel이다. 생성자에서 Execute와 CanExecute가 lambda 함수로 정의 된것을 볼 수 있다.
using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;

namespace XamlSamples
{
    class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            this.AddCharCommand = new Command<string>((key) =>
                {
                    // Add the key to the input string.
                    this.InputString += key;
                });

            this.DeleteCharCommand = new Command((nothing) =>
                {
                    // Strip a character from the input string.
                    this.InputString = this.InputString.Substring(0,
                                        this.InputString.Length - 1);
                },
                (nothing) =>
                {
                    // Return true if there's something to delete.
                    return this.InputString.Length > 0;
                });
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                if (inputString != value)
                {
                    inputString = value;
                    OnPropertyChanged("InputString");
                    this.DisplayText = FormatText(inputString);

                    // Perhaps the delete button must be enabled/disabled.
                    ((Command)this.DeleteCharCommand).ChangeCanExecute();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set
            {
                if (displayText != value)
                {
                    displayText = value;
                    OnPropertyChanged("DisplayText");
                }
            }
            get { return displayText; }
        }

        // ICommand implementations
        public ICommand AddCharCommand { protected set; get; }

        public ICommand DeleteCharCommand { protected set; get; }

        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}",
                                          str.Substring(0, 3),
                                          str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}",
                                          str.Substring(0, 3),
                                          str.Substring(3, 3),
                                          str.Substring(6));
            }
            return formatted;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
        }
    }
}

ViewModel은 AddCharCommand property가 여러 버튼의 Command propert로 binding되었고 각각이 CommandParameter로 구분됨을 알 수 있다. 이 버튼들은 displayText property 에 보여지는 전화번호 문자열인 InputString property에 문자를 추가한다.

두번째로 ICommand 형식인 DeleteCharCommand property가 있는데 한 문자씩 지우는 버튼이고 문자열이 없는 경우 disable 되어야 한다.

다음은 keypad를 구성하는 XAML이다.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">

    <Grid HorizontalOptions="Center"
          VerticalOptions="Center">
      <Grid.BindingContext>
        <local:KeypadViewModel />
      </Grid.BindingContext>

      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>

      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>

      <!-- Internal Grid for top row of items -->
      <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Frame Grid.Column="0"
               OutlineColor="Accent">
          <Label Text="{Binding DisplayText}" />
        </Frame>

        <Button Text="⇦"
                Command="{Binding DeleteCharCommand}"
                Grid.Column="1"
                BorderWidth="0" />
      </Grid>

      <Button Text="1"
              Command="{Binding AddCharCommand}"
              CommandParameter="1"
              Grid.Row="1" Grid.Column="0" />

      <Button Text="2"
              Command="{Binding AddCharCommand}"
              CommandParameter="2"
              Grid.Row="1" Grid.Column="1" />

      <Button Text="3"
              Command="{Binding AddCharCommand}"
              CommandParameter="3"
              Grid.Row="1" Grid.Column="2" />

      <Button Text="4"
              Command="{Binding AddCharCommand}"
              CommandParameter="4"
              Grid.Row="2" Grid.Column="0" />

      <Button Text="5"
              Command="{Binding AddCharCommand}"
              CommandParameter="5"
              Grid.Row="2" Grid.Column="1" />

      <Button Text="6"
              Command="{Binding AddCharCommand}"
              CommandParameter="6"
              Grid.Row="2" Grid.Column="2" />

      <Button Text="7"
              Command="{Binding AddCharCommand}"
              CommandParameter="7"
              Grid.Row="3" Grid.Column="0" />

      <Button Text="8"
              Command="{Binding AddCharCommand}"
              CommandParameter="8"
              Grid.Row="3" Grid.Column="1" />

      <Button Text="9"
              Command="{Binding AddCharCommand}"
              CommandParameter="9"
              Grid.Row="3" Grid.Column="2" />

      <Button Text="*"
              Command="{Binding AddCharCommand}"
              CommandParameter="*"
              Grid.Row="4" Grid.Column="0" />

      <Button Text="0"
              Command="{Binding AddCharCommand}"
              CommandParameter="0"
              Grid.Row="4" Grid.Column="1" />

      <Button Text="#"
              Command="{Binding AddCharCommand}"
              CommandParameter="#"
              Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

첫 button의 Command property는 DeleteCharCommand에 바인딩되어 있고 나머지는 AddCharCommand에 각 버튼의 Text와 같은 CommandParameter과 함께 바인딩되어 있다.




Invoking Asynchronous Methods


Command들도 비동기적으로 호출 될 수 있다. 이는 Execute method를 정의할 때 async와 await를 사용해서 처리할 수 있다.
DownloadCommand = new Command (async () => await DownloadAsync ());

이는 DownloadAsync method가 Task이고 method 실행이 일시 정지 될수 있음(await)을 알려준다.
async Task DownloadAsync ()
{
  await Task.Run (() => Download ());
}

void Download ()
{
  ...
}















댓글 없음:

댓글 쓰기