전체 페이지뷰

2017년 3월 15일 수요일

Chapter 6. Button Clicks, part 2

Distinguishing views with IDs


TwoButtons 프로그램에서는 한개의 이벤트 핸들러 내에서 객체를 비교함으로써 뷰를 구별하는 기술을 보았습니다. 그런 방법은 구분할 뷰가 적을 때에는 괜찮지만 계산기 프로그램과 같은 것에 적용하려면 좋은 방법이 아닙니다.

Element 클래스는 뷰를 식별하기 위해 string 유형의 StyleId 속성을 정의합니다. 이 속성은 Xamarin.Forms 내부적으로는 사용되지 않으므로 응용 프로그램에 마음대로 설정할 수 있습니다. 그리고 그 값을 if~else, switch~case, 또는 Parse 메소드 중 어느 것으로도 구분하여 사용할 수 있습니다.

이제 보여드릴 프로그램은 계산기가 아니라 계산기의 일부인 숫자패드 프로그램입니다. SimplestKeypad라는 이 프로그램은 키의 행과 열을 맞추는데 StackLayout을 사용합니다(StackLayout이 이런 용도에 적당치 않음을 보여드릴 목적도 있습니다!).

이 프로그램은 합쳐서 다섯 개의 StackLayout 인스턴스를 생성합니다. mainStack이 수직으로 서 있고 네 개의 수평 StackLayout이 추가로 존재하여 거기에 10개의 키를 배열합니다. 단순하게 하기 위해 keypad는 계산기 키 순서가 아닌 전화기 키 순서로 가정합니다.

public class SimplestKeypadPage : ContentPage
{
    Label displayLabel;
    Button backspaceButton;
    public SimplestKeypadPage()
    {
        // Create a vertical stack for the entire keypad.
        StackLayout mainStack = new StackLayout
        {
            VerticalOptions = LayoutOptions.Center,
            HorizontalOptions = LayoutOptions.Center
        };
        // First row is the Label.
        displayLabel = new Label
        {
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)),
            VerticalOptions = LayoutOptions.Center,
            HorizontalTextAlignment = TextAlignment.End
        };
        mainStack.Children.Add(displayLabel);
        // Second row is the backspace Button.
        backspaceButton = new Button
        {
            Text = "\u21E6",
            FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Button)),
            IsEnabled = false
        };
        backspaceButton.Clicked += OnBackspaceButtonClicked;
        mainStack.Children.Add(backspaceButton);
        // Now do the 10 number keys.
        StackLayout rowStack = null;
        for (int num = 1; num <= 10; num++)
        {
            if ((num - 1) % 3 == 0)
            {
                rowStack = new StackLayout
                {
                    Orientation = StackOrientation.Horizontal
                };
                mainStack.Children.Add(rowStack);
            }
            Button digitButton = new Button
            {
                Text = (num % 10).ToString(),
                FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Button)),
                StyleId = (num % 10).ToString()
            };
            digitButton.Clicked += OnDigitButtonClicked;
            // For the zero button, expand to fill horizontally.
            if (num == 10)
            {
                digitButton.HorizontalOptions = LayoutOptions.FillAndExpand;
            }
            rowStack.Children.Add(digitButton);
        }
        this.Content = mainStack;
    }
    void OnDigitButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        displayLabel.Text += (string)button.StyleId;
        backspaceButton.IsEnabled = true;
    }
    void OnBackspaceButtonClicked(object sender, EventArgs args)
    {
        string text = displayLabel.Text;
        displayLabel.Text = text.Substring(0, text.Length - 1);
        backspaceButton.IsEnabled = displayLabel.Text.Length > 0;
    }
}
cs

10개의 키가 하나의 Clicked 핸들러를 공유합니다. StyleId 속성은 키와 관련된 번호를 나타내므로 프로그램은 해당 숫자를 레이블에 표시된 문자열에 간단하게 추가 할 수 있습니다. 이 경우 StyleIdButtonText 속성과 동일하고 대신 Text 속성을 사용할 수 있지만 일반적인 경우에 언제나 편리한 것은 아닙니다.

Backspace 버튼의 핸들러는 따로 분리되어 있습니다(합치는 것도 가능하지만 기능이 다르므로 분리합니다).

키패드의 FontSize는 모두 NamedSize.Large로 지정되어 크기를 약간 크게 했습니다.
아래는 3개의 플랫폼에서 렌더링된 화면입니다.


버튼을 연타하여 숫자가 커지면 어떻게 되나 알아보고 싶을지도 모르겠습니다만 그렇게 한다면 기대 밖의 일이 벌어질 것입니다. 숫자가 점점 많아져서 라벨의 폭을 벗어나면 수직 StackLayout이 커지고 버튼도 밀려내려갈 것입니다.

또한 버튼에 숫자가 아닌 문자 또는 기호가 포함되어 있으면 각 단추 너비가 내용에 따라 달라지므로 단추가 정렬되지 않습니다.

이 문제는 HorizontalOptionsExpands 플래그로 해결할 수 있지 않을까요? 답은 아니요 입니다. Expands 플래그는 StackLayout의 뷰 간에 존재하는 여분의 공간을 균등하게 나눠줍니다. 각각의 뷰는 같은 양만큼 증가하겠지만 버튼은 다른 폭으로 시작하므로 언제나 다른 크기를 갖게 됩니다. 예를 들어 앞에서 작성한 TwoButtonsButtonLambdas 프로그램의 버튼을 보시기 바랍니다. 두 버튼의 HorizontalOptions 속성은 모두 FillAndExpand이지만 버튼의 콘텐트가 다르므로 폭이 다릅니다.

이 문제의 해결책은 17장에서 살펴 보게될 Grid라는 layout입니다.

Saving transient data

SimplestKeypad 프로그램으로 중요한 숫자를 치고 있다가 전화가 걸려와서 작업을 방해 받았다고 가정해 봅시다. 이후에 전화를 끊고 프로그램을 종료했습니다.

다음번에 다시 SimplestKeypad 프로그램을 작동하면 어떻게 될까요? 이전에 입력했던 긴 숫자는 지워질까요? 아니면 마지막으로 프로그램을 떠난 상태로 재시작될까요? 물론 이런 간단한 프로그램에서는 그런 것이 중요치 않지만 사용자들은 일반적으로 마지막 떠난 상태를 프로그램이 기억하고 있기를 기대합니다.

이러한 이유로 Application 클래스는 프로그램이 데이터를 저장하고 복원하는데 도움이되는 두 가지 기능을 지원합니다.


  • ApplicationProperties 속성 : string keyobject item으로 이루어진 dictionary. 이 dictionary는 프로그램이 종료되기 전에 자동 저장되고, 다음 번 그 앱이 작동할 때 사용 가능하게 됩니다.
  • Application 클래스는 세 개의 protected virtual 메소드인 OnStart, OnSleep, OnResume를 정의하고, Xamarin.Forms 템플릿에 의해 만들어지는 App 클래스는 이 메소드들을 오버라이드 합니다. 이 메소드들이 application lifecycle 이벤트를 다룰수 있게 해 줍니다.

이 기능을 사용하려면 응용 프로그램을 재시작 시에 상태를 복원할 수 있도록 어떤 정보를 저장해야 할지를 구분해야 합니다. 일반적으로 이것은 application settings(색상과 글꼴처럼 사용자가 설정한)와 transient data(절반만 입력된 입력란 같은)의 조합입니다. 응용 프로그램 설정은 대개 전체 application에 적용되는 반면 transient data는 응용 프로그램의 각 페이지마다 고유합니다. 이 data의 각 아이템은 Properties dictionary에 들어있다고 하면 각 아이템은 key를 필요로 합니다. 그러나 프로그램이 워드문서와 같이 큰 파일을 저장해야 한다면 Properties 딕셔너리를 이용해서는 안되며 플랫폼의 파일 시스템에 직접 접근해야 합니다(20장 Async and file I/O에서 다룹니다).

또한 Properties에서 사용하는 데이터 형식은 .NET, C#에서 지원하는 기본 데이터 형식 (예 : string, int double)으로 제한해야 합니다.

SimplestKeypad 프로그램은 transient data 단일 항목만 저장해야 하며 dictionary 키는 "displayLabelText"로 하는 것이 합리적입니다.

때로 application lifecycle과 관계없이 properties 딕셔너리에 접근하여 정보를 저장하거나 찾아야 할 수도 있습니다. 예를 들어, SimplestKeypad 프로그램은 displayLabelText 속성이 언제 변경되는지 정확하게 알고 있습니다. 그것은 숫자를 삭제하거나 입력하는  두개의 Clicked 이벤트가 발생할 때인데, 이 때 이 두 개의 이벤트 핸들러는 Properties 딕셔너리에 새 값을 저장할 수 있을 것입니다.

여기서 잠깐: PropertiesApplication 클래스의 프로퍼티입니다. 그렇다면 SimplestKeypadPage의 코드가 딕셔너리에 액세스 할 수 있도록 App 클래스의 인스턴스를 저장해야 합니까? 그럴 필요는 없습니다. ApplicationCurrent라는 static 프로퍼티를 정의하여 현재 앱의 Application 클래스의 인스턴스를 반환합니다.

LabelText 속성을 딕셔너리에 저장하려면 SimplestKeypad의 두 Clicked 이벤트 핸들러 아래쪽에 다음 줄을 추가하기만 하면 됩니다.

Application.Current.Properties["displayLabelText"= displayLabel.Text;
cs

displayLabelText 키가 딕셔너리에 아직 없는데 어쩌나 하는 걱정은 마시기 바랍니다. Properties 딕셔너리는  generic IDictionary 인터페이스를 구현하여, 키가 이미있는 경우 이전 항목을 대체할 인덱서를 정의하고, 키가 없는 경우 사전에 새 항목을 추가합니다.

그런 다음 SimplestKeypadPage 생성자는 딕셔너리의 항목을 검색하는 다음 코드를 사용하여 LabelText 속성을 초기화 합니다.

IDictionary<stringobject> properties = Application.Current.Properties;
if (properties.ContainsKey("displayTextLabel"))
{
    displayLabel.Text = properties["displayLabelText"as string;
    backspaceButton.IsEnabled = displayLabel.Text.Length > 0;
}
cs

여러분의 앱은 이렇게 Properties 딕셔너리를 저장하고 검색하기만 하면 됩니다. 그러면 Xamarin.Forms가 각 플랫폼별 application storage에 딕셔너리의 내용을 저장하고 로딩하는 것을 맡아줄 겁니다.

그러나 좀 더 세련되고 체계적으로 Properties 딕셔너리와 정보를 주고받으려면 앱의 라이프 사이클에 맞추는 편이 좋을 것입니다. Xamarin.Forms의 템플릿이 자동 생성 해주는 App 클래스 내부에 있는 다음의 메소드 세 개를 보시기 바랍니다.

public class App : Application
{
    public App()
    {            
        ...
    }
    protected override void OnStart()
    {
        // Handle when your app starts
    }
    protected override void OnSleep()
    {
        // Handle when your app sleeps
    }
    protected override void OnResume()
    {
        // Handle when your app resumes
    }
}
cs


가장 중요한 것은 OnSleep()이 호출될 때입니다. 더 이상 커맨드가 없어서 앱이 inactive-백그라운드에서 작업 중일때는 제외하고-상태가 되면 슬립모드로 접어듭니다. 그런 이후 앱은 종료될 수도 있고 OnResume() 시그널이 있으면 재작동할 수도 있습니다. 그러나 중요한 것은 OnSleep 호출 후에는 응용 프로그램이 종료된다는 알림이 없다는 것입니다. OnSleep은 종료 바로 직전에 호출되므로 종료 알림을 받는 것으로 여겨도 되겠습니다. 예를 들어 앱 실행중에 사용자가 전화기를 끄면, 앱은 OnSleep 호출을 받습니다.

종료 직전에 OnSleep이 호출된다는 법칙에도 예외는 있습니다. 프로그램이 갑자기 죽는 경우입니다. 이 경우에도 OnSleep이 호출된다면 좋겠지만 그런 것을 기대하기는 어려울 것입니다. Xamarin.Forms 프로그램을 디버깅하다가 비주얼 스튜디오 혹은 자마린 스튜디오에서 디버깅을 중지하면 프로그램은 OnSleep 호출없이 종료됩니다. 그러므로 혹 lifecycle을 이용한 앱을 디버깅 해야 한다면 IDE가 아닌 폰 자체를 이용하여 앱을 중지하고 재개하고 종료하는 습관을 들이는 편이 좋겠습니다.

Xamarin.Forms 프로그램이 작동 중일 때, OnSleep을 확실히 콜하는 방법은 전화기의 홈 버튼을 누르는 것입니다. 그 다음 프로그램을 다시 불러와서 OnResume를 호출할 수 있습니다. 프로그램을 다시 불러오는 방법은 홈 메뉴에서 프로그램을 선택하거나(iOS, Android의 경우), Back 버튼을 누르는 것입니다(Android, Windows phone의 경우).

Xamarin.Forms 프로그램이 작동 중일때 앱 변환기를 불러오는 방법은 다음과 같습니다.
Home 버튼 두번 누르기: iOS
MultiTask 버튼 누르기 : Android(구형 안드로이드일 때는 Home 버튼 길게 누르기)
Back 버튼 길게 누르기 : Windows 폰
프로그램이 OnSleep 호출 되었다가, 재선택 되어 실행되면 OnResume 호출이 됩니다.
만약 재선택 하지 않고 프로그램을 종료시킨다면(iOS에서는 앱 이미지를 위로 스와이프, 안드로이드나 윈도우즈 폰에서는 앱 이미지의 우상단 X표를 눌러서 종료할 수 있습니다) 더 이상의 알림 없이 실행 종료될 것입니다.

따라서 기본 법칙은 다음과 같습니다: 앱이 OnSleep을 호출 할 때마다 Properties 딕셔너리에 저장하려는 앱에 대한 모든 정보가 포함되어 있는지 확인해야 합니다.

라이프 사이클 이벤트를 프로그램 데이터를 저장하고 복원하기 위해서만 사용하는 경우에는 OnResume 메서드를 처리할 필요가 없습니다. 프로그램이 OnResume 호출을 받을 때, 운영 체제는 이미 프로그램 내용과 상태를 자동으로 복원한 상태이기 때문입니다. 원한다면 OnResume을 Properties 딕셔너리를 지우는 타이밍으로 사용할 수 있습니다. 프로그램을 종료하기 전에 또 다시 OnSleep 호출을 받을 수 있기 때문입니다. 그러나 프로그램이 웹 서비스와의 연결을 설정했거나 연결 중이었을 경우에는 OnResume을 사용하여 해당 연결의 복원을 시도할 수 있습니다. 프로그램이 비활성 상태에 있었던 기간에 연결 시간이 초과되었을 수 있고 새로운 데이터를 필요로 할 수 있습니다..

Properties 딕셔너리를 이용하여 프로그램 시작 시 데이터를 복원하는 데에는 약간의 유연성을 발휘할 수 있습니다. 자마린 프로그램이 시작되고 PCL에서 코드를 실행할 수 있는 첫번째 기회는 App 클래스 생성자에서 입니다. 그 시점에서 Properties 딕셔너리는 이미 플랫폼별 저장소로부터 데이터를 복구했을 겁니다. 두번째로 실행되는 코드는 App 생성자가 인스턴스화한 앱의 첫 페이지 생성자 내에 있습니다. Application (및 App)의 OnStart 호출이 그 뒤를 따르고,  그 다음 OnAppearing이라는 오버라이드 가능한 메서드가 페이지 클래스에서 호출됩니다. 이 시작 프로세스 중에 언제든지 데이터를 검색할 수 있습니다.

앱에서 저장해야 하는 데이터는 일반적으로 page 클래스에 있지만 OnSleep override는 App 클래스에 있습니다. 따라서 page 클래스와 App 클래스는 서로 정보를 주고 받아야 합니다. 그렇게 하기 위한 한가지 방법은 먼저 page 클래스 내에 OnSleep메소드를 정의하여 Properties 딕셔너리를 저장하고 난 후 AppOnSleep 메소드로부터 page의 OnSleep을 호출하게 하는 것입니다. 이 방법은 single-page 앱에서 유용하게 쓰이는데(Application 클래스에는 MainPage라는 정적 속성이 있어서 OnSleep 메소드가 억세스하는데 사용됩니다) multi-page 앱에서는 잘 통하지 않는 방법입니다.

다음 방법은 App 클래스의 public 프로퍼티로 저장해야할 데이터를 정의해 두는 방법입니다. 예를 들면 다음과 같습니다.
public class App : Application
{
    public App()
    {            
       ...
    }
 
    public string DisplayLabelText { set; get; }
 
}
cs

이렇게 하면 페이지 클래스는 아무때나 해당 프로퍼티를 설정하고 검색할 수 있게 됩니다.
App 클래스는 페이지를 인스턴스화 하기 전에 생성자의 Properties 딕셔너리에서 이 모든 속성을 복원할 수 있으며, OnSleep override에서 저장할 수 있게 해줍니다.

PersistentKeypad라는 프로그램으로 구현해 보겠습니다. SimplestKeypad 프로그램과 거의 동일하지만 키패드의 내용을 저장하고 복원할 수 있다는 차이가 있습니다. 먼저 보여드릴 코드는 PersistentKeypadApp 클래스로서 DisplayLabelText라는 public 프로퍼티를 가지고 있으며, OnSleep override 메소드에서 저장되고 App 생성자에서 로딩되도록 했습니다.

namespace PersistentKeypad
{
    public class App : Application
    {
        const string displayLabelText = "displayLabelText";
 
        public App()
        {         
            if (Properties.ContainsKey(displayLabelText))
            {
                DisplayLabelText = (string)Properties[displayLabelText];
            }   
            MainPage = new PersistentKeypadPage();
        }        
 
        public string DisplayLabelText { get; set; }
 
        protected override void OnStart()
        {
            // Handle when your app starts
        }
 
        protected override void OnSleep()
        {
            // Handle when your app sleeps
            Properties[displayLabelText] = DisplayLabelText;
        }
 
        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}
cs


오타를 피하기 위해 문자열 딕셔너리 키를 상수로 정의합니다. 프로퍼티 명과 첫글자가 소문자인것만 다릅니다. DisplayLabelText 속성은 PersistentKeypadPage 생성자에서 사용할 수 있도록 PersistentKeypadPage를 인스턴스화하기 전에 설정됩니다.

저장할 항목이 더 많은 앱은 AppSettings라는 클래스에 내용을 통합하고 해당 클래스를 XML 또는 JSON 문자열로 직렬화한 다음 그 문자열을 딕셔너리에 저장할 수도 있습니다.

PersistentKeypadPage 클래스는 생성자에서 DisplayLabelText 프로퍼티에 액세스하고 두 이벤트 핸들러에서 프로퍼티를 설정합니다.

public class PersistentKeypadPage : ContentPage
{
    Label displayLabel;
    Button backspaceButton;
 
    public PersistentKeypadPage()
    {
        ...
            
        // New code for loading previous keypad text.
 
        App app = Application.Current as App;
        displayLabel.Text = app.DisplayLabelText;
        backspaceButton.IsEnabled = displayLabel.Text != null && displayLabel.Text.Length > 0;
    }
    void OnDigitButtonClicked(object sender, EventArgs args)
    {
        Button button = (Button)sender;
        displayLabel.Text += (string)button.StyleId;
        backspaceButton.IsEnabled = true;
 
        // save keypad text
        App app = Application.Current as App;
        app.DisplayLabelText = displayLabel.Text;
    }
 
    void OnBackspaceButtonClicked(object sender, EventArgs args)
    {
        string text = displayLabel.Text;
        displayLabel.Text = text.Substring(0, text.Length - 1);
        backspaceButton.IsEnabled = displayLabel.Text.Length > 0;
 
        // Save keypad text.
        App app = Application.Current as App;
        app.DisplayLabelText = displayLabel.Text;
    }
}
cs

Properties 딕셔너리 및 application lifecycle 이벤트를 사용하는 프로그램을 테스트 하다가 전화 또는 시뮬레이터에서 프로그램을 언인스톨 한 경우, 저장된 모든 데이터가 사라지므로 다음 번 비주얼 스튜디오(혹은 자마린 스튜디오)에서 프로그램 배포 시에는 처음 실행되는 것처럼 빈 딕셔너리가 나올 것이라는 점 주의 바랍니다.

댓글 없음:

댓글 쓰기