5장.Part 5. 데이터 바인딩부터 MVVM까지From Data Bindings to MVVM

샘플 다운로드 샘플 다운로드Download Sample Download the sample

MVVM(Model-View-ViewModel) 아키텍처 패턴은 XAML을 염두에 두고 고안되었습니다. 해당 패턴은 세 가지 소프트웨어 계층(뷰라고 하는 XAML 사용자 인터페이스, 모델이라고 하는 기본 데이터, ViewModel이라고 하는 뷰와 모델 간의 중개자)으로 구분합니다. 종종 뷰와 ViewModel은 XAML 파일에 정의된 데이터 바인딩을 통해 연결됩니다. 뷰의 BindingContext는 일반적으로 ViewModel의 인스턴스입니다.The Model-View-ViewModel (MVVM) architectural pattern was invented with XAML in mind. The pattern enforces a separation between three software layers — the XAML user interface, called the View; the underlying data, called the Model; and an intermediary between the View and the Model, called the ViewModel. The View and the ViewModel are often connected through data bindings defined in the XAML file. The BindingContext for the View is usually an instance of the ViewModel.

간단한 ViewModelA Simple ViewModel

ViewModel에 대한 소개로 ViewModel이 없는 프로그램을 먼저 살펴 보겠습니다.As an introduction to ViewModels, let’s first look at a program without one. 이전에 XAML 파일이 다른 어셈블리의 클래스를 참조할 수 있도록 새 XML 네임 스페이스 선언을 정의하는 방법을 살펴 보았습니다.Earlier you saw how to define a new XML namespace declaration to allow a XAML file to reference classes in other assemblies. 다음은 System 네임 스페이스에 대한 XML 네임 스페이스 선언을 정의하는 프로그램입니다.Here’s a program that defines an XML namespace declaration for the System namespace:

xmlns:sys="clr-namespace:System;assembly=mscorlib"

해당 프로그램은 다음과 같이 x:Static을 사용하여 정적 DateTime.Now 속성에서 현재 날짜 및 시간을 얻고 DateTime 값을 StackLayoutBindingContext로 설정합니다.The program can use x:Static to obtain the current date and time from the static DateTime.Now property and set that DateTime value to the BindingContext on a StackLayout:

<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>

BindingContext 매우 특별 한 속성이입니다. 설정 하는 경우는 BindingContext 해당 요소의 모든 자식에 의해 상속 된 요소에 있습니다.BindingContext is a very special property: When you set the BindingContext on an element, it is inherited by all the children of that element. 즉, StackLayout의 모든 자식은 해당 BindingContext와 동일한 것을 가지며, 여기에는 해당 개체의 속성에 대한 간단한 바인딩이 포함될 수 있습니다.This means that all the children of the StackLayout have this same BindingContext, and they can contain simple bindings to properties of that object.

One-Shot DateTime 프로그램에서 두 개의 자식은 DateTime 값의 속성에 바인딩을 포함하고 있지만, 다른 두 자식은 바인딩 경로가 없는 것으로 보입니다.In the One-Shot DateTime program, two of the children contain bindings to properties of that DateTime value, but two other children contain bindings that seem to be missing a binding path. 이는 다음과 같이 DateTime 값 자체가 StringFormat에 사용됨을 의미합니다.This means that the DateTime value itself is used for the StringFormat:

<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>

물론, 큰 문제는 페이지가 처음 만들어지면 날짜 및 시간이 한번 설정되고 결코 변경되지 않는다는 것입니다.Of course, the big problem is that the date and time are set once when the page is first built, and never change:

XAML 파일은 항상 현재 시간을 나타내는 시계를 표시할 수 있지만 이를 도와주는 코드가 필요합니다. MVVM의 관점에서 보면, 모델 및 ViewModel은 완전한 코드로 작성된 클래스입니다.A XAML file can display a clock that always shows the current time, but it needs some code to help out. When thinking in terms of MVVM, the Model and ViewModel are classes written entirely in code. 뷰는 종종 데이터 바인딩을 통해 뷰모델에 정의된 속성을 참조하는 XAML 파일입니다.The View is often a XAML file that references properties defined in the ViewModel through data bindings.

적절한 모델은 ViewModel을 모르고 있으며, 적절한 ViewModel은 뷰를 모르고 있습니다.A proper Model is ignorant of the ViewModel, and a proper ViewModel is ignorant of the View. 그러나 프로그래머는 매우 빈번하게 ViewModel에 노출된 데이터 유형을 특정 사용자 인터페이스와 연관되는 데이터 유형에 맞게 조정합니다.However, very often a programmer tailors the data types exposed by the ViewModel to the data types associated with particular user interfaces. 예를 들어, 모델이 8 비트 문자인 ASCII 문자열을 포함하는 데이터베이스에 접근하는 경우 ViewModel은 사용자 인터페이스에서 유니코드를 독점적으로 사용하기 위해 해당 문자열 사이를 유니코드 문자열로 변환해야 합니다.For example, if a Model accesses a database that contains 8-bit character ASCII strings, the ViewModel would need to convert between those strings to Unicode strings to accommodate the exclusive use of Unicode in the user interface.

MVVM의 간단한 예제(여기에 표시된 것처럼)는 종종 모델이 전혀 없는 경우가 많으며, 패턴에는 데이터 바인딩과 연결된 뷰 및 ViewModel만 포함합니다.In simple examples of MVVM (such as those shown here), often there is no Model at all, and the pattern involves just a View and ViewModel linked with data bindings.

다음은 DateTime이라는 단 하나의 속성을 가진 시계에 대한 ViewModel이지만, 매초 DateTime 속성을 업데이트합니다.Here’s a ViewModel for a clock with just a single property named DateTime, but which updates that DateTime property every second:

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 인터페이스를 구현합니다. 즉, 속성 중 하나가 변경될 때마다 클래스가 PropertyChanged 이벤트를 발생시킵니다.ViewModels generally implement the INotifyPropertyChanged interface, which means that the class fires a PropertyChanged event whenever one of its properties changes. Xamarin.Forms의 데이터 바인딩 메커니즘은 해당 PropertyChanged 이벤트에 처리기를 붙여 속성이 변경될 때 이를 알리고 새 값으로 대상을 업데이트합니다.The data binding mechanism in Xamarin.Forms attaches a handler to this PropertyChanged event so it can be notified when a property changes and keep the target updated with the new value.

해당 ViewModel에 기반한 시계는 다음과 같이 간단해질 수 있습니다.A clock based on this ViewModel can be as simple as this:

<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>

ClockViewModel이 속성 요소 태그를 사용하여 LabelBindingContext에 어떻게 설정되는지 주목하십시오.Notice how the ClockViewModel is set to the BindingContext of the Label using property element tags. 또는 Resources 컬렉션에서 ClockViewModel을 인스턴스화하고 StaticResource 태그 확장을 통해 BindingContext로 설정할 수 있습니다.Alternatively, you can instantiate the ClockViewModel in a Resources collection and set it to the BindingContext via a StaticResource markup extension. 또는 코드 숨김 파일은 ViewModel을 인스턴스화할 수 있습니다.Or, the code-behind file can instantiate the ViewModel.

LabelText 속성에 있는 Binding 태그 확장은 DateTime 속성을 형식화합니다.The Binding markup extension on the Text property of the Label formats the DateTime property. 다음과 같이 표시됩니다.Here’s the display:

다음과 같이 마침표로 속성을 구분하여 ViewModel의 DateTime 속성의 개별 속성에 접근할 수도 있습니다.It’s also possible to access individual properties of the DateTime property of the ViewModel by separating the properties with periods:

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

대화형 MVVMInteractive MVVM

MVVM은 기본 데이터 모델을 기반으로하는 대화형 뷰의 양방향 데이터 바인딩을 함께 사용하는 경우가 꽤 자주 있습니다.MVVM is quite often used with two-way data bindings for an interactive view based on an underlying data model.

다음은 Color 값을 Hue, SaturationLuminosity 값으로 변환하는 HslViewModel 클래스입니다.Here’s a class named HslViewModel that converts a Color value into Hue, Saturation, and Luminosity values, and vice versa:

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");

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

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

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Hue, SaturationLuminosity 속성을 변경하면 Color 속성이 바뀌고, Color가 바뀌면 다른 세 가지 속성이 바뀝니다.Changes to the Hue, Saturation, and Luminosity properties cause the Color property to change, and changes to Color causes the other three properties to change. 이는 속성이 실제로 변경되지 않는 한 클래스가 PropertyChanged 이벤트를 호출하지 않는다는 점을 제외하면 무한 루프처럼 보일 수 있습니다.This might seem like an infinite loop, except that the class doesn't invoke the PropertyChanged event unless the property has actually changed. 이로 인해 제어 불가능한 경우 없이 피드백 루프가 끝납니다.This puts an end to the otherwise uncontrollable feedback loop.

다음 XAML 파일은 Color 속성이 ViewModel의 Color 속성에 바인딩된 BoxViewHue, SaturationLuminosity 속성에 바인딩된 세 개의 Slider와 세 개의 Label 뷰를 포함합니다.The following XAML file contains a BoxView whose Color property is bound to the Color property of the ViewModel, and three Slider and three Label views bound to the Hue, Saturation, and Luminosity properties:

<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의 바인딩은 기본값이 OneWay이므로.The binding on each Label is the default OneWay. 값만 표시하면 됩니다.It only needs to display the value. 하지만 각 Slider의 바인딩은 TwoWay이므로.But the binding on each Slider is TwoWay. Slider는 ViewModel에서 초기화할 수 있습니다.This allows the Slider to be initialized from the ViewModel. ViewModel이 인스턴스화될 때, Color 속성은 Aqua로 설정됩니다.Notice that the Color property is set to Aqua when the ViewModel is instantiated. 하지만 Slider를 변경하면 뷰모델의 속성 값을 새로 설정해야 새로운 색상이 계산됩니다.But a change in the Slider also needs to set a new value for the property in the ViewModel, which then calculates a new color.

ViewModel을 사용하여 명령 실행Commanding with ViewModels

대부분의 경우에서 MVVM 패턴은 데이터 항목의 조작에 제한 됩니다. ViewModel에서 뷰 병렬 데이터 개체의 사용자 인터페이스 개체입니다.In many cases, the MVVM pattern is restricted to the manipulation of data items: User-interface objects in the View parallel data objects in the ViewModel.

하지만 뷰에는 ViewModel에서 다양한 작업을 실행하는 단추가 포함되는 경우가 있습니다.However, sometimes the View needs to contain buttons that trigger various actions in the ViewModel. 그러나 ViewModel은 특정 사용자 인터페이스 패러다임을 ViewModel에 묶어주기 때문에 Clicked 처리기를 포함시키면 안됩니다.But the ViewModel must not contain Clicked handlers for the buttons because that would tie the ViewModel to a particular user-interface paradigm.

ViewModel이 특정 사용자 인터페이스 개체와 좀 더 독립되도록 해주지만, ViewModel 내에서 메서드를 호출할 수 있도록 하려면 command 인터페이스가 있어야 합니다.To allow ViewModels to be more independent of particular user interface objects but still allow methods to be called within the ViewModel, a command interface exists. 이 명령의 인터페이스는 Xamarin.Forms의 다음과 같은 요소에서 지원됩니다.This command interface is supported by the following elements in Xamarin.Forms:

  • Button
  • MenuItem
  • ToolbarItem
  • SearchBar
  • TextCell(그리고 ImageCell도 따름)TextCell (and hence also ImageCell)
  • ListView
  • TapGestureRecognizer

SearchBarListView 요소를 제외하고, 해당 요소들은 다음과 같이 두 개의 속성을 정의합니다.With the exception of the SearchBar and ListView element, these elements define two properties:

  • System.Windows.Input.ICommand 유형의 CommandCommand of type System.Windows.Input.ICommand
  • Object 유형의 CommandParameterCommandParameter of type Object

SearchBarSearchCommandSearchCommandParameter 속성을 정의하는 반면 ListViewICommand 유형의 RefreshCommand를 정의합니다.The SearchBar defines SearchCommand and SearchCommandParameter properties, while the ListView defines a RefreshCommand property of type ICommand.

ICommand 인터페이스는 다음과 같이 두 개의 메서드와 하나의 이벤트를 정의합니다.The ICommand interface defines two methods and one event:

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

ViewModel은 ICommand 유형의 속성을 정의할 수 있습니다.The ViewModel can define properties of type ICommand. 그런 다음 해당 속성들을 Button이나 다른 요소의 Command 속성 또는 해당 인터페이스를 구현하는 사용자 지정 뷰에 바인딩할 수 있습니다.You can then bind these properties to the Command property of each Button or other element, or perhaps a custom view that implements this interface. 선택적으로 CommandParameter 속성을 설정하여 해당 ViewModel 속성에 바인딩된 개별 Button 개체(또는 다른 요소)를 식별할 수 있습니다.You can optionally set the CommandParameter property to identify individual Button objects (or other elements) that are bound to this ViewModel property. 내부적으로 Button은 사용자가 Button을 누를 때마다 Execute 메서드를 호출하고 Execute 메서드에 자신의 CommandParameter를 전달합니다.Internally, the Button calls the Execute method whenever the user taps the Button, passing to the Execute method its CommandParameter.

CanExecute 메서드 및 CanExecuteChanged 이벤트는 Button을 탭하는 현재의 상황이 무효가 될 수 있는 경우에 사용합니다. 이 경우 Button은 스스로 비활성화되어야 합니다.The CanExecute method and CanExecuteChanged event are used for cases where a Button tap might be currently invalid, in which case the Button should disable itself. ButtonCommand 속성이 처음 설정될 때와 CanExecuteChanged 이벤트가 발생할 때마다 CanExecute를 호출합니다.The Button calls CanExecute when the Command property is first set and whenever the CanExecuteChanged event is fired. CanExecutefalse를 반환하면 Button은 스스로 비활성화되고 Execute를 호출하지 않습니다.If CanExecute returns false, the Button disables itself and doesn’t generate Execute calls.

ViewModel에 명령 추가를 돕기 위하여 Xamarin.Forms는 ICommand를 구현하는 두 클래스를 정의하는 데, 이는 CommandExecuteCanExecute에 대한 인수의 유형이 TCommand<T>입니다.For help in adding commanding to your ViewModels, Xamarin.Forms defines two classes that implement ICommand: Command and Command<T> where T is the type of the arguments to Execute and CanExecute. 해당 두 클래스는 여러 생성자와 ViewModel이 CanExecuteChanged 이벤트를 발생시키도록 Command 개체를 강제로 호출하는 ChangeCanExecute 메서드의 연결을 정의합니다.These two classes define several constructors plus a ChangeCanExecute method that the ViewModel can call to force the Command object to fire the CanExecuteChanged event.

다음은 전화 번호를 입력하기 위한 간단한 키패드 용 ViewModel입니다.Here is a ViewModel for a simple keypad that is intended for entering telephone numbers. 다음과 같이 ExecuteCanExecute 메서드는 생성자에서 람다 함수로 오른쪽에 정의됩니다.Notice that the Execute and CanExecute method are defined as lambda functions right in the constructor:

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()
        {
            AddCharCommand = new Command<string>((key) =>
                {
                    // Add the key to the input string.
                    InputString += key;
                });

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

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

                    // Perhaps the delete button must be enabled/disabled.
                    ((Command)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)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

해당 ViewModel은 AddCharCommand 속성이 CommandParameter에 의해 구별되는 여러 단추(또는 명령 인터페이스가 있는 다른 항목)의 Command 속성에 바인딩되어 있다고 가정합니다.This ViewModel assumes that the AddCharCommand property is bound to the Command property of several buttons (or anything else that has a command interface), each of which is identified by the CommandParameter. 해당 단추는 InputString 속성에 DisplayText 속성의 전화번호 형식 문자를 추가합니다.These buttons add characters to an InputString property, which is then formatted as a phone number for the DisplayText property.

또한 DeleteCharCommand라는 ICommand 유형의 두 번째 속성이 있습니다.There is also a second property of type ICommand named DeleteCharCommand. 해당 단추는 백 스페이스 단추에 바인딩되어 있지만 삭제하려는 문자가 없는 경우 단추를 비활성화해야 합니다.This is bound to a back-spacing button, but the button should be disabled if there are no characters to delete.

다음 키패드는 시각적으로 정교하지 않습니다.The following keypad is not as visually sophisticated as it could be. 대신, 명령 인터페이스 사용을 보다 명확하게 보여주기 위해 태그를 최소한으로 줄였습니다.Instead, the markup has been reduced to a minimum to demonstrate more clearly the use of the command interface:

<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="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </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="&#x21E6;"
                    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>

해당 태그에 나타나는 첫 번째 ButtonCommand 속성은 DeleteCharCommand에 바인딩되어 있습니다. 나머지는 AddCharCommandButton 표면에 표시되는 문자와 동일한 CommandParameter를 가지고 있습니다.The Command property of the first Button that appears in this markup is bound to the DeleteCharCommand; the rest are bound to the AddCharCommand with a CommandParameter that is the same as the character that appears on the Button face. 동작 중인 프로그램은 다음과 같습니다.Here’s the program in action:

비동기 메서드 호출Invoking Asynchronous Methods

명령은 비동기 메서드 또한 호출할 수 있습니다.Commands can also invoke asynchronous methods. 이를 위해 다음과 같이 Execute 메서드를 지정할 때 asyncawait 키워드를 사용하면 됩니다.This is achieved by using the async and await keywords when specifying the Execute method:

DownloadCommand = new Command (async () => await DownloadAsync ());

이는 다음과 같이 DownloadAsync 메서드가 Task이고 기다려야(await) 한다는 것을 의미합니다.This indicates that the DownloadAsync method is a Task and should be awaited:

async Task DownloadAsync ()
{
    await Task.Run (() => Download ());
}

void Download ()
{
    ...
}

탐색 메뉴 구현Implementing a Navigation Menu

해당 시리즈 글의 모든 소스 코드가 들어있는 XamlSamples 프로그램은 해당 홈 페이지에 대한 ViewModel을 사용합니다.The XamlSamples program that contains all the source code in this series of articles uses a ViewModel for its home page. 해당 ViewModel은 샘플 페이지, 제목 및 간단한 설명의 유형을 포함하는 Type, TitleDescription이라는 세 가지 속성을 가진 짧은 클래스 정의입니다.This ViewModel is a definition of a short class with three properties named Type, Title, and Description that contain the type of each of the sample pages, a title, and a short description. 또한 ViewModel은 프로그램의 모든 페이지 컬렉션인 All이라는 정적 속성을 정의합니다.In addition, the ViewModel defines a static property named All that is a collection of all the pages in the program:

public class PageDataViewModel
{
    public PageDataViewModel(Type type, string title, string description)
    {
        Type = type;
        Title = title;
        Description = description;
    }

    public Type Type { private set; get; }

    public string Title { private set; get; }

    public string Description { private set; get; }

    static PageDataViewModel()
    {
        All = new List<PageDataViewModel>
        {
            // Part 1. Getting Started with XAML
            new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
                                  "Display a Label with many properties set"),

            new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
                                  "Interact with a Slider and Button"),

            // Part 2. Essential XAML Syntax
            new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
                                  "Explore XAML syntax with the Grid"),

            new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
                                  "Explore XAML syntax with AbsoluteLayout"),

            // Part 3. XAML Markup Extensions
            new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
                                  "Using resource dictionaries to share resources"),

            new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
                                  "Using the x:Static markup extensions"),

            new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
                                  "Explore XAML markup extensions"),

            // Part 4. Data Binding Basics
            new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
                                  "Bind properties of two views on the page"),

            new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
                                  "Use Sliders with reverse bindings"),

            new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
                                  "Use a ListView with data bindings"),

            // Part 5. From Data Bindings to MVVM
            new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
                                  "Obtain the current DateTime and display it"),

            new PageDataViewModel(typeof(ClockPage), "Clock",
                                  "Dynamically display the current time"),

            new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
                                  "Use a view model to select HSL colors"),

            new PageDataViewModel(typeof(KeypadPage), "Keypad",
                                  "Use a view model for numeric keypad logic")
        };
    }

    public static IList<PageDataViewModel> All { private set; get; }
}

MainPage에 대한 XAML 파일은 다음과 같이 ItemsSource 속성이 All 속성으로 설정되고 각 페이지의 TitleDescription 속성을 표시하는 TextCell을 포함하는 ListBox입니다.The XAML file for MainPage defines a ListBox whose ItemsSource property is set to that All property and which contains a TextCell for displaying the Title and Description properties of each page:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.MainPage"
             Padding="5, 0"
             Title="XAML Samples">

    <ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
              ItemSelected="OnListViewItemSelected">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Title}"
                          Detail="{Binding Description}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

다음과 같이 페이지는 스크롤 가능한 목록으로 표시됩니다.The pages are shown in a scrollable list:

코드 숨김 파일에서 처리기(handler)는 사용자가 항목을 선택할 때 실행됩니다.The handler in the code-behind file is triggered when the user selects an item. 처리기는 다음과 같이 ListBoxSelectedItem 속성을 null로 설정한 다음 선택한 페이지를 인스턴스화하고 해당 페이지로 이동합니다.The handler sets the SelectedItem property of the ListBox back to null and then instantiates the selected page and navigates to it:

private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
    (sender as ListView).SelectedItem = null;

    if (args.SelectedItem != null)
    {
        PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
        Page page = (Page)Activator.CreateInstance(pageData.Type);
        await Navigation.PushAsync(page);
    }
}

비디오Video

Xamarin Evolve 2016: Xamarin.Forms 및 Prism을 사용 하 여 간단 하 게 MVVMXamarin Evolve 2016: MVVM Made Simple with Xamarin.Forms and Prism

요약Summary

XAML은 특히 데이터 바인딩 및 MVVM을 사용할 때 Xamarin.Forms 응용 프로그램에서 사용자 인터페이스를 정의하는 강력한 도구입니다.XAML is a powerful tool for defining user interfaces in Xamarin.Forms applications, particularly when data-binding and MVVM are used. 결과는 코드에서 모든 배경을 지원하는 사용자 인터페이스를 깔끔하고 세련되며, 잠재적으로 도구화하여 표현할 수 있습니다.The result is a clean, elegant, and potentially toolable representation of a user interface with all the background support in code.