본문 바로가기
Develop/.NET

[WPF] Template, Presenter + Binding

by KyungHoon 2021. 8. 5.

이번 포스팅에서는 WPF에서 UI를 다룰 때 필수적인 개념이라고 할 수 있는 Template과 Presenter 대해 알아보겠습니다.

 


1. Template, 템플릿이란?

Template, 템플릿이란 무엇일까요? 제가 처음 Template을 접했을 때는 간단하게 Control을 꾸미기 위한 틀 정도로만 생각하고 넘어갔던 것 같습니다. 좀 더 자세히 알아볼까요?

WPF UI 프로그래밍 필수 개념, 템플릿에 대해 알아보자.

Template이란 부모 자식 관계를 갖는 엘리먼트들을 생성시킬 수 있는 정의. 즉 루트 엘리먼트, 루트 엘리먼트의 자식 엘리먼트, ··· 등 각각의 엘리먼트의 속성에 무엇이 있는지 선언하는 것입니다.

 

이렇게 정의된 Template은 LoadContent 메서드를 통해 정의에 따라 각각의 엘리먼트들의 인스턴스들이 실제적으로 생성된다.

여기서 주의할 점은 XAML에서 엘리먼트들을 선언하는 것은 바로 인스턴스를 생성한다는 의미이지만, Template을 정의할 때는 인스턴스가 바로 생성되는 것이 아니라는 차이점이 있다.

MSDN - DataTemplate.LoadContent Method : LoadContent를 호출하면 DataTemplate의 UIElement개체가 생성되고 다른 UIElement의 시각적 트리에 추가할 수 있습니다.

 


2. Template의 종류?

그렇다면 Template의 종류에는 어떤 것들이 있을까요?

Template의 종류

Template의 종류는 크게 3가지로 ControlTemplate, DataTemplate, ItemsPanelTemplate으로 나누어 볼 수 있습니다.

 

글로만 따라가다 보면 이해가 안될수 있기 때문에 예제를 통해 좀 더 자세히 파헤쳐 보겠습니다. ObservableObject.cs 파일과 MainWindowViewModel.cs 파일 2개를 만들어 주겠습니다. 

 

using System.ComponentModel;

namespace TemplateExample
{
    public class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        protected void OnPropertyChanged(string? name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

MainWindowViewModel에서 ItemsSource로 사용하기 위한 랜덤 색깔들을 더미 데이터로 만들어 주었습니다.

 

using System;
using System.Collections.Generic;
using System.Drawing;

namespace TemplateExample
{
    public class MainWindowViewModel : ObservableObject
    {
        private Random random;

        private List<string> _colors;
        public List<string> Colors
        {
            get => _colors;
            set
            {
                _colors = value;
                OnPropertyChanged();
            }
        }

        public MainWindowViewModel()
        {
            random = new();
            _colors = new();
            SetRandomColors();
        }

        private void SetRandomColors()
        {
            for(int i = 0; i < 10; i++)
            {
                Color randomColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256));
                Colors.Add(randomColor.ToString());
            }
        }
    }
}

MainWindow에서 화면에 데이터를 출력해주기 위해서는 DataContext를 지정해 주어야겠죠?

using System.Windows;

namespace TemplateExample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += (s, e) =>
            {
                this.DataContext = new MainWindowViewModel();
            };
        }
    }
}

여기까지 오늘의 Template과 Presenter에 대해 알아보기 위한 사전 작업을 완료했습니다. 그렇다면 본격적으로 Template과 Presenter에 대해서 살펴볼까요?

 

2-1. ControlTemplate

System.Windows.Controls.Control 클래스는 WPF에서 컨트롤의 기본이 되는 클래스입니다. 이 클래스에는 Template 속성이 있는데, 이 Template 속성에 정의하는 Template이 ControlTemplate입니다. 즉 Control의 그 자체의 외형을 정의하는 요소입니다. Control은 자체적으로 외형을 직접 그리지 않고, 외형은 전적으로 ControlTemplate에 의해 결정된다.

ContentControl과 ItemsControl은 Control 클래스를 상속받는다.

ContentControl : 자신의 자식으로 Content 하나를 받을 수 있는 컨트롤.

ItemsControl : 리스트 또는 컬렉션처럼 데이터를 통해 다수의 반복되는 자식들을 가지는 컨트롤.

ControlTemplate 정의를 할 때 배치되는 Presenter는 무엇일까요?

Presenter에는 크게 2가지로 ContentPresenter와 ItemsPresenter가 있다.

 

2-1-1 ContentPresenter

ContentControl의 ControlTemplate을 정의했을 때 ContentControl에 정의되는 Content를 어딘가에 표시되도록 해야 합니다. 아무 컨트롤이나 사용해서 Content를 표시되게 처리할 수 있을까요? 그렇지 않습니다. Content에는 다양한 타입의 객체가 생성될 수 있기에 아무 컨트롤이나 사용해서 처리가 불가능합니다. 그렇다면 이때 필요한 것이 무엇일까요?

이때 필요한 것이 ContentPresenter로 Content를 표시하는 역할을 하는 엘리먼트이다. ContentControl의 ControlTemplate안에 ContentPresenter배치하게 되면, 이후 Content는 그 ContentPresenter의 자식으로 배치되어, Content의 내용을 표시할 위치를 표시하는 역할을 하게 된다.

 

2-1-2 ItemsPresenter

ContentPresenter와 마찬가지로 반복되는 리스트나 컬렉션 등 자식들이 표시되어야 할 부분에 ItemsPresenter를 배치시키면 해당 위치에 자식들이 나타나게 된다.

 

위에서 Control의 종류는 ContentControl과 ItemsControl이 있었는데요 오늘은 ItemsControl인 ListView를 통해 예제를 다뤄보도록 하겠습니다. 위에서 이미 언급했듯이 Control 클래스를 상속받는 ItemsControl에는 Template이라는 속성이 있습니다. 여기서 Template 속성에 정의하는 ControlTemplate을 통해 ListView Control 자체의 외형을 정의할 겁니다.

<Window x:Class="TemplateExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <ListView ItemsSource="{Binding Colors}">
            <ListView.Template>
                <ControlTemplate TargetType="{x:Type ListView}">
                    
                </ControlTemplate>
            </ListView.Template>
        </ListView>    
    </Grid>
</Window>

 

여기서 아무 컨트롤이나 사용해서 데이터를 출력할 수 있을까요? 다음과 같이 작성하고 실행해 보겠습니다.

 

<ListView ItemsSource="{Binding Colors}">
    <ListView.Template>
        <ControlTemplate TargetType="{x:Type ListView}">
           <TextBlock Text="{Binding}"/> 
        </ControlTemplate>
    </ListView.Template>
</ListView>

아무 컨트롤이나 사용해 데이터를 표시하려고 했지만 제대로 출력이 되질 않습니다. 그 이유는 Content에는 다양한 타입의 객체가 생성될 수 있기 때문입니다. 

아무 컨트롤이나 사용했을 때

 

 

그렇다면 이제 ItemsPresenter를 통해 Content의 내용을 표시할 위치를 표시해 볼까요?

 <ListView ItemsSource="{Binding Colors}">
    <ListView.Template>
        <ControlTemplate TargetType="{x:Type ListView}">
            <ItemsPresenter />
        </ControlTemplate>
    </ListView.Template>
</ListView>

 

ItemsPresenter를 사용했을 때

ItemsPresenter를 사용했더니 이제서야 제대로 출력이 되는 모습을 볼 수 있습니다. 이렇듯 ControlTemplate을 통해 데이터를 표시하고자 할때는 반드시 Presenter를 통해 Content의 내용을 표시할 위치를 표시해 주어야 합니다.

 

ControlTemplate에서 Presenter에 대해 알아봤지만 아직 이 Control 자체의 외형에 대해 정의하지 않았죠? 지금부터 간단하게 정의해 볼게요. ListView Control 자체의 외형에 대해서 간단하게 정의해 보았습니다.

<ListView ItemsSource="{Binding Colors}">
    <ListView.Template>
        <ControlTemplate TargetType="{x:Type ListView}">
            <Grid>
                <UniformGrid Columns="4">
                    <Border Style="{StaticResource SQUARE}"/>
                    <Border Style="{StaticResource SQUARE}"/>
                    <Border Style="{StaticResource SQUARE}"/>
                    <Border Style="{StaticResource SQUARE}"/>
                </UniformGrid>
                <ItemsPresenter/>
            </Grid>
        </ControlTemplate>
    </ListView.Template>
</ListView>

어떤가요? 아래의 모습을 보면 아까전의 흰 배경 모습과는 다르게 ListView Control의 외형 자체를 정의해서 4개의 Border가 나타나도록 만들었습니다.

ListView Control 자체의 외형을 정의한 모습


2-2. DataTemplate

위를 통해 알 수 있듯이 ControlTemplate은 Control 그 자체의 외형을 정의하는 요소라 하였습니다. 그렇다면 이번에는 Control 자체의 외형이 아닌 Content의 내용을 출력할 외형을 결정하는 방법에는 어떤 것을 사용해야 할까요? 이때 필요한 것이 바로 DataTemplate입니다.

 

ContentControl의 ContentTemplate 속성에 정의되는 Template이 바로 DataTemplate입니다. DataTemplate 정의를 통해 여러 가지 방법으로 Content를 표현할 수 있습니다. 이 말인즉슨 Content(Data)를 출력할 방법을 결정할 수 있는 Template입니다.

 

ItemsControl에는 ItemsSource라는 속성을 통해 컬렉션을 설정하고 이 컬렉션 각각의 Item에 대해 화면에 출력합니다. 각각의 Item의 데이터를 다양한 모양과 형식으로 출력하려면 어떻게 해야 할까요?

 

ItemsControl의 ItemTemplate 속성에 정의되는 DataTemplate을 통해 외형을 정해두면 컬렉션의 Item이 DataTemplate이 정한 외형으로 표현된다. 특히 리스트나 트리 같이 반복적으로 데이터를 표시하는 컨트롤에서 데이터를 표시하는 방법을 커스터 마이징 할 수 있는 중요한 도구가 바로 이 DataTemplate입니다.

 

 

예제를 통해 알아볼게요. 예제에서 Border 배경을 어둡게 변경하였더니 글자가 제대로 보이지 않아서 글자 색도 변경하고 싶고 크기도 조금 키우고 싶습니다. 이럴때는 어떻게 해야 할까요? 이럴때 필요한것이 Control 자체의 외형이 아닌 Content의 내용을 출력할 외형을 결정하는 방법인 DataTemplate을 활용하는 것입니다. 글자색을 하얀색으로 변경하고 크기도 조금 키워볼게요.

<ListView.ItemTemplate>
    <DataTemplate>
        <!--<DataTemplate.Resources>
             <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Foreground" Value="White"/>
                <Setter Property="FontSize" Value="15"/>
            </Style>
        </DataTemplate.Resources>-->
                  
        <TextBlock Text="{Binding}"
                   Foreground="White"
                   FontSize="15"/>
    </DataTemplate>
</ListView.ItemTemplate>

 

ListView의 ItemTemplate 속성에 정의하는 DataTemplate 속성을 통해 Content의 출력할 외형을 변경할 수 있습니다. 과연 어떻게 변경이 되었을까요?

성공적으로 TextBlock의 Foreground와 FontSize가 변경된 것을 볼 수 있습니다. 주석 처리 되어 있는 DataTemplate.Resources를 통해 Style을 만들어 적용하는 방법도 있습니다.


2-3. ItemsPanelTemplate

ItemsControl은 DataTemplate을 통해 반복적인 데이터를 표현한다 했는데, 어떤 모양으로 반복시킬 것인지는 어떻게 정할까요? 데이터는 DataTemplate을 통해 정했는데, 전체적인 Item을 어떤 모양으로 배치할지 어떻게 정할까요?

어떤 때는 가로로 배치하기도 하고, 어떤 때는 세로로 배치하고, 경우에 따라 원형으로 배치할 수도 있습니다. 이럴 때 ItemsControl의 ItemsPanel 속성에 정의되는 Template이 ItemsPanelTemplate입니다.

 

ItemsPanelTemplate에는 Panel을 상속받은 레이아웃 컨트롤을 정의해 이 레이아웃이 정하는 방식대로 Item을 출력합니다. 즉 DataTemplate에 의해 생성된 엘리먼트들이 ItemsPanelTemplate에 의해 생성된 Panel의 자식으로 배치된다는 뜻입니다. 따라서 ItemsPanelTemplate은 ContentControl에서는 사용되지 않고 ItemsControl에만 사용됩니다.

 

  ControlTemplate DataTemplate ItemsPanelTemplate
Control Control.Template X X
ContentControl ContentControl.Template.ContentPresenter ContentControl.ContentTemplate X
ItemsControl ItemsControl.Template, ItemsPresenter ItemsControl.ItemTemplate ItemsControl.ItemsPanel

 

ItemsPanelTemplate도 예제를 살펴보아야겠죠? ItemsPanel은 ControlTemplate과 DataTemplate보다는 간결합니다. ListView의 ItemsPanel 속성에 정의하는 ItemsPanelTemplate을 사용하면 됩니다. 즉 StackPanel, DockPanel, WrapPanel··· 등등 Panel의 종류라면 가능합니다. WrapPanel을 통해 나타내 보겠습니다.

 

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapPanel />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

 

DataTemplate에서 생성된 TextBlock 엘리먼트들이 ItemsPanelTemplate에 의해 생성된 Panel의 자식으로 잘 배치가 될까요?

 

ItemsPanelTemplate 적용 전(좌), 후{우)

StackPanel처럼 쌓여있던 TextBlock들이 ItemsPanelTemplate에 WrapPanel을 적용 후 방향에 따라 순서대로 배치되게 변경되었습니다.


3. Binding

Template에 정의된 엘리먼트들은 출력을 위해서 부모에게서 데이터를 받아와야 하는데 어떤 것이 부모가 될지 모르기에 Template을 정의할 때 필연적으로 Binding 표현식을 사용할 수밖에 없습니다.

Binding어떤 요소와 또 다른 요소를 연결해 데이터를 전달 혹은 공유하는 방법을 말합니다. Binding은 데이터를 제공하는 소스와 제공받을 타겟을 설정하는데, 타겟은 당연히 Binding 표현식이 사용되는 속성이 됩니다.

ElementName 명시적으로 Name을 가진 엘리먼트
Source VisualTree에 없는 리소스나 비 UI 엘리먼트 참조
RelativeSource 자신의 위치부터 상대적인 위치에 있는 부모나 동료 참조
X 암시적으로 DataContext를 참조하며, 상위로 올라가면서 참조

 

3-1. Binding - Source

Binding 표현식의 Source는 Path를 통해 Source의 자세한 속성들을 참조할 수 있습니다. Template에서는 RelativeSource 또는 비워둡니다. ElementName과 Source 또한 명시적 참조가 가능하지만 RelativeSource와 비워두는 것은 상대적과 암시적 참조가 모두 가능하기 때문입니다.

특히 ItemsControl.ItemTemplate에 설정되는 DataTemplate에서는 내부적으로 Item의 데이터 전달을 DataContext를 통해 많이 하기 때문에 일반적으로 비워두는 방법을 많이 사용합니다.

Binding의 특별한 표현식 TemplateBindning?

Binding에는 TemplateBinding이라는 특별한 표현식이 있습니다. 어떤 속성 = "{TemplateBinding 속성 이름}" 형태로 표현합니다. 이는 ControlTemplate안에서만 사용하는 방식으로 Template 속성에 자신을 설정한 부모의 속성 값을 받아오는 문법입니다.

TemplateBinding은 어떤 속성 = "{Binding RelativeSource={RelativeSource TemplatedParent}}"와 같은 표현이지만 ControlTemplate을 위해 최적화되고 가볍게 만들어졌다고 합니다. ControlTemplate에서 일반적인 Binding 표현식을 사용해도 되지만, Binding의 특별한 기능을 사용할 필요가 없다면 TemplateBinding을 사용하는 것이 편하고 가볍습니다.

 

 

마지막으로 Binding 표현식에 대한 예제를 보고 마무리 하도록 하겠습니다. ListView에 대한 Style을 설정해주었습니다. 그리고 Border를 보면 다르게 적용된 Border 2가지가 보입니다.

 

첫번째 Border는 TemplateBinding을 사용해 부모의 Background 값을 받아오고 있습니다. 즉 자신을 설정한 부모인 ListView의 Background를 받아오게 되는 것입니다. ListView.Style에서 DeepPink로 설정해두었기에 이 색상으로 변경될 것입니다.

 

두번째 Border는 RelativeSource의 Mode인 TemplatedParent를 통해 상대적인 위치에 있는 부모를 참조하였습니다. 즉 자신의 위치부터 상대적인 위치의 부모인 UniformGrid의 Background를 참조하게 되는 것입니다. 그렇기에 Red 색상으로 변경될 것입니다.

<ListView ItemsSource="{Binding Colors}">
    <ListView.Style>
        <Style TargetType="{x:Type ListView}">
            <Setter Property="Background" Value="DeepPink"/>
            <Setter Property="Foreground" Value="Blue"/>
        </Style>
    </ListView.Style>
            
    <ListView.Template>
        <ControlTemplate TargetType="{x:Type ListView}">
            <Grid>
                <UniformGrid Columns="4" Background="Red">
                    <Border Style="{StaticResource SQUARE}"/>
                    <Border Background="{TemplateBinding Background}"/>
                    <Border Style="{StaticResource SQUARE}"/>
                    <Border Background="{Binding RelativeSource={RelativeSource TemplatedParent}}"/>
                </UniformGrid>
                <ItemsPresenter />
            </Grid>
        </ControlTemplate>
    </ListView.Template>

    <ListView.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}"
                       Foreground="White"
                       FontSize="15"/>
        </DataTemplate>
    </ListView.ItemTemplate>

    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>

 


 

글을 마치면서

이상으로 Template과 Presenter, Control과 Binding을 비롯해 WPF에서 정말 중요한 개념들에 대해서 정리하고 공부해 보았습니다. 이전까지 위의 내용들에 대해서 간단하게 알고 있거나 제대로 알지 못했었는데 이번 포스팅을 통해 더욱 자세하게 알게 된 것 같습니다. 다음 포스팅에서는 또 다른 주제를 공부하고 정리해보겠습니다. 감사합니다.

 

 

+ P.S

2021년 8월 13일

제가 쓴 글을 보면서 글로만 봤을 때 잘 이해가 가지 않을수도 있을것 같아 이해를 돕고자 간단하지만 예제를 추가해 글 중간 중간 첨부하였습니다.

댓글