자알못 자마린 – Xamarin.Forms Navigation 기초 1

Xamarin.Forms의 Navigation

지금까지 만들어 본 Xamarin.Forms 앱은 하나의 Page만 갖는 앱이다. 하지만 앱 대부분은 여러 Page로 구성되어 있다. 여러 Page를 구성하고 Page 간 이동을 위해 사용하는 것이 Navigation이다.

 

Hierarchical Navigation

Hierarchical Navigation은 스택 구조를 사용하여 Page를 관리한다. 현재 보이는 Page는 스택의 최상단에 위치한다. 새로운 Page를 띄우면 새로운 Page가 스택의 최상단에 위치하며 이전 Page로 돌아가면 Page가 스택에서 제거된다.

이전 Page로 돌아갈 수 있도록 화면 상단 Navigation Bar에 있는 Back Button을 구현해야 한다.

Navigation 관련 Page 주요 속성

  • Title: 화면 상단에 나타날 제목
  • NavigationPage.HasNavigationBar: Navigation Bar 표시 여부
  • NavigationPage.HasBackButton: Navigation Back Button 표시 여부

Hierarchical Navigation 관련 주요 메소드

  • PushAsync(): 내비게이션 스택에 새로운 Page를 추가하여 불러옴
  • PopAsync(): 내비게이션 스택 최상단에 있는 Page를 제거한 후 이전 Page로 돌아감

 

Hierarchical Navigation 예제

아래 예제는 WelcomePage에서 IntroductionPage로 이동하는 예제.

WelcomePage.xaml

간단한 Label과 Button 구현

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage Title="Welcome" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.WelcomePage">
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">    
        <Label Text="안녕요?ㅎ" HorizontalOptions="Center" VerticalOptions="Center"></Label>
        <Button Text="Next" Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

WelcomePage.xaml.cs

Button을 클릭하면 IntroductionPage를 띄우도록 구현

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class WelcomePage : ContentPage
    {
        public WelcomePage()
        {
            InitializeComponent();
        }

        // async 한정: await 작업이 완료될 때까지 async로 한정된 메소드의 실행을 멈춤
        async void Handle_Clicked(object sender, System.EventArgs e)
        {
            // PushAsync()는 비동기 메소드이므로 await 키워드를 사용하여 호출
            await Navigation.PushAsync(new IntroductionPage());
        }
    }
}

IntroductionPage.xaml

역시 간단한 Label과 Button 구현. 이전 Page로 돌아갈 수 있도록 Page에 HasNavigationBar 속성과 HasBackButton 속성을 추가한다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.IntroductionPage"
             Title="Introduction" NavigationPage.HasNavigationBar="true" NavigationPage.HasBackButton="true">
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="Hierarchical Navigation은 이렇게 작동합니다."></Label>
        <Button Text="Back" Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

IntroductionPage.xaml.cs

Button을 클릭하면 이전 Page로 되돌아가도록 구현

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class IntroductionPage : ContentPage
    {
        public IntroductionPage()
        {
            InitializeComponent();
        }

        async void Handle_Clicked(object sender, System.EventArgs e)
        {
            //대부분의 플랫폼은 명시적으로 Navigation Bar에 뒤로가기 버튼이 있으므로 
            //Back Button을 별도로 구현할 필요는 없지만... 여기선 실습을 위해 구현함
            await Navigation.PopAsync();
        }

        //OnBackButtonPressed() 메소드: 안드로이드 및 윈도우폰에서 뒤로가기 물리키를 눌렀을 때의 행동을 정의
        protected override bool OnBackButtonPressed()
        {
            // true 값을 반환하면 안드로이드 및 윈도우 폰에서 뒤로가기 키가 비활성화됨
            return true;
        }
    }
}

App.xaml.cs

MainPage를 NavigationPage로 지정해주어야 정상 작동한다. 그렇지 않으면 ‘PushAsync is not supported globally on [Platform], please use a NavigationPage.’ 라는 Exception Message 가 발생한다.

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            //NavigationPage 지정
            MainPage = new NavigationPage(new WelcomePage());
        }

        // ......
    }
}

실행 결과

 

Modal Page

Modal Page는 유저가 특정 작업을 완료하도록 잡아두는 Page다. 좌우로 나타나는 Hierarchical Navigation과 다르게 Modal Page는 화면 하단에서 나타난다. 또한 Navigation Bar가 없으므로 다른 Page로 이동할 수단을 구현해주어야 한다.

Modal Page를 구현하는 방법은 매우 간단하다. 위의 Hierarchical Navigation 예제에서 PushAsync(), PopAsync() 메소드를 각각 PushModalAsync(), PopModalAsync()로 대체해주기만 하면 된다.

WelcomePage.xaml, IntroductionPage.xaml

생략… 위의 Hierarchical Navigation 예제를 그대로 사용한다.

WelcomePage.xaml.cs

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class WelcomePage : ContentPage
    {
        public WelcomePage()
        {
            InitializeComponent();
        }

        async void Handle_Clicked(object sender, System.EventArgs e)
        {
            // Modal Page 띄우기
            await Navigation.PushModalAsync(new IntroductionPage());
        }
    }
}

IntroductionPage.xaml.cs

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class IntroductionPage : ContentPage
    {
        public IntroductionPage()
        {
            InitializeComponent();
        }

        async void Handle_Clicked(object sender, System.EventArgs e)
        {
            //Modal Page에서 이전 Page로 이동
            await Navigation.PopModalAsync();
        }
    }
}

실행 화면

 

Simple Master Detail Page

어느 Page에서 사용 중이던 객체를 새롭게 띄운 Page로 넘겨 사용할 수 있다.

아래 예제는 ListView로 구현한 연락처의 Item을 선택하면 Item의 세부 정보를 출력하는 Page를 띄우는 예제. 먼저 ListView Template에 대해 알아야 하는데 잘 모른다면 ‘자알못 자마린: Xamarin.Forms ListView 기초’ 글을 참고하자.

일련의 과정을 정리하자면 다음과 같다.

  1. ContactsPage.xaml: 연락처를 ListView로 보여주는 Page
  2. ContactsPage.xaml.cs: ListView의 Item을 선택하면 Item을 ContactDetailPage로 넘겨주는 코드
  3. ContactDetailPage.xaml: 선택한 연락처 정보를 세부적으로 보여주는 Simple Master Detail Page
  4. ContactDetailPage.xaml.cs: Item을 넘겨받는 코드

Contact.cs

ListView에 어떤 내용(Item)이 들어갈 지를 정의할 클래스를 만든다. 여기선 이름, 상태, 이미지URL을 정의한다.

namespace MyFirstXamarinApp
{
	public class Contact
	{
		public string Name { get; set; }
		public string Status { get; set; }
		public string ImageUrl { get; set; }
	}
}

ContactsPage.xaml

ListView를 보여줄 Page. Item을 텍스트로만 보여주는 TextCell을 사용했으며, Item을 선택했을 때 Simple Master Detail Page(ContactDetailPage)를 보여주도록 ItemSelected 이벤트 메소드를 지정했다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage Padding="0, 20, 0, 0" Title="Contacts" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ContactsPage">
    <!-- ItemSelected 이벤트 지정 -->
    <ListView x:Name="listView" ItemSelected="Handle_ItemSelected">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Name}" Detail="{Binding Status}" />>
            </DataTemplate>
        </ListView.ItemTemplate>        
    </ListView>
</ContentPage>

ContactsPage.xaml.cs

ListView의 Item을 선택했을 때 Simple Master Detail Page를 보여주는 코드. 선택한 Item 정보는 Simple Master Detail Page의 생성자를 통해 전달된다.

using System.Collections.Generic;

using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ContactsPage : ContentPage
    {
        async void Handle_ItemSelected(object sender, Xamarin.Forms.SelectedItemChangedEventArgs e)
        {
            if (e.SelectedItem == null)
                return;

            // 선택된 아이템을 Contact 타입으로 변환
            var contact = e.SelectedItem as Contact;

            // contact 객체를 Simple Master Detail Page(여기선 ContactDetailPage)로 전달
            await Navigation.PushAsync(new ContactDetailPage(contact));

            // ListView로 돌아갔을 때 아이템 선택 상태를 해제하려면
            // ListView의 SelectedItem 속성을 null로 지정
            listView.SelectedItem = null;
        }

        public ContactsPage()
        {
            InitializeComponent();

            listView.ItemsSource = new List<Contact> {
                new Contact { Name = "라이언", ImageUrl = "http://lorempixel.com/100/100/people/1", Status = "안녕요?ㅎ" },
                new Contact { Name = "어피치", ImageUrl = "http://lorempixel.com/100/100/people/2" }
            };
        }
    }
}

ContactDetailPage.xaml

새롭게 띄워질 Simple Master Detail Page를 어떻게 보여줄 것인지를 정의. 여기서는 Navigation Bar와 Label을 출력한다.

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage Padding="20" Title="{Binding Name}" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ContactDetailPage">
    <Label Text="{Binding Status}" />
</ContentPage>

ContactDetailPage.xaml.cs

이전 Page에서 선택한 ListView의 Item 정보를 가져오는 코드. 생성자를 통해 가져온 정보를 BindingContext 속성에 지정해주면 된다.

using System;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ContactDetailPage : ContentPage
    {
        public ContactDetailPage(Contact contact)
        {
            if (contact == null)
                throw new ArgumentNullException();

            // BindingContext 속성: XAML 태그 확장(XAML Markup Extension)에서 사용하기 위한 속성
            BindingContext = contact;

            InitializeComponent();
        }
    }
}

App.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
    public partial class App : Application
    {
            public App()
        {
            InitializeComponent();

            MainPage = new NavigationPage(new ContactsPage());
        }

        // ...
    }
}

 

출력결과

 

Master Detail Page

Master Detail Page는 한 쪽에는 간략한 정보를, 다른 한 쪽에는 세부 정보를 보여주는 Page다. Master Detail Page는 간략한 정보를 보여주는 Master Page와 세부 정보를 보여주는 Detail Page로 구성되어 있다.

주요 속성

  • IsPresented: 표시할 것인지 설정

 

Master Detail Page 예제

아래 예제는 연락처를 ListView로 보여주는 MasterPage를, ListView의 Item을 터치하면 세부 사항을 보여주는 DetailPage를 보여주는 예제다.

Contact.cs

위의 예제 그대로 사용

ContactsPage.xaml

MasterDetailPage를 구현한다. MasterPage 및 DetailPage는 속성요소구문(Property Element Syntax)을 통해 구현한다.

<?xml version="1.0" encoding="UTF-8"?>
<!-- IsPresented: 표시여부 설정 -->
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.ContactsPage"
                  IsPresented="true">
    
    <!-- MasterPage, DetailPage 각각 구현을 위해 속성요소구문 사용 -->
    <MasterDetailPage.Master>
        <ContentPage Padding="0, 20, 0, 0" Title="Contacts">
            <!-- ItemSelected 이벤트 지정 -->
            <ListView x:Name="listView" ItemSelected="Handle_ItemSelected">
               <ListView.ItemTemplate>
                   <DataTemplate>
                       <TextCell Text="{Binding Name}" Detail="{Binding Status}" />>
                    </DataTemplate>
                </ListView.ItemTemplate>        
            </ListView>
        </ContentPage>
    </MasterDetailPage.Master>
    
    <MasterDetailPage.Detail>
        <ContentPage></ContentPage>
    </MasterDetailPage.Detail>
    
</MasterDetailPage>

ContactsPage.xaml.cs

MasterDetailPage의 속성, 동작 등을 정의하는 코드

using System.Collections.Generic;
using Xamarin.Forms;

namespace MyFirstXamarinApp
{
    public partial class ContactsPage : MasterDetailPage
    {
        //MasterDetailPage에서는 이전 Page로 돌아가기 위해 기다릴 필요가 없으므로 비동기 메소드를 사용하지 않는다.
        void Handle_ItemSelected(object sender, Xamarin.Forms.SelectedItemChangedEventArgs e)
        {
            //선택된 아이템을 Contact 타입으로 변환
            var contact = e.SelectedItem as Contact;

            //MasterDetailPage를 NavigationPage로 구현해야 Navigation Bar가 표시되어 이전으로 돌아갈 수 있음
            Detail = new NavigationPage(new ContactDetailPage(contact));

            //Item을 선택한 후 MasterPage를 표시하지 않으려면 IsPresented 속성을 false로 지정
            IsPresented = false; //IsMasterPresented


            /* MasterDetailPage에선 어떤 항목이 선택되었는지 표시되어야 하므로
             * 아래 코드는 사용하지 않는다. */
            //if (e.SelectedItem == null)
            //    return;
            //listView.SelectedItem = null;
        }

        public ContactsPage()
        {
            InitializeComponent();

            listView.ItemsSource = new List<Contact> {
                new Contact { Name = "라이언", ImageUrl = "http://lorempixel.com/100/100/people/1", Status = "안녕요?ㅎ" },
                new Contact { Name = "어피치", ImageUrl = "http://lorempixel.com/100/100/people/2" }
            };
        }
    }
}

ContactDetailPage.xaml 및 ContactDetailPage.xaml.cs

위의 예제 그대로 사용

App.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
    public partial class App : Application
    {
            public App()
        {
            InitializeComponent();

            MainPage = new ContactsPage();
        }

        // ...
    }
}

실행 화면

 

Tabbed Page

Tabbed Page는 화면 하단에 여러 개의 탭을 둘 수 있는 Page다. Tab은 안드로이드에선 Page 상단에 나타나지만 iOS에서는 하단에 나타난다.

각 Page는 Children 속성에 포함된다.

 

Tabbed Page 구현 방법 1: Page 직접 구현

TappedPage 내에 여러 개의 Page를 직접 구현한다. 간단한 방법이지만 탭의 개수가 많거나 Page 내용이 복잡하면 관리하기 어려울 수 있다.

아래 예제는 TabbedPage 내에 ContentPage 2개를 직접 구현한 예제.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    <!-- Icon 속성은 iOS에서 탭 메뉴에 보이는 이미지 -->
    <ContentPage Title="Tap 1" Icon="clock.png">
        <Label Text="Content of Page 1" HorizontalOptions="Center" VerticalOptions="Center"></Label>
    </ContentPage>
        <ContentPage Title="Tap 2" Icon="clock.png">
        <Label Text="Content of Page 2" HorizontalOptions="Center" VerticalOptions="Center"></Label>
    </ContentPage>
</TabbedPage>

실행 화면

Tabbed Page 구현방법 2: 외부 Page 참조

별도로 독립된 Page를 만든 후 이것을 Tabbed Page로 활용한다.

아래 예제는 ContentPage0이라는 독립적인 Page를 만든 후 이를 Tabbed Page로 구현한 예제.

ContentPage0.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" x:Class="MyFirstXamarinApp.ContentPage0">
    <Label Text="Content of Page 0" VerticalOptions="Center" HorizontalOptions="Center"></Label>
</ContentPage>

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<!-- TabbedPage 내에 외부 Page를 추가하기 위해 XML 네임스페이스 선언(xmlns:local) 추가. 
     표준 XAML이 아닌 형식을 사용하기 때문. -->
<TabbedPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp" 
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    
    <!-- 외부 Page 1개 추가 -->
    <local:ContentPage0 Title="Tap 0" Icon="clock.png"></local:ContentPage0>
    
    <!-- 직접 구현한 Page 2개 구현 -->
    <ContentPage Title="Tap 1" Icon="clock.png">
        <Label Text="Content of Page 1" HorizontalOptions="Center" VerticalOptions="Center"></Label>
    </ContentPage>
        <ContentPage Title="Tap 2" Icon="clock.png">
        <Label Text="Content of Page 2" HorizontalOptions="Center" VerticalOptions="Center"></Label>
    </ContentPage>
</TabbedPage>

실행화면

 

Tabbed Page 상단에 Navigation Bar 추가하기

Tabbed Page 상단에 Navigation Bar를 보여주려면 TabbedPage 내에 NavigationPage를 사용하면 된다. 그런데 NavigationPage를 구현하려면 어떤 Page를 Root Page로 사용할지 정해야 한다. 이를 위해 Root Page의 객체를 NavigationPage의 생성자로 넘겨줘야 한다. C# 코드로는 다음과 같이 구현한다.

// 내비게이션 Page 구현(Root Page는 ContactsPage)
MainPage = new NavigationPage(new ContactsPage());

그렇다면 XAML에서 NavigationPage를 구현하려면 어떻게 해야 할까…? XAML에서 생성자로 인수를 넘기려면 <x:Arguments> 지시문을 사용하면 된다. <x:Arguments> 내용은 생성자에 대한 인수로 사용된다.

아래 예제는 TabbedPage에 NavigationPage 2개를 각각 직접구현, 외부참조 방식으로 구현한 예제.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<!-- TabbedPage 내에 외부 Page를 추가하기 위해 XML 네임스페이스 선언(xmlns:local) 추가. 
     표준 XAML이 아닌 형식을 사용하기 때문. -->
<TabbedPage 
    xmlns:local="clr-namespace:MyFirstXamarinApp" 
    xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.MainPage">
    
    <!-- 외부 ContentPage -->
    <local:ContentPage0 Title="Tap 0" Icon="clock.png"></local:ContentPage0>
    
    <!-- 내비게이션 Page(직접구현) -->
    <NavigationPage Title="Tap 1" Icon="clock.png">
        <!-- <x:Arguments> 내용은 NavigationPage의 생성자로 전달됨 -->
        <x:Arguments>
            <ContentPage Title="Tap 1">
                <Label Text="Content of Page 1" HorizontalOptions="Center" VerticalOptions="Center"></Label>
            </ContentPage>
        </x:Arguments>
    </NavigationPage>
    
    <!-- 내비게이션 Page(외부참조) -->
    <NavigationPage Title="Tap 2" Icon="clock.png">
        <x:Arguments>
            <local:WelcomePage />
        </x:Arguments>
    </NavigationPage>
</TabbedPage>

App.xaml.cs

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
    public partial class App : Application
    {
            public App()
        {
            InitializeComponent();

            MainPage = new MainPage();

            // ...
        }
    }
}

실행화면