레이블이 xamarin.forms인 게시물을 표시합니다. 모든 게시물 표시
레이블이 xamarin.forms인 게시물을 표시합니다. 모든 게시물 표시

2018년 2월 1일 목요일

Xamarin.Forms app을 Google Play store에 올리기

초간단 Xamarin.forms app을 만들어 Google Play store에 올림.

https://github.com/hallower/WhooingNewsReader
https://play.google.com/store/apps/details?id=kr.blogspot.charlie0301.NewsReader

리스트뷰만 있는 앱이다 보니 생각보다 Android에서 돌아가는게 느리진 않고
C# 으로 만들앱이다 보니 Google Play store에 올리는데 문제가 있진 않을까 했는데 생각보다 잘 올라가서 신기함.

일단 Google Play store에 올리는 방법
https://developer.xamarin.com/guides/android/deployment,_testing,_and_metrics/publishing_an_application/

Apple App Store에 올리는 방법
https://developer.xamarin.com/guides/ios/deployment,_testing,_and_metrics/app_distribution/app-store-distribution/publishing_to_the_app_store/


Google Play Store에 올리는 방법은 다음과 같이 나눠져 있고
사실 간단한 앱은 Part 1-3까지만 진행하면 등록이 된다.


위 Part 1,2,3를 따라하면서 큰 문제점은 없었고
워낙 잘 정리가 되어 있어서 따라하기만 하면 되지만!

- 모든 절차를 따라해야 나중에 APK를 업로드할 때 문제가 없다.
 : APK 업로드 시 문제점이 무엇인지 알려주므로 문제점이 있다면 Guide를 참고하여 수정 후 다시 수행하면 된다.

- 처음 업로드, 앱 등록 시 Visual Studio에서 자동 처리가 되지 않는다.
 : 처음에는 수동으로 APK를 signing한 뒤 play store에 업로드 해야 한다.

- APK 업로드 시 Signing된 APK를 업로드 해야 한다.
 : 아래 예시 그림에서 signed-apks 폴더 내의 APK를 업로드 해야 한다. 아래 그림에 보이는 MyApp.MyApp.apk는 아니다.
 : 당연한 거겠지만 signing 할 때는 Google Play에 등록된 upload 인증서를 사용해서 signing해야 한다.
  . 참고 http://charlie0301.blogspot.kr/2018/01/google-play-app-signing.html


2017년 1월 12일 목요일

Xamarin.Forms DependencyService

Xamarin.Forms를 사용하다보면
Xamarin.Form에서 제공하지 않는 Platform 특화 기능들을 사용해야 할 필요가 있는데
이때 필수적으로 사용해야 하는 것이 Dependency Service임.

Cross Platform App을 만들기 위해
Shared Project 구조를 가진 Xamarin.Forms 프로젝트에서는
어쩔 수 없이 Platform project의 기능을 호출해야 하지만 이는 Shared code project에서 Platform project로의 strong dependency를 가지는 것이라 바람직하지 않음.
그래서 Dependency Service을 이용하면 다소 불편하기 하지만 언급된 문제를 회피하여 필요한 Platform dependent한 기능을 사용할 수 있음.

아래는 Xamarin documentation URL임.

Accessing Native Features with DependencyService
: https://developer.xamarin.com/guides/xamarin-forms/dependency-service/


간단히 보자면 DependencyService를 사용하기 위해서는 다음의 사항들이 필요하다.


DependencyService를 사용하는 Shared code project app 구조는 대충 이런 느낌이다.

App 만들 때 가장 기본적인 Device Orientation을 조회하는 기능을 DependencyService를 사용해서 해결하는 예제를 보면(... 이런것도 Xamarin.Forms에 없다는 것은 뭐.. 그냥.. )

Checking Device Orientation



구조는 대충 이런 느낌이다.

먼저 Shared code project에서 아래와 같이 Interface를 정의한다.
namespace DependencyServiceSample.Abstractions
{
    public enum DeviceOrientations
    {
        Undefined,
        Landscape,
        Portrait
    }

    public interface IDeviceOrientation
    {
        DeviceOrientations GetOrientation();
    }
}

Platform project에서 정의된 IDeviceOrientation에 맞춰 Class를 구현한다.
단 default 생성자 작성을 잊지 말자. default 생성자가 구현되어 있지 않다면 dependency service로 호출 시 exception이 발생한다.

using DependencyServiceSample.Droid;
using Android.Hardware;

namespace DependencyServiceSample.Droid
{
    public class DeviceOrientationImplementation : IDeviceOrientation
    {
        public DeviceOrientationImplementation() { }

        public static void Init() { }

        public DeviceOrientations GetOrientation()
        {
            IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();

            var rotation = windowManager.DefaultDisplay.Rotation;
            bool isLandscape = rotation == SurfaceOrientation.Rotation90 || rotation == SurfaceOrientation.Rotation270;
            return isLandscape ? DeviceOrientations.Landscape : DeviceOrientations.Portrait;
        }
    }
}

DependencyService에 IDeviceOrientation를 구현한 DeviceOrientationImplementation class를 dependency service에 등록을 위해 metadata를 선언하고 Xamarin.Forms의 dependency service의 register를 호출하여 등록한다.

using DependencyServiceSample.Droid; //enables registration outside of namespace
using Android.Hardware;

[assembly: Xamarin.Forms.Dependency (typeof (DeviceOrientationImplementation))]
namespace DependencyServiceSample.Droid {
    ...
Xamarin.Forms.Forms.Init(e, assembliesToInclude);
// register the dependencies in the same
Xamarin.Forms.DependencyService.Register<TextToSpeechImplementation>();

그럼 Shared code project에서 아래와 같이 사용 가능하다. 끝..

public MainPage ()
{
    var orient = new Button {
        Text = "Get Orientation",
        VerticalOptions = LayoutOptions.CenterAndExpand,
        HorizontalOptions = LayoutOptions.CenterAndExpand,
    };
    orient.Clicked += (sender, e) => {
       var orientation = DependencyService.Get<IDeviceOrientation>().GetOrientation();
       switch(orientation){
           case DeviceOrientations.Undefined:
                orient.Text = "Undefined";
                break;
           case DeviceOrientations.Landscape:
                orient.Text = "Landscape";
                break;
           case DeviceOrientations.Portrait:
                orient.Text = "Portrait";
                break;
       }
    };
    Content = orient;
}

2016년 12월 11일 일요일

Xamarin Android 개발 시 에러(Method 'Android.Support.V4.Widget.DrawerLayout.AddDrawerListener' not found.) 발생 시

Xamarin.form으로 Android app 개발 시 아래 exception이 발생 되면

 System.MissingMethodException: Method 'Android.Support.V4.Widget.DrawerLayout.AddDrawerListener' not found.

1. Nuget manager에서 업데이트가 필요한 package가 있다면 업데이트

모두 업데이트 해보고 clean & build 후 설치된 기존 앱을 지우고 다시 설치해보고
excpetion이 다시 발생한다면 2번 방법을 사용

2. 이미 모두 업데이트 되어 있다면 아래 링크의 방법을 사용해서 특정 버전으로 설치

: https://forums.xamarin.com/discussion/77924/missingmethodexception-android-support-v4-widget-drawerlayout-adddrawerlistener-not-found

임시 방편인것 같지만 일단 사용하니 정상 동작함. 답변만 첨부하자면


JamesMontemagnoJamesMontemagnoJames Montemagno ANSWER ✓
So I just reproduced your issue and I am sorry that you and other ran into this. It looks like there was an issue in the templates and I am working with the team to quickly resolve this.
To fix:
  • Simply right click on the android project and select "Manage Nuget Packages"
  • Go to Updates
  • Find Xamarin.Android.Support.v7.AppCompat
  • Select version 23.3.0 from the version picker and hit Install
  • You additionally will need to install the RecyclerView 23.3.0 NuGet Package. Go to Browse and search for: Xamarin.Android.Support.v7.RecyclerView and make sure to pick 23.3.0 to install for the correct version
  • Now do a full clean/rebuild and it should work
  • This should install the correct versions of everything you need for Android
 
3. 설치된 앱을 삭제하고 재 설치

답변에서 빠진게 있었는데 기존 설치되었다면 해당 app을 지우고 다시 설치해야 한다.
안그러면 다른 exception이 발생된다.


2016년 11월 1일 화요일

Xamarin.forms 사용 중 ViewModel에서 page navigation이 필요한 경우

Xamarin.forms를 사용 하여 MVVM pattern을 구현하다보면
View가 아닌 ViewModel에서 event 처리를 하면서 page navigation이 필요할 때가 있음.

하지만 Navigation 객체에는 View에 있으므로
View의 INavigation type 객체를 ViewModel로 넘겨서 이를 사용하기 하는 방법이 필요.

이렇게 되면 XAML에서 BindingContext로 ViewModel을 지정하는 것이 아니라
Code 상에서 BindingContext를 지정하면서 인자로 Navigation 객체를 넘겨야 한다.

자세한 것은 아래 링크 참고


http://www.johankarlsson.net/2014/09/navigation-from-viewmodel-using.html

 public class MainPageViewModel : INotifyPropertyChanged
    {
        private PersonRepository _repository;
        private INavigation _navigation; // HERE

        public MainPageViewModel (INavigation navigation) // HERE
        {
            _navigation = navigation; // AND HERE

            // This should be injected
            _repository = new PersonRepository();

            // Populate the ViewModel
            var person = _repository.GetPerson(42);
            Name = person.Name;
            Updated = person.Updated;
        }
    }

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 ()
{
  ...
}