자알못 자마린: Xamarin.Forms XAML 기초

Table of Content


코드를 이용한 UI 구현

XAML로 구현할 수 있는 모든 UI는 C# 코드로 구현할 수 있다. 그러나 C# 코드로 UI를 구현하면 XAML로 구현하는 것보다 코드가 매우 복잡해진다.

페이지에 요소를 동적으로 추가하려는 경우에 사용할 수 있지만 이런 경우는 흔치 않다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

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

            // 코드를 이용하여 Label 추가
            Content = new Label
            {
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center,
                Text = "안녕요?ㅎ"
            };
        }
    }
}

 

XAML을 이용한 UI 구현

XAML로 UI를 구현하는 것은 C#코드로만 UI를 구현하는 것보다 간결하다. 복잡한 UI를 구현할 때 C# 코드로 구현하는 것보다 유연하게 대처할 수 있다.

XAML로 앱의 시각적 모양을, C#으로 앱의 동작을 구현하는 것이 일반적이다.

<?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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    <StackLayout>
        <!-- Place new controls here -->
        <Label Text="안녕요?ㅎ" 
               HorizontalOptions="Center" 
               VerticalOptions="Center" />
    </StackLayout>
</ContentPage>

 

XAML을 이용한 UI 생성 과정

런타임 시 InitializeComponent() 메소드가 실행된 후 LoadFromXaml() 메소드에 의해 XAML 파일이 XAML 파서로 전달되어 UI가 생성된다.

  • XAML 파서: XAML을 파일을 처리하여 UI 생성 및 C# 객체 인스턴스화

위 동작 코드는 MainPage.xaml.g.cs 파일에 나타나 있으며, 이 파일은 자동 생성된다. 이 파일은 기본적으로 숨겨져 있으므로 내용을 보려면 다음 절차를 따라야 한다.

  • 솔루션 탐색기에서 해당 프로젝트 마우스 우클릭 -> 옵션 표시 메뉴 -> 모든 파일 체크 클릭
  • Xamarin 공유 프로젝트 -> obj -> netstandard2.0 -> MainPage.xaml.g.cs 파일 더블클릭

 

ContentPage

ContentPage는 하나의 View를 표시하는 페이지

  • View: 시각적 요소와 관련된 기본 클래스. Button, Label, Slider 등의 요소를 제공.

다음과 같이 ContentPage 내에 단일 View가 아닌 다중 View를 선언하면 실행 시 나중에 추가된 View만 보이거나  ”Content’ 속성이 두 번 이상 설정되었습니다.’ 라는 오류가 발생한다.

<?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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <!-- ContentPage 내에 2개의 뷰(View)를 배치하면 오류 발생 -->
    <Label Text="안녕요?ㅎ"></Label>
    <Slider></Slider>
</ContentPage>

ContentPage 내에 Layout을 하나 정의한 후 그 Layout 안에 여러 개의 View를 배치하면 문제를 해결할 수 있다.

  • StackLayout: 가로 또는 세로 방향으로 요소를 배치하는 레이아웃
<?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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <!-- StackLayout 내에 2개의 뷰를 배치 -->
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <Label Text="안녕요?ㅎ"></Label>
        <Slider></Slider>
    </StackLayout>
</ContentPage>

 

XAML로 만든 요소에 접근하는 방법 1 – x:Name 속성을 사용하기

XAML로 만든 요소를 C# 코드에서 접근하려면 x:Name 속성을 사용하여 요소에 이름을 부여해야 한다. x는 페이지 태그에서 선언한 접두사다.

  • 예: x:Name=”VIEW_NAME”
<?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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <!-- Label에 label이라는 이름 부여 -->
        <Label x:Name="label" Text="안녕요?ㅎ"></Label>
        
        <!-- Slider에 slider라는 이름 부여 & ValueChanged 이벤트 등록 -->
        <Slider x:Name="slider" ValueChanged="Handle_ValueChanged"></Slider>
    </StackLayout>
</ContentPage>

위 XAML에서 xmlns:x를 Microsoft 네임스페이스에 대한 XML 네임스페이스를 선언한다.

요소에 이름을 부여하면 MainPage.xaml.g.cs 파일에 자동 구현된 FindByName() 메소드 호출로 인해 C# 코드로 요소를 제어할 수 있게 된다.

아래 예제는 slider 변화에 따라 label에 표시되는 값이 달라지는 예제

using System;
using Xamarin.Forms;

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

            slider.Value = 0.5; // slider의 기본값 설정
        }

        void Handle_ValueChanged(object sender, Xamarin.Forms.ValueChangedEventArgs e)
        {
            label.Text = String.Format("Value is {0}", e.NewValue);
        }
    }
}

 

XAML로 만든 요소에 접근하는 방법 2 – 데이터 바인딩

데이터 바인딩이란 두 요소의 속성을 서로 연결하는 것이다. 데이터 바인딩을 이용하면 이벤트 처리기를 만들지 않아도 되므로 이전의 방법보다 간결하다.

데이터 바인딩을 사용하려면 XAML 태그 확장(XAML Markup Extension)을 사용해야 한다. XAML 태그 확장에 관해서는 Microsoft Docs를 참고.

<?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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <!-- 
        # Label의 Text 속성에 데이터 바인딩을 위한 XAML 태그 확장 사용
         - Binding: 두 개체를 연결할 데이터 바인딩 선언 
         - Source: XAML 파일에 정의된 다른 객체를 참조. x:Reference 뒤에 오는 값은 다른 객체에서 x:Name 속성에 정의한 값
         - Value: 다른 객체로부터 넘겨받는 현재 시점의 값
         - StringFormat: 출력할 문자열 지정. 표준 C# 문자열 형식 사용 가능.
        -->
        <Label x:Name="label"
               Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Value is {0}'}"></Label>
        <Slider x:Name="slider"></Slider>
    </StackLayout>
</ContentPage>

 

바인딩 컨텍스트(BindingContext) 속성

여러 속성에 데이터 바인딩을 적용하면 Source 속성을 중복하여 작성하게 된다. 이를 막고 간결하게 작성하기 위해 BindingContext 속성을 사용한다.

그러나 중복하여 작성하게 되는 부분이 적으면 사용하지 않는 게 가독성 면에서 좋은 것 같다. 이거 쓰면 많이 헷갈린다…

아래 예제는 slider 값에 따라 label의 투명도가 조절되는 예제 (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:local="clr-namespace:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <StackLayout VerticalOptions="Center" HorizontalOptions="Center">
        <!-- 
        # Label의 Text 속성과 Opacity 속성에 데이터 바인딩 사용
         - 각 속성에 Source={x:Reference slider} 부분이 중복됨
        -->
        <Label x:Name="label"
               Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Value is {0}'}"
               Opacity="{Binding Source={x:Reference slider},
                                 Path=Value}"></Label>
        <Slider x:Name="slider"></Slider>
    </StackLayout>
</ContentPage>

아래 예제는 slider 값에 따라 BoxView와 label의 투명도가 조절되는 예제 (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:local="clr-namespace:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    
    <!-- StackLayout에 BindingContext 속성을 정의 -->
    <StackLayout BindingContext="{x:Reference slider}" VerticalOptions="Center" HorizontalOptions="Center">
        <BoxView Color="green"
                 Opacity="{Binding Value}"></BoxView>
        <Label x:Name="label"
               Text="{Binding Value, StringFormat='Value is {0}'}"
               Opacity="{Binding Value}"></Label>
        <Slider x:Name="slider"></Slider>
    </StackLayout>
</ContentPage>

 

플랫폼별 UI 차이점 두기

StackLayout에서 VerticalOptions을 주지 않으면 요소가 위쪽에서부터 차곡차곡 배치된다. 이런 경우 안드로이드에선 문제가 되지 않지만 iOS에선 상단 바의 내용과 앱에서 표시되는 내용이 겹치게 된다. iPhone X인 경우 Notch에 요소가 가려진다.

상단 여백을 설정하지 않은 경우

이를 해결하려면 플랫폼 별로 UI를 다루게 두어야 한다. 다음과 같이 Device 클래스의 OS 속성 값을 비교하면 된다.

using Xamarin.Forms;

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

            if (Device.OS == TargetPlatform.Android)
                Padding = new Thickness(0, 0, 0, 0); // 좌, 상, 우, 하
            else if (Device.OS == TargetPlatform.iOS)
                Padding = new Thickness(0, 40, 0, 0);
            else if (Device.OS == TargetPlatform.Windows)
                Padding = new Thickness(10, 20, 30, 40);
        }
    }
}

그러나 Xamarin 2.3.4 버전부터 Device.OS는 더 이상 쓰이지 않는다. Device.OS 대신 Device.RuntimePlatform을 사용해야 한다.

using Xamarin.Forms;

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

            switch (Device.RuntimePlatform)
            {
                case Device.Android:
                    Padding = new Thickness(0, 0, 0, 0);
                    break;
                case Device.iOS:
                    Padding = new Thickness(0, 40, 0, 0);
                    break;
                case Device.UWP:
                    Padding = new Thickness(10, 20, 30, 40);
                    break;
            }
        }
    }
}

Device.OnPlatform<T> 메소드를 사용하면 코드를 더 줄일 수 있으나 이것 역시 Xamarin 2.3.4 버전부터 쓰이지 않음. 대신 switch(RuntimePlatform)을 쓰라는 경고를 내뱉으니 이걸 쓰자…

  • XAML에서 <OnPlatform> 태그를 사용하려 하면 ‘Type OnPlatform not found in xmlns http://xamarin.com/schemas/2014/forms ‘ 라는 경고 메시지가 뜸. 안 써야 하나…?
상단 여백을 플랫폼 별로 다르게 설정한 경우

XAML Compilation

XAML Compilation을 사용하면 런타임 때가 아닌 컴파일할 때 오류를 잡아낼 수 있다. 또한 XAML 요소의 로딩이 더 빨라져 약간의 최적화가 이루어진다.

XAML Compilation을 사용하려면 애트리뷰트를 추가해야 한다. PROJECT_NAME.AssemblyInfo.cs 파일에 애트리뷰트를 추가하거나 xaml.cs 파일의 네임스페이스 위에 애트리뷰트를 추가하면 된다.

예전엔 기본 옵션이 Skip이었으나 최근에 Compile로 바뀐 듯…

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

/* XAML Compilation을 위한 애트리뷰트 및 어셈블리 */
[assembly: XamlCompilation (XamlCompilationOptions.Compile)]
namespace MyFirstXamarinApp
{
	public partial class App : Application
	{
		// 코드 생략...
	}
}

 

연습문제: 격언 생성기

Button을 클릭하면 다음 격언을 보여주는 앱을 만들어 보기

조건

앱에 다음과 같은 View를 포함한다.

  • Next 버튼
  • 폰트 크기를 보여주는 Label
  • 폰트 크기를 조절하는 Slider
  • 격언을 보여주는 Label

앱에 다음과 같은 기능을 구현한다.

  • Slider를 조절하면 Label에 폰트 크기를 출력한다. 동시에 격언을 보여주는 Label의 폰트 크기도 조절되어야 한다.
    • XAML Slider에 Maximum, Minimum 속성을 부여할 때 Maximum 속성을 먼저 부여해야 런타임 시 에러가 발생하지 않음.
  • 플랫폼(Android, iOS, UWP)별로 Padding을 다르게 설정한다. 단 iOS 플랫폼에선 Safe Area를 설정한다.

실행 결과

MainPage.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:MyFirstXamarinApp" x:Class="MyFirstXamarinApp.MainPage">
    <StackLayout BindingContext="{x:Reference sliderFontSize}" 
                 VerticalOptions="Start" 
                 HorizontalOptions="Fill">
        <Button x:Name="button_Next" 
                Clicked="button_Next_Clicked"
                Text="Next"></Button>
        <Label x:Name="labelFontSize"
               Text="{Binding Value, StringFormat='Font Size is {0}'}"></Label>
        <Slider x:Name="sliderFontSize" 
                Maximum="100" 
                Minimum="10"></Slider>
        <Label x:Name="labelMaxim" 
               FontSize="{Binding Value}}"
               Text="Next 버튼을 누르면 격언이 나타납니다."></Label>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs

using System;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

namespace MyFirstXamarinApp
{
    public partial class MainPage : ContentPage
    {
        private int index = 0;
        private string[] maxims = new string[]
        {
            "늦었다고 생각했을 때가 정말 늦었다.",
            "시작은 반이 아니다. 시작일 뿐이다.",
            "개천에서 용 난 사람 만나면 개천으로 빨려 들어간다.",
            "티끌 모아봤자 티끌이다.",
            "헌신하면 헌신짝된다.",
            "기쁨을 나누면 질투가 되고 슬픔을 나누면 약점이 된다.",
            "슬픔은 가락이 되고 사랑은 시가 되리니"
        };

        public MainPage()
        {
            InitializeComponent();

            switch (Device.RuntimePlatform)
            {
                case Device.Android:
                    Padding = new Thickness(20, 0, 20, 20);
                    break;
                case Device.iOS:
                    On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(true); // iOS Safe Area 설정
                    break;
                case Device.UWP:
                    Padding = new Thickness(20, 40, 20, 20);
                    break;
            }
        }

        private void button_Next_Clicked(object sender, EventArgs e)
        {
            labelMaxim.Text = maxims[index++];

            if (index >= maxims.Length)
                index = 0;
        }
    }
}

 

UI를 만들기 위한 연습

BackgroundColor 속성 사용하기

요소가 생각하는 대로 배치되지 않으면 어떻게 렌더링되는지 쉽게 볼 수 있도록 BackgroundColor를 설정한다.

전체 페이지를 처음부터 완벽하게 구현하려 하지 말 것

한 번에 하나의 요소씩 집중해서 구현하도록 한다.

UI를 구현하는 방법은 다양함

내 솔루션은 타인과 다를 수 있다. 옳고 그른 건 방법은 없다.

다양한 장치(시뮬레이터)에서 실험해 보기

제곧내…

최대한 스스로 연습해보기

남의 솔루션을 가능한 한 빨리 참고하지 않는다.

 

댓글 남기기