[WPF] DataTemplate, DataTemplateSelector 사용하기

[WPF] DataTemplate, DataTemplateSelector 사용하기

이번 글에서는 DataTemplateDataTemplateSelector에 대해 알아보겠습니다. DataTemplate는 말 그대로 데이터를 보여주는 틀이라고 보시면 됩니다. WPF에 이 데이터는 화면에 어떻게 그리는지 알려주는 설계도와 같습니다. DataTemplate를 사용하면 ListBox에 텍스트로 표시되는 것을 정해진 틀로 바꿔 보여줄 수 있습니다.

DataTemplate

ListBox로 텍스트만 표시할 수 있지만 DataTemplate로 미리 정의하면 채팅 프로그램처럼 표시할 수 있습니다.

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

    <Window.Resources>
        <DataTemplate DataType="{x:Type chat:ChatMessage}">
            <Border Background="LightGray"
                    CornerRadius="10"
                    Padding="10"
                    Margin="5">
                <StackPanel>
                    <TextBlock Text="{Binding Text}"
                               FontSize="14"/>
                    <TextBlock Text="{Binding Time, StringFormat=t}"
                               FontSize="10"
                               Foreground="Gray"/>
                </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <ListBox Grid.Row="0"
                 Grid.Column="0"
                 ItemsSource="{Binding Messages}"/>
    </Grid>
</Window>

Window.Resources안에 DataTemplate를 보시면 ChatMessage에 대한 데이터 템플릿이 정의 되어 있습니다.

public class MainViewModel : BindableBase
{
    private ObservableCollection<ChatMessage> messages = new ObservableCollection<ChatMessage>();
    public ObservableCollection<ChatMessage> Messages { get => messages; set => SetProperty(ref messages, value); }

    #region constructor
    public MainViewModel()
    {
        Messages.Add(new ChatMessage()
        {
            Text = "hi",
        });
        Messages.Add(new ChatMessage()
        {
            Text = "nice too meet you",
        });
    }
    #endregion
}

위 코드는 MainViewModel.cs 코드입니다. ListBox에서는 Messages에 들어있는 ChatMessage에 대해서는 xaml에 정의된 DataTemplate대로 표시할 것입니다.

public class ChatMessage : BindableBase
{
    private DateTime time = DateTime.Now;
    public DateTime Time { get => time; set => SetProperty(ref time, value); }

    private string text = "";
    public string Text { get => text; set => SetProperty(ref text, value); }
}

ChatMessage 클래스입니다. 현재 시간과 텍스트 데이터를 담을 수 있습니다.

위 코드를 실행하면 아래와 같이 프로그램이 표시됩니다.

DataTemplateSelector

DataTemplateSelector는 정의된 DataTemplate중에 어떤 것을 선택할 것인지 결정 할 때 사용됩니다. 같은 데이터인데 조건에 따라 다른 디자인을 보여주고 싶은 상황이 대표적입니다. 위와 같은 채팅 화면에서 내가 보낸 채팅 메시지는 노랑색으로 표시할 수 있습니다. 이 경우 같은 ChatMessage지만 DataTemplateSelector를 사용하여 DataTemplate를 선택할 수 있습니다.

우선 ChatMessage에 내가 보낸 채팅 메시지인지를 구별하는 IsMe 속성을 만들어 줍니다.

public class ChatMessage : BindableBase
{
    private DateTime time = DateTime.Now;
    public DateTime Time { get => time; set => SetProperty(ref time, value); }

    private string text = "";
    public string Text { get => text; set => SetProperty(ref text, value); }

    private bool isMe = false;
    public bool IsMe { get => isMe; set => SetProperty(ref isMe, value); }
}

그 다음 리소스 딕셔너리를 하나 생성하여 DataTemplates.xaml로 이름을 만들어 준 다음 색상이 다른 DataTemplate를 두 개 만들어 줍니다.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:chat="clr-namespace:WpfDataTemplate.Chat">

    <DataTemplate x:Key="YourChatMessage"
                  DataType="{x:Type chat:ChatMessage}">
        <Border Background="LightGray"
                CornerRadius="10"
                Padding="10"
                Margin="5">
            <StackPanel>
                <TextBlock Text="{Binding Text}"
                           FontSize="14"/>
                <TextBlock Text="{Binding Time, StringFormat=t}"
                           FontSize="10"
                           Foreground="Gray"/>
            </StackPanel>
        </Border>
    </DataTemplate>

    <DataTemplate x:Key="MyChatMessage"
                  DataType="{x:Type chat:ChatMessage}">
        <Border Background="Yellow"
                CornerRadius="10"
                Padding="10"
                Margin="5">
            <StackPanel>
                <TextBlock Text="{Binding Text}"
                           FontSize="14"/>
                <TextBlock Text="{Binding Time, StringFormat=t}"
                           FontSize="10"
                           Foreground="Gray"/>
            </StackPanel>
        </Border>
    </DataTemplate>

    <chat:ChatMessageSelector x:Key="ChatMessageSelector"
                              MyTemplate="{StaticResource MyChatMessage}"
                              YourTemplate="{StaticResource YourChatMessage}"/>
</ResourceDictionary>

각각의 데이터 템플릿들은 YourChatMessageMyChatMessage로 이름을 지정하였습니다. 또한 중요한 것은 아래 ChatMessageSelector가 입력되었다는 것입니다. 이것은 아래 DataTemplateSelector를 상속받는 클래스에 대한 요소입니다.

DataTemplateSelector를 상속받는 클래스 하나 만들어 줍니다. 이름은 ChatMessageSelector입니다.

public class ChatMessageSelector : DataTemplateSelector
{
    public DataTemplate? MyTemplate { get; set; } = null;
    public DataTemplate? YourTemplate { get; set; } = null;

    public override DataTemplate? SelectTemplate(object item, DependencyObject container)
    {
        // item은 현재 그려질 데이터 하나 (Message 객체)
        if (item is ChatMessage message)
        {
            if (message.IsMe)
                return MyTemplate;
            else
                return YourTemplate;
        }

        return base.SelectTemplate(item, container);
    }
}

보시면 IsMe를 통해 어떤 데이터 템플릿을 반환할 지 결정합니다. 이제 MainViewModel에 아래와 같이 IsMe 속성을 결정해줍니다.

public class MainViewModel : BindableBase
{
    private ObservableCollection<ChatMessage> messages = new ObservableCollection<ChatMessage>();
    public ObservableCollection<ChatMessage> Messages { get => messages; set => SetProperty(ref messages, value); }

    #region constructor
    public MainViewModel()
    {
        Messages.Add(new ChatMessage()
        {
            Text = "hi",
            IsMe = false,
        });
        Messages.Add(new ChatMessage()
        {
            Text = "nice too meet you",
            IsMe = true,
        });
    }
    #endregion
}

이제 실행하여 확인해봅니다.

이상으로 DataTemplate과 DataTemplateSelector에 대해 알아보았습니다.


github