자알못 자마린 – Xamarin.Forms Resource Dictionary, Style, Messaging Center, Xamarin.Essentials

Table of Content

Resource Dictionary

Xamarin.Forms로 코딩을 하다보면 똑같은 코드를 그대로 활용해야 하는 경우가 생긴다. 예를 들어 똑같은 디자인의 Button을 여러 개 배치한 후 버튼의 이름만 다르게 해야 한다면 Button 태그 내용을 그대로 복붙한 후 Text 속성만 다르게 하면 된다. 그런데 이 방법은 나중에 디자인을 변경할 때 Button 태그 하나하나마다 수정해줘야 하기 때문에 영 좋지 않다…

이를 해결하기 위해 사용하는 것이 Resource Dictionary다. Resource Dictionary에 반복적으로 사용할 속성 값을 등록해놓은 후 끌어다 쓰면 나중에 코드 수정이 편해진다.

Resource Dictionary를 많이 정의하면 애플리케이션 구동 속도를 늦출 수 있다. 따라서 여러 페이지에서 Resource Dictionary를 참조하는 경우 사용하는 것이 좋다.

특정 Page의 Resource Dictionary

모든 페이지에는 ResourceDictionary 형식의 Resource라는 속성이 있다. 특정 Page 내에서 이 Resource 속성을 구현하면 해당 Page 내에서만 Resouce Dictionary를 사용할 수 있다.

MainPage.xaml (XAML에서 Resource Dictionary를 사용하는 경우)

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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">
    
    <!-- Resource Dictionary 사용을 위해 속성 요소 구문 사용 -->
    <ContentPage.Resources>
        <ResourceDictionary>
            <x:Int32 x:Key="borderRadius">20</x:Int32>
            <Color x:Key="buttonBackgroundColor">Gray</Color>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login"
                BackgroundColor="{StaticResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
        <Button Text="Register"
                BackgroundColor="{StaticResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs (C# 코드로 Resource Dictionary를 사용하는 경우)

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

            // 초기의 Resource 속성은 null이므로 초기화
            this.Resources = new ResourceDictionary();
            this.Resources["BorderRadius"] = 20; // ResourceDictionary 등록
        }
    }
}

App의 Resource Dictionary

App에서 Resource Dictionary를 정의하면 모든 Page에서 Resource Dictionary를 사용할 수 있다.

Visual Studio를 사용하면 기본적으로 App.xaml이 생성되지 않는다고 한다… (확인해보지 못함) 이런 경우 새로운 ContentPage를 추가한 다음 ContentPage를 Application으로 변경하면 된다고 한다…

App.xaml

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.App">
    <!-- Page에서 정의한 Resource Dictionary는 Application에서 정의한 Resource Dictionary보다 우선함 -->
    <Application.Resources>
        <ResourceDictionary>
            <x:Int32 x:Key="borderRadius">20</x:Int32>
            <Color x:Key="buttonBackgroundColor">Gray</Color>
        </ResourceDictionary>
    </Application.Resources>
</Application>

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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">
    
    <!-- Resource Dictionary 사용을 위해 속성 요소 구문 사용 -->
    <!-- Page에서 정의한 Resource Dictionary는 Application에서 정의한 Resource Dictionary보다 우선함 -->
    <ContentPage.Resources>
        <ResourceDictionary>
            <x:Int32 x:Key="borderRadius">20</x:Int32>
            <Color x:Key="buttonBackgroundColor">Green</Color>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login"
                BackgroundColor="{StaticResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
        <Button Text="Register"
                BackgroundColor="{StaticResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
    </StackLayout>
</ContentPage>

Layout의 Resource Dictionary

Layout 내에 Resource Dictionary를 정의할 수 있다. 예를 들어 <StackLayout> 내에 <StackLayout.Resource>를 사용하면 Stack Layout 내에서 사용할 수 있는 Resource Dictionary를 정의할 수 있다.

Layout 내에서 Resource Dictionary를 정의하는 것은 코드가 너무 장황해지기 때문에 추천하지 않는다.

DynamicResource

위의 예제에서 Resource Dictionary를 사용하기 위해 XAML 태그 확장 구문에서 StaticResource를 사용했다. StaticResource는 처음 한 번만 사용되며 변경되지 않는다. 따라서 StaticResource 값을 변경해도 변경사항이 적용되지 않는다. 따라서 런타임 중 자원이 변경되게 하려면 DynamicResource를 사용해야 한다.

아래 예제는 “Change Style” Button을 터치하면 Button 색깔이 핑크 색으로 바뀌는 예제.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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.Resources>
        <ResourceDictionary>
            <x:Int32 x:Key="borderRadius">20</x:Int32>
            <Color x:Key="buttonBackgroundColor">Green</Color>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login"
                BackgroundColor="{DynamicResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
        <Button Text="Register"
                BackgroundColor="{DynamicResource buttonBackgroundColor}"
                TextColor="White"
                BorderRadius="{StaticResource borderRadius}"
                FontAttributes="Bold"></Button>
        <Button Text="Change Style"
                Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs

using Xamarin.Forms;

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

        void Handle_Clicked(object sender, System.EventArgs e)
        {
            Resources["buttonBackgroundColor"] = Color.Pink;
        }
    }
}

Style

ResourceDictionary의 Style을 사용하면 View의 디자인을 분리하여 구현할 수 있다. HTML에서 디자인을 CSS로 구현하는 것과 비슷하다고 보면 된다.

특정 Page의 Style

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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.Resources>
        <ResourceDictionary>
            <Style x:Key="button" TargetType="Button">
                <Setter Property="BackgroundColor" Value="Gray"></Setter>
                <Setter Property="TextColor" Value="White"></Setter>
                <Setter Property="BorderRadius" Value="20"></Setter>
                <Setter Property="FontAttributes" Value="Bold"></Setter>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login" Style="{StaticResource button}"></Button>
        <Button Text="Register" Style="{StaticResource button}"></Button>
        <Button Text="Change Style" Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

App의 Style

App과 Page에서 같은 이름의 key로 Style을 정의한 경우 Page에서 정의한 Style이 App에서 정의한 것을 덮어 씌운다. 즉 Page에서 정의한 Style이 App에서 정의한 것보다 우선한다.

App.xaml

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyFirstXamarinApp.App">
    <!-- Page에서 정의한 Resource Dictionary는 Application에서 정의한 Resource Dictionary보다 우선함 -->
    <Application.Resources>
        <Style TargetType="Button">
            <!-- Style 요소 안에는 하나 이상의 Setter를 정의-->
            <Setter Property="BackgroundColor" Value="Gray"></Setter>
            <Setter Property="TextColor" Value="White"></Setter>
            <Setter Property="BorderRadius" Value="20"></Setter>
            <Setter Property="FontAttributes" Value="Bold"></Setter>
        </Style>
    </Application.Resources>
</Application>

Style 상속

Style의 BasedOn 속성을 사용하면 이미 정의된 Style을 상속받아 구현할 수 있다.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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.Resources>
        <ResourceDictionary>
            <Style x:Key="button" TargetType="Button">
                <Setter Property="BackgroundColor" Value="Gray"></Setter>
                <Setter Property="TextColor" Value="White"></Setter>
                <Setter Property="BorderRadius" Value="20"></Setter>
                <Setter Property="FontAttributes" Value="Bold"></Setter>
            </Style>
            <Style x:Key="primaryButton" TargetType="Button" BasedOn="{StaticResource button}">
                <Setter Property="BackgroundColor" Value="Pink"></Setter>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login" Style="{StaticResource primaryButton}"></Button>
        <Button Text="Register" Style="{StaticResource button}"></Button>
        <Button Text="Change Style" Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

Implicit Style

Style 정의 시 Key를 정의하지 않으면 TargetType에 지정한 View 모두에 적용된다. 특정 부분만 다르게 하고 싶다면 특정 속성을 따로 지정해주면 된다.

하지만 Implicit Style을 사용하면 다른 Style에서 상속받을 수 없게 된다.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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.Resources>
        <ResourceDictionary>
            <Color x:Key="buttonBackgroundColor">Green</Color>
            <Style TargetType="Button">
                <Setter Property="BackgroundColor" Value="Gray"></Setter>
                <Setter Property="TextColor" Value="White"></Setter>
                <Setter Property="BorderRadius" Value="20"></Setter>
                <Setter Property="FontAttributes" Value="Bold"></Setter>
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Button Text="Login"></Button>
        <Button Text="Register" BackgroundColor="Yellow"></Button>
        <Button Text="Change Style" BackgroundColor="{StaticResource buttonBackgroundColor}" Clicked="Handle_Clicked"></Button>
    </StackLayout>
</ContentPage>

Messaging Center

MainPage에서 TargetPage라는 새로운 Page를 띄운다. 이 때 MainPage가 TargetPage에서 발생한 이벤트를 감지하려 한다. 이런 경우 다음과 같이 구현할 수 있다.

  • TargetPage는 특정 View의 이벤트 처리기와 MainPage에 이벤트를 전달할 델리게이트를 갖는다.
  • MainPage는 TargetPage의 객체를 생성한 후 TargetPage 객체 내의 델리게이트에 MainPage의 이벤트 핸들러를 추가한다.
  • MainPage에서 TargetPage를 띄운다.

위의 방법을 좀 더 간소화할 수 있는 방법은 Messaging Center를 사용하는 것이다. Messaging Center는 이벤트 발신자(Publisher)와 이벤트 구독자(Subscriber)로 구분된다. Publisher가 메시지를 Messaing Center에 보내면 Subscriber는 이를 받아볼 수 있다.

아래 예제는 TargetPage의 Slider 값을 MainPage에 보여주는 예제.

Events.cs

Events 클래스는 Messaging Center에서 사용할 메시지를 관리하기 위해 만든 정적 클래스다. 구독자(Subscriber) 수가 많거나 나중에 더 좋은 메시지로 바꾸려는 경우 별도의 정적 클래스로 관리하면 유지보수가 용이해진다.

namespace MyFirstXamarinApp
{
    public static class Events
    {
        public static string SliderValueChanged = "SliderValueChanged";
    }
}

TargetPage.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.TargetPage">
    <StackLayout Margin="20">
        <Slider x:Name="slider" ValueChanged="OnValueChanged"></Slider>
        <Label Text="{Binding Source={x:Reference slider}, Path=Value}"></Label>
    </StackLayout>
</ContentPage>

TargetPage.xaml.cs

using Xamarin.Forms;

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

        void OnValueChanged(object sender, ValueChangedEventArgs e)
        {
            // 첫 번째 매개변수: 게시자(Publisher). 여기서는 TargetPage(this).
            // 두 번째 매개변수: 메시지(Message). 다른 의미로는 메시지 식별자.
            // 세 번째 매개변수: 구독자(Subscriber)에게 보낼 객체
            MessagingCenter.Send(this, Events.SliderValueChanged, e.NewValue);
        }
    }
}

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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">
    
    <StackLayout Spacing="20" VerticalOptions="Center">
        <Label x:Name="label" HorizontalOptions="Center"></Label>
        <Button Text="Target Page" Clicked="OnClick"></Button>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs

using Xamarin.Forms;

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

        void OnClick(object sender, System.EventArgs e)
        {
            // 새롭게 띄울 Page 객체 생성
            var page = new TargetPage();

            // 제네릭: 발신자(Publisher)의 타입과 발신자(Publisher)로부터 받아올 객체의 타입
            // 첫 번째 매개변수: 구독자(Subscriber). 여기서는 MainPage(this)
            // 두 번째 매개변수: 메시지(Message). 엄밀히 말하면 메시지를 식별하기 위한 값.
            // 세 번째 매개변수: 구독받은 정보를 처리할 이벤트 핸들러
            MessagingCenter.Subscribe<TargetPage, double>(this, Events.SliderValueChanged, OnSliderValueChanged);

            // Page 새롭게 띄우기
            Navigation.PushAsync(page);

            // 메시지 수신을 원치 않는 경우: Unsubscribe() 메소드 사용
            // 제네릭: 구독자(Subscriber)의 타입
            MessagingCenter.Unsubscribe<MainPage>(this, Events.SliderValueChanged);
        }

        private void OnSliderValueChanged(TargetPage source, double newValue)
        {
            label.Text = newValue.ToString();
        }
    }
}

실행화면

Xamarin.Essentials

Xamarin.Essentials는 애플리케이션 개발에 유용한 기능들을 모아놓은 크로스 플랫폼 API다. 개발자가 디바이스 정보, 배터리 상태, GPS, SMS, E-Mail, 공유 기능들을 직접 구현하지 않고도 Xamarin.Essentials에 구현된 것을 가져다 쓰기만 하면 간단하게 구현된다.

각 프로젝트(.Forms, .iOS, .Android, .UWP 등)에 Xamarin.Essentials Nuget 패키지를 추가하여 사용하면 된다. Xamarin.Essentials이 제공하는 API 및 사용 예제는 https://docs.microsoft.com/en-us/xamarin/essentials에서 볼 수 있다.

그 외에 Xamarin.Forms에서 사용할 수 있는 다양한 플러그인들은 https://github.com/xamarin/xamarincomponents를 참고.

아래 예제는 Xamarin.Essentials를 사용하여 장치의 기본적인 정보(디바이스, 디스플레이, 배터리, 네트워크 상태)를 보여주는 예제.

MainPage.xaml

<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
    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">
    
    <StackLayout Margin="20" VerticalOptions="Start">
        <Button Text="Check Device Info" Clicked="Handle_Clicked"></Button>
        <Label x:Name="deviceInfo" HorizontalOptions="Start"></Label>
        <Label x:Name="deviceDisplayInfo" HorizontalOptions="Start"></Label>
        <Label x:Name="batteryInfo" HorizontalOptions="Start"></Label>
        <Label x:Name="networkAccess" HorizontalOptions="Start"></Label>
    </StackLayout>
</ContentPage>

MainPage.xaml.cs

using Xamarin.Forms;
using Xamarin.Essentials;

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

        void Handle_Clicked(object sender, System.EventArgs e)
        {
            deviceInfo.Text = "Device: " + DeviceInfo.Model + "\n"
                + "Manufacturer: " + DeviceInfo.Manufacturer + "\n"
                + "Device Name: " + DeviceInfo.Name + "\n"
                + "Version: " + DeviceInfo.VersionString + "\n"
                + "Platform: " + DeviceInfo.Platform + "\n"
                + "Idiom: " + DeviceInfo.Idiom + "\n"
                + "Device Type: " + DeviceInfo.DeviceType + "\n";

            deviceDisplayInfo.Text = "MainDisplayInfo: " + DeviceDisplay.MainDisplayInfo + "\n\n"
                + "Orientation: " + DeviceDisplay.MainDisplayInfo.Orientation + "\n"
                + "Rotation: " + DeviceDisplay.MainDisplayInfo.Rotation + "\n"
                + "Width: " + DeviceDisplay.MainDisplayInfo.Width + "\n"
                + "height: " + DeviceDisplay.MainDisplayInfo.Height + "\n"
                + "Density: " + DeviceDisplay.MainDisplayInfo.Density + "\n";

            // 안드로이드의 경우 BatteryStats 권한 및 어셈블리 추가 필요
            batteryInfo.Text = "Battery Percentage: " + Battery.ChargeLevel + "\n"
                + "Battery State: " + Battery.State + "\n"
                + "Battery Source: " + Battery.PowerSource + "\n";

            // 안드로이드의 경우 AccessNetworkState 권한 및 어셈블리 추가 필요
            networkAccess.Text = "Network Status: " + Connectivity.NetworkAccess.ToString();
        }
    }
}

실행화면

댓글 남기기