.NET MAUI - Control Templates

2022. 8. 2. 00:00MAUI

반응형

ContentView 를 상속한 사용자 지정 컨트롤 및 ContentPage 를 상속한 페이지의 사용 가능 하다.

 

컨트롤 템플릿은 일종의 Layout 과 같다. 

web page 같은 경우 Layout 을 먼저 만들어서 Header, Body,Footer  같은 형태를 만들고

Body 를 변경하며 작업하기도 한다. Footer 와 Header 는 고정하고 Body 만 변경하는 식이다.

ControlTemplate 또한 이러한 작업을 할 수 있다. 

 

ControlTemplate 을 다음과 같이 정의 한다고 해보자

-Header

-Body

-Footer

 

PageA 에서 

- Hello 라는 Label 을 두고

 

PageB 에서

- World 라는 Label 을 뒀다면

 

ControlTemplate 를 적용했다고 가정했을때 다음과 같을 수 있다.

Header

Hello

Footer

 

Header

World
Footer

 

Hello 와 World 같은 것은 Contents 이다. contents 는 ContentPresenter 에 대체 된다. 

App.xaml 에 다음과 같이  ControlTemplate 를 정의 했다고 하자

<ControlTemplate x:Key="FooterHeaderControlTemplate">
    <StackLayout BindingContext="{Binding Source={RelativeSource TemplatedParent}}">
        <Label Text="Header" />
        <ContentPresenter />
        <Label Text="Footer" />
    </StackLayout>
</ControlTemplate>

Page.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.ControlTemplate.Page"
             Title="CardViewPage"
              ControlTemplate="{StaticResource FooterHeaderControlTemplate}"
              >
    <ContentPage.Resources>
</ContentPage.Resources>
    <Label Text="Hello" TextColor="Red" FontSize="24" />
</ContentPage>

 

사용자 지정 컨트롤에서  ControlTemplate 사용법

MyButtonControl.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.ControlTemplate.MyButtonControl"
             x:Name="this">
    <Border BindingContext="{x:Reference this}"
           BackgroundColor="{Binding BackgroundColor}"
            Background="{Binding Background, FallbackValue='#000000'}"
            Stroke="{Binding Stroke}"
            StrokeThickness="{Binding StrokeThickness}"
            StrokeShape="RoundRectangle 40,0,0,40"
            
           >
        <StackLayout HorizontalOptions="Center">
            <Label Text="{Binding ButtonTitle, FallbackValue='TestTitle'}" />
            <Button Text="{Binding ButtonContent, FallbackValue='TestButton'}" />
            <Label Text="{Binding ButtonFooter, FallbackValue='TestFooter'}"/>
        </StackLayout>
    </Border>
</ContentView>

MyButtonControl.xaml.cs

namespace MauiApp1.ControlTemplate;

public partial class MyButtonControl : ContentView
{
    public static readonly BindableProperty ButtonTitleProperty = BindableProperty.Create(nameof(ButtonTitle), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty ButtonContentProperty = BindableProperty.Create(nameof(ButtonContent), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty ButtonFooterProperty = BindableProperty.Create(nameof(ButtonFooter), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(MyButtonControl), Colors.Aqua);
    public static readonly BindableProperty StrokeProperty = BindableProperty.Create(nameof(Stroke), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(nameof(StrokeThickness), typeof(string), typeof(MyButtonControl), string.Empty);

    public string Stroke
    {
        get => (string)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }
    public string StrokeThickness
    {
        get => (string)GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }
    public string ButtonTitle
    {
        get => (string)GetValue(ButtonTitleProperty);
        set => SetValue(ButtonTitleProperty, value);
    }
    public string ButtonContent
    {
        get => (string)GetValue(ButtonContentProperty);
        set => SetValue(ButtonContentProperty, value);
    }
    public string ButtonFooter
    {
        get => (string)GetValue(ButtonFooterProperty);
        set => SetValue(ButtonFooterProperty, value);
    }
    public Color BorderColor
    {
        get => (Color)GetValue(BorderColorProperty);
        set => SetValue(BorderColorProperty, value);
    }
    public MyButtonControl()
    {
        InitializeComponent();
    }
}

Page.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.ControlTemplate.Page"
             xmlns:local ="clr-namespace:MauiApp1.ControlTemplate"
             Title="ButtonViewPage"
              >
    <ContentPage.Resources></ContentPage.Resources>
    <StackLayout>
        <local:MyButtonControl  Stroke="#C49B33"
                                StrokeThickness="8"
                                ButtonTitle="My Button" 
                                ButtonFooter="야호"
                                ButtonContent="가즈아!!" 
                                ControlTemplate="{StaticResource FooterHeaderControlTemplate}"/>
    </StackLayout>

</ContentPage>

 

 

 

Template Binding 과 Template Parent 의 차이점

과거 WPF 에서는 이 둘 사이에 차이점이 있었다. 

굳이 이야기 하자면 

Template Binding 은 Compile time 에 binding 되고 Binding 된 값이 변경되어도 변경이 되지 않는다.

Template Parent 는 Runtime 에 Binding 되고 Runtime 중  Binding 된 값이 변경되면  변경이 적용된다.

 

하지만!!!

MAUI 에서는 그런 점이 없는 걸로 보인다. 

실제 MSDN 의 설명에는 다음과 같은 내용이 있다. 

 

TemplateBinding 태그 확장을 사용하는 것은 RelativeSource 태그 확장을 사용하여 템플릿의 루트 요소의 BindingContext를 템플릿 기반 부모로 설정하고 Binding 태그 확장을 사용하여 자식 개체의 바인딩을 확인하는 것과 같습니다. 실제로 TemplateBinding 태그 확장은 Source가 RelativeBindingSource.TemplatedParent인 Binding을 만듭니다.

 

 

예제를 통해 관련 내역을 확인해 보자

아래 예제는 위에 에제에서 Content 부분을 TemplateParent 를 통해 ViewModel 의 Content 속성과 binding 하고

Footer 부분을 TemplateBinding 을 통해 ViewModel 의 Footer 속성과 binding 하였다. 

 

그리고 button 을 누르면 runtime 중에 두 속성에 값을 변경하도록 하였다. 

 

 

MyButtonContentChangeViewModel

using System.ComponentModel;
using System.Windows.Input;

namespace MauiApp1.ControlTemplate;
public class MyButtonContentChangeViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public MyButtonContentChangeViewModel()
    {
        RuntimeChangeContentCommand = new Command(
            execute: () =>
            {
                Content = $"Change Content {DateTime.Now}";
                Footer = $"Change Footer {DateTime.Now}";
            });

        Content = "Init Content";
        Footer = "Init Footer";
    }

    private string content;
    public string Content
    {
        private set
        {
            if (content != value)
            {
                content = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Content"));
            }
        }
        get
        {
            return content;
        }
    }

    private string footer;
    public string Footer
    {
        private set
        {
            if (footer != value)
            {
                footer = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Footer"));
            }
        }
        get
        {
            return footer;
        }
    }
    public ICommand RuntimeChangeContentCommand { private set; get; }
}

MyButtonControl.xaml (사용자 지정 컨트롤)

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.ControlTemplate.MyButtonControl"
             x:Name="this">
    <Border BindingContext="{x:Reference this}"
           BackgroundColor="{Binding BackgroundColor}"
            Background="{Binding Background, FallbackValue='#000000'}"
            Stroke="{Binding Stroke}"
            StrokeThickness="{Binding StrokeThickness}"
            StrokeShape="RoundRectangle 40,0,0,40"
            
           >
        <StackLayout HorizontalOptions="Center">
            <Label Text="{Binding ButtonTitle, FallbackValue='TestTitle'}" />
            <Button Text="{Binding ButtonContent, FallbackValue='TestButton'}" />
            <Label Text="{Binding ButtonFooter, FallbackValue='TestFooter'}"/>
        </StackLayout>
    </Border>
</ContentView>

MyButtonControl.xaml.cs (사용자 지정 컨트롤)

namespace MauiApp1.ControlTemplate;

public partial class MyButtonControl : ContentView
{
    public static readonly BindableProperty ButtonTitleProperty = BindableProperty.Create(nameof(ButtonTitle), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty ButtonContentProperty = BindableProperty.Create(nameof(ButtonContent), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty ButtonFooterProperty = BindableProperty.Create(nameof(ButtonFooter), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(MyButtonControl), Colors.Aqua);
    public static readonly BindableProperty StrokeProperty = BindableProperty.Create(nameof(Stroke), typeof(string), typeof(MyButtonControl), string.Empty);
    public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(nameof(StrokeThickness), typeof(string), typeof(MyButtonControl), string.Empty);

    public string Stroke
    {
        get => (string)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }
    public string StrokeThickness
    {
        get => (string)GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }
    public string ButtonTitle
    {
        get => (string)GetValue(ButtonTitleProperty);
        set => SetValue(ButtonTitleProperty, value);
    }
    public string ButtonContent
    {
        get => (string)GetValue(ButtonContentProperty);
        set => SetValue(ButtonContentProperty, value);
    }
    public string ButtonFooter
    {
        get => (string)GetValue(ButtonFooterProperty);
        set => SetValue(ButtonFooterProperty, value);
    }
    public Color BorderColor
    {
        get => (Color)GetValue(BorderColorProperty);
        set => SetValue(BorderColorProperty, value);
    }
    public MyButtonControl()
    {
        InitializeComponent();
    }
}

App.xaml

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiApp1"
             x:Class="MauiApp1.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <Color x:Key="bgColor">#C0C0C0</Color>
            <Color x:Key="fgColor">#0000AD</Color>
            <x:Double x:Key="fontSize">22</x:Double>
            <Style x:Key="baseLabelStyle" TargetType="Label">
                <Setter Property="TextColor" Value="{StaticResource fgColor}" />
                <Setter Property="FontSize" Value="{StaticResource fontSize}" />
            </Style>
            <Style x:Key="infoLabelStyle" TargetType="Label" BasedOn="{StaticResource baseLabelStyle}">
                <Setter Property="FontAttributes" Value="Bold" />
            </Style>
            <ControlTemplate x:Key="FooterHeaderControlTemplate">
                <StackLayout>
                    <Label Text="{Binding ButtonTitle,
                                  Source={RelativeSource TemplatedParent},
                                  StringFormat='ButtonTile value is {0}'}"/>

                    <ContentPresenter />
                    <Label Text="{TemplateBinding ButtonFooter,
                        StringFormat='ButtonFooter value is {0}'}" />
                </StackLayout>
            </ControlTemplate>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Page.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.ControlTemplate.Page"
             xmlns:local ="clr-namespace:MauiApp1.ControlTemplate"
             Title="ButtonViewPage"
              >
    <ContentPage.BindingContext>
        <local:MyButtonContentChangeViewModel />
    </ContentPage.BindingContext>
    <StackLayout>
        <local:MyButtonControl  Stroke="#C49B33"
                                StrokeThickness="8"
                                ButtonTitle="My Button" 
                                ButtonFooter="야호"
                                ButtonContent="가즈아!!" 
                                ControlTemplate="{StaticResource FooterHeaderControlTemplate}"/>
        
        <local:MyButtonControl  Stroke="#C49B33"
                                StrokeThickness="8"
                                ButtonTitle="My Button" 
                                ButtonFooter="{Binding Footer}"
                                ButtonContent="{Binding Content}" 
                                ControlTemplate="{StaticResource FooterHeaderControlTemplate}"/>

        <Button Command="{Binding RuntimeChangeContentCommand}" Text="변경하자"/>
    </StackLayout>

</ContentPage>

변경하자 클릭 후

 

관련영상

https://youtu.be/81K1e10RfzA

 

반응형

'MAUI' 카테고리의 다른 글

.NET MAUI - Triggers (1)  (0) 2022.08.04
.NET MAUI - Data Templates  (0) 2022.08.03
.NET MAUI - Binding Command  (0) 2022.08.01
.NET MAUI - Binding Relative bindings  (0) 2022.07.29
.NET MAUI - Binding Value Converters  (0) 2022.07.28