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(); } } }