Desktop UI

Data Templates — Візуалізація об'єктів у WPF

Перетворення C#-об'єктів у красивий UI автоматично через DataTemplate — від простих шаблонів до DataTemplateSelector та HierarchicalDataTemplate

Data Templates: Візуалізація об'єктів

Вступ

Уявіть, що у вас є клас Person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

І ви хочете відобразити його у UI:

<ContentControl Content="{Binding CurrentPerson}"/>

Що ви побачите? Рядок "MyApp.Models.Person" — результат виклику ToString().

Проблема: WPF не знає, як відобразити ваш об'єкт. Він знає тільки, що це об'єкт типу Person, і викликає ToString() за замовчуванням.

Як показати щось красиве — ім'я, вік, email, можливо фото? Як зробити так, щоб кожен Person відображався як картка з аватаром та інформацією?

Рішення: Data Templates — шаблони, що визначають, як WPF має візуалізувати об'єкти певного типу.

Для кого ця стаття? Якщо ви вже знайомі з Data Binding та Value Converters, ця стаття покаже, як автоматично перетворювати C#-об'єкти у красивий UI без ручного створення контролів у code-behind.

Проблема: ToString() замість красивого UI

Розберемо детально, чому об'єкти відображаються як рядки.

Експеримент: Відображення об'єкта

ViewModel:

public class MainViewModel : INotifyPropertyChanged
{
    private Person _currentPerson;
    
    public Person CurrentPerson
    {
        get => _currentPerson;
        set
        {
            _currentPerson = value;
            OnPropertyChanged();
        }
    }
    
    public MainViewModel()
    {
        CurrentPerson = new Person
        {
            FirstName = "Іван",
            LastName = "Петренко",
            Age = 25,
            Email = "ivan@example.com"
        };
    }
    
    // ... INotifyPropertyChanged implementation
}

XAML (без DataTemplate):

<Window x:Class="MyApp.MainWindow"
        DataContext="{Binding Source={StaticResource viewModel}}">
    <StackPanel Margin="20">
        <TextBlock Text="Поточна особа:"/>
        <ContentControl Content="{Binding CurrentPerson}"/>
    </StackPanel>
</Window>

Результат: ContentControl показує "MyApp.Models.Person" — результат ToString().

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Чому так відбувається?

Loading diagram...
sequenceDiagram
    participant Binding as Binding Engine
    participant CC as ContentControl
    participant Person as Person Object
    participant WPF as WPF Rendering
    
    Note over Binding,WPF: Ініціалізація
    Binding->>CC: Content = Person instance
    CC->>WPF: Як відобразити Person?
    WPF->>WPF: Шукає DataTemplate для Person
    WPF->>WPF: DataTemplate не знайдено
    WPF->>Person: ToString()
    Person-->>WPF: "MyApp.Models.Person"
    WPF->>CC: Відображає рядок
    
    style WPF fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Person fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Процес:

  1. ContentControl.Content отримує об'єкт Person
  2. WPF шукає DataTemplate для типу Person
  3. Якщо DataTemplate не знайдено → викликає Person.ToString()
  4. Відображає результат ToString() як текст

Обхідний шлях (антипатерн)

Можна перевизначити ToString():

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    
    public override string ToString()
    {
        return $"{FirstName} {LastName}, {Age} років, {Email}";
    }
}

Результат: ContentControl показує "Іван Петренко, 25 років, ivan@example.com".

Але це антипатерн:

❌ Обмежене форматування

Тільки текст. Не можна додати кольори, іконки, кнопки, зображення.

❌ Один формат

ToString() повертає один рядок. Не можна мати різні представлення для різних контекстів.

❌ Змішування логіки

Модель не має знати, як вона відображається. Це порушує принцип розділення відповідальності.

❌ Не масштабується

Для складних об'єктів (з вкладеними об'єктами, колекціями) ToString() стає нечитабельним.

DataTemplate: Шаблон візуалізації

DataTemplate — це XAML-шаблон, що визначає, як WPF має відображати об'єкти певного типу.

Анатомія DataTemplate

<DataTemplate DataType="{x:Type local:Person}">
    <!-- XAML розмітка для відображення Person -->
    <StackPanel>
        <TextBlock Text="{Binding FirstName}"/>
        <TextBlock Text="{Binding LastName}"/>
        <TextBlock Text="{Binding Age}"/>
    </StackPanel>
</DataTemplate>

Ключові елементи:

  • DataTemplate — контейнер для шаблону
  • DataType="{x:Type local:Person}" — тип об'єкта, для якого цей шаблон
  • Всередині — звичайний XAML з Binding до властивостей об'єкта
  • DataContext всередині шаблону — це сам об'єкт (Person)

Перший робочий приклад

Визначення DataTemplate у ресурсах:

<Window.Resources>
    <DataTemplate DataType="{x:Type local:Person}">
        <Border Background="LightBlue" 
                BorderBrush="DarkBlue" 
                BorderThickness="1" 
                Padding="10" 
                CornerRadius="5">
            <StackPanel>
                <TextBlock Text="{Binding FirstName}" 
                           FontWeight="Bold" 
                           FontSize="16"/>
                <TextBlock Text="{Binding LastName}" 
                           FontSize="14"/>
                <TextBlock Text="{Binding Age, StringFormat='Вік: {0} років'}" 
                           FontSize="12" 
                           Foreground="Gray"/>
                <TextBlock Text="{Binding Email}" 
                           FontSize="12" 
                           Foreground="DarkBlue"/>
            </StackPanel>
        </Border>
    </DataTemplate>
</Window.Resources>

<StackPanel Margin="20">
    <TextBlock Text="Поточна особа:"/>
    <ContentControl Content="{Binding CurrentPerson}"/>
</StackPanel>

Результат: Замість "MyApp.Models.Person" ви побачите красиву картку з ім'ям, прізвищем, віком та email.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Що відбувається під капотом?

Loading diagram...
sequenceDiagram
    participant Binding as Binding Engine
    participant CC as ContentControl
    participant Person as Person Object
    participant WPF as WPF Rendering
    participant DT as DataTemplate
    
    Note over Binding,DT: Ініціалізація
    Binding->>CC: Content = Person instance
    CC->>WPF: Як відобразити Person?
    WPF->>WPF: Шукає DataTemplate для Person
    WPF->>DT: Знайдено DataTemplate з DataType=Person
    DT->>WPF: Повертає XAML шаблон
    WPF->>WPF: Створює UI з шаблону
    WPF->>WPF: Встановлює DataContext = Person
    WPF->>CC: Відображає створений UI
    
    style WPF fill:#10b981,stroke:#059669,color:#ffffff
    style DT fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Процес:

  1. ContentControl.Content отримує об'єкт Person
  2. WPF шукає DataTemplate з DataType="{x:Type Person}"
  3. Знаходить шаблон у ресурсах
  4. Створює UI з шаблону (Border → StackPanel → TextBlock-и)
  5. Встановлює DataContext створеного UI = об'єкт Person
  6. Binding-и всередині шаблону прив'язуються до властивостей Person

Implicit vs Explicit DataTemplates

DataTemplate може бути неявним (implicit) або явним (explicit).

Implicit DataTemplate (без x:Key)

Визначення: DataTemplate без x:Key — автоматично застосовується до всіх об'єктів цього типу.

<Window.Resources>
    <!-- Implicit — без x:Key -->
    <DataTemplate DataType="{x:Type local:Person}">
        <Border Background="LightBlue" Padding="10">
            <TextBlock Text="{Binding FirstName}"/>
        </Border>
    </DataTemplate>
</Window.Resources>

<!-- Автоматично використовує DataTemplate для Person -->
<ContentControl Content="{Binding CurrentPerson}"/>
<ContentControl Content="{Binding AnotherPerson}"/>
<ListBox ItemsSource="{Binding People}"/>  <!-- Кожен Person відображається через шаблон -->

Переваги:

  • ✅ Автоматичне застосування — не потрібно вказувати шаблон вручну
  • ✅ Консистентність — всі об'єкти типу Person виглядають однаково
  • ✅ Менше коду — один шаблон для всього проєкту

Коли використовувати:

  • Коли є один стандартний спосіб відображення об'єкта
  • Для моделей, що використовуються у багатьох місцях
  • Для автоматичної візуалізації у колекціях (ListBox, ComboBox)

Explicit DataTemplate (з x:Key)

Визначення: DataTemplate з x:Key — застосовується вручну через ContentTemplate або ItemTemplate.

<Window.Resources>
    <!-- Explicit — з x:Key -->
    <DataTemplate x:Key="personCardTemplate" DataType="{x:Type local:Person}">
        <Border Background="LightGreen" Padding="10">
            <StackPanel>
                <TextBlock Text="{Binding FirstName}" FontWeight="Bold"/>
                <TextBlock Text="{Binding Email}"/>
            </StackPanel>
        </Border>
    </DataTemplate>
    
    <DataTemplate x:Key="personCompactTemplate" DataType="{x:Type local:Person}">
        <TextBlock Text="{Binding FirstName}"/>
    </DataTemplate>
</Window.Resources>

<!-- Вручну вказуємо шаблон -->
<ContentControl Content="{Binding CurrentPerson}" 
                ContentTemplate="{StaticResource personCardTemplate}"/>

<ContentControl Content="{Binding AnotherPerson}" 
                ContentTemplate="{StaticResource personCompactTemplate}"/>

Переваги:

  • ✅ Гнучкість — різні шаблони для різних контекстів
  • ✅ Контроль — явно вказуєте, який шаблон використовувати
  • ✅ Множинність — кілька шаблонів для одного типу

Коли використовувати:

  • Коли потрібні різні представлення одного об'єкта (картка, компактний вигляд, детальний вигляд)
  • Для спеціальних випадків (наприклад, "вибраний елемент" виглядає інакше)
  • Коли шаблон використовується тільки в одному місці

Порівняльна таблиця

АспектImplicit DataTemplateExplicit DataTemplate
x:Key❌ Немає✅ Є
ЗастосуванняАвтоматичне для всіх об'єктів типуВручну через ContentTemplate/ItemTemplate
Область діїВсі об'єкти типу у scope ресурсівТільки де явно вказано
Кількість шаблонівОдин на типНеобмежена кількість
Use CaseСтандартне відображенняСпеціальні випадки, варіації

ContentTemplate vs ItemTemplate

Різні контроли використовують DataTemplate по-різному.

ContentControl.ContentTemplate

ContentControl (Button, Label, GroupBox) відображає один об'єкт через ContentTemplate.

Синтаксис:

<ContentControl Content="{Binding CurrentPerson}" 
                ContentTemplate="{StaticResource personTemplate}"/>

Приклад: Картка профілю

<Window.Resources>
    <DataTemplate x:Key="profileCardTemplate">
        <Border Background="#F5F5F5" 
                BorderBrush="#CCCCCC" 
                BorderThickness="1" 
                Padding="20" 
                CornerRadius="10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                
                <!-- Аватар (placeholder) -->
                <Ellipse Width="80" 
                         Height="80" 
                         Fill="#2196F3" 
                         Grid.Column="0" 
                         Margin="0,0,20,0"/>
                
                <!-- Інформація -->
                <StackPanel Grid.Column="1" VerticalAlignment="Center">
                    <TextBlock FontSize="20" FontWeight="Bold">
                        <TextBlock.Text>
                            <MultiBinding StringFormat="{}{0} {1}">
                                <Binding Path="FirstName"/>
                                <Binding Path="LastName"/>
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                    <TextBlock Text="{Binding Age, StringFormat='Вік: {0} років'}" 
                               FontSize="14" 
                               Foreground="Gray" 
                               Margin="0,5,0,0"/>
                    <TextBlock Text="{Binding Email}" 
                               FontSize="14" 
                               Foreground="#2196F3" 
                               Margin="0,5,0,0"/>
                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>
</Window.Resources>

<ContentControl Content="{Binding CurrentPerson}" 
                ContentTemplate="{StaticResource profileCardTemplate}"/>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

ItemsControl.ItemTemplate

ItemsControl (ListBox, ComboBox, ListView) відображає колекцію об'єктів через ItemTemplate.

Синтаксис:

<ListBox ItemsSource="{Binding People}" 
         ItemTemplate="{StaticResource personTemplate}"/>

Приклад: Список контактів

<Window.Resources>
    <DataTemplate x:Key="contactItemTemplate">
        <Border Background="White" 
                BorderBrush="#E0E0E0" 
                BorderThickness="0,0,0,1" 
                Padding="10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                
                <!-- Аватар -->
                <Ellipse Width="40" 
                         Height="40" 
                         Fill="#4CAF50" 
                         Grid.Column="0" 
                         Margin="0,0,10,0"/>
                
                <!-- Інформація -->
                <StackPanel Grid.Column="1" VerticalAlignment="Center">
                    <TextBlock FontWeight="Bold" FontSize="14">
                        <TextBlock.Text>
                            <MultiBinding StringFormat="{}{0} {1}">
                                <Binding Path="FirstName"/>
                                <Binding Path="LastName"/>
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                    <TextBlock Text="{Binding Email}" 
                               FontSize="12" 
                               Foreground="Gray"/>
                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>
</Window.Resources>

<ListBox ItemsSource="{Binding People}" 
         ItemTemplate="{StaticResource contactItemTemplate}"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>

ViewModel:

public class MainViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Person> People { get; set; }
    
    public MainViewModel()
    {
        People = new ObservableCollection<Person>
        {
            new Person { FirstName = "Іван", LastName = "Петренко", Email = "ivan@example.com" },
            new Person { FirstName = "Марія", LastName = "Коваленко", Email = "maria@example.com" },
            new Person { FirstName = "Олександр", LastName = "Шевченко", Email = "alex@example.com" }
        };
    }
    
    // ... INotifyPropertyChanged implementation
}

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Порівняльна таблиця

АспектContentTemplateItemTemplate
КонтролиContentControl, Button, LabelListBox, ComboBox, ListView
КількістьОдин об'єктКолекція об'єктів
ВластивістьContentTemplateItemTemplate
DataContextОб'єкт з ContentКожен елемент колекції
Use CaseКартка профілю, деталі об'єктаСписки, меню, випадаючі списки

DataTemplateSelector: Програмний вибір шаблону

DataTemplateSelector дозволяє вибирати шаблон програмно залежно від даних.

Коли потрібен DataTemplateSelector?

👥 Різні типи об'єктів

Колекція містить об'єкти різних типів (TextMessage, ImageMessage, LinkMessage).

⭐ Різні категорії

VIP клієнти відображаються інакше, ніж звичайні клієнти.

📊 Умовне форматування

Елементи з високим пріоритетом — червоні, з низьким — зелені.

🔀 Складна логіка

Вибір шаблону залежить від кількох властивостей або складних умов.

Створення DataTemplateSelector

Крок 1: Створити клас-селектор

using System.Windows;
using System.Windows.Controls;

namespace MyApp.Selectors
{
    public class MessageTemplateSelector : DataTemplateSelector
    {
        // Шаблони (встановлюються з XAML)
        public DataTemplate TextMessageTemplate { get; set; }
        public DataTemplate ImageMessageTemplate { get; set; }
        public DataTemplate LinkMessageTemplate { get; set; }
        
        // Метод вибору шаблону
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            // Перевірка типу об'єкта
            return item switch
            {
                TextMessage => TextMessageTemplate,
                ImageMessage => ImageMessageTemplate,
                LinkMessage => LinkMessageTemplate,
                _ => base.SelectTemplate(item, container)
            };
        }
    }
}

Крок 2: Визначити моделі

public abstract class Message
{
    public string Sender { get; set; }
    public DateTime Timestamp { get; set; }
}

public class TextMessage : Message
{
    public string Text { get; set; }
}

public class ImageMessage : Message
{
    public string ImageUrl { get; set; }
    public string Caption { get; set; }
}

public class LinkMessage : Message
{
    public string Url { get; set; }
    public string Title { get; set; }
}

Крок 3: Визначити шаблони та селектор у XAML

<Window.Resources>
    <!-- Шаблон для текстових повідомлень -->
    <DataTemplate x:Key="textMessageTemplate">
        <Border Background="#E3F2FD" 
                Padding="10" 
                Margin="5" 
                CornerRadius="5">
            <StackPanel>
                <TextBlock Text="{Binding Sender}" 
                           FontWeight="Bold" 
                           FontSize="12"/>
                <TextBlock Text="{Binding Text}" 
                           FontSize="14" 
                           TextWrapping="Wrap" 
                           Margin="0,5,0,0"/>
                <TextBlock Text="{Binding Timestamp, StringFormat='HH:mm'}" 
                           FontSize="10" 
                           Foreground="Gray" 
                           HorizontalAlignment="Right"/>
            </StackPanel>
        </Border>
    </DataTemplate>
    
    <!-- Шаблон для зображень -->
    <DataTemplate x:Key="imageMessageTemplate">
        <Border Background="#F3E5F5" 
                Padding="10" 
                Margin="5" 
                CornerRadius="5">
            <StackPanel>
                <TextBlock Text="{Binding Sender}" 
                           FontWeight="Bold" 
                           FontSize="12"/>
                <Rectangle Width="200" 
                           Height="150" 
                           Fill="LightGray" 
                           Margin="0,5,0,0"/>
                <TextBlock Text="{Binding Caption}" 
                           FontSize="12" 
                           TextWrapping="Wrap" 
                           Margin="0,5,0,0"/>
            </StackPanel>
        </Border>
    </DataTemplate>
    
    <!-- Шаблон для посилань -->
    <DataTemplate x:Key="linkMessageTemplate">
        <Border Background="#FFF3E0" 
                Padding="10" 
                Margin="5" 
                CornerRadius="5">
            <StackPanel>
                <TextBlock Text="{Binding Sender}" 
                           FontWeight="Bold" 
                           FontSize="12"/>
                <TextBlock Text="{Binding Title}" 
                           FontSize="14" 
                           Foreground="#2196F3" 
                           TextDecorations="Underline" 
                           Margin="0,5,0,0"/>
                <TextBlock Text="{Binding Url}" 
                           FontSize="10" 
                           Foreground="Gray" 
                           Margin="0,5,0,0"/>
            </StackPanel>
        </Border>
    </DataTemplate>
    
    <!-- Селектор -->
    <local:MessageTemplateSelector x:Key="messageTemplateSelector"
                                    TextMessageTemplate="{StaticResource textMessageTemplate}"
                                    ImageMessageTemplate="{StaticResource imageMessageTemplate}"
                                    LinkMessageTemplate="{StaticResource linkMessageTemplate}"/>
</Window.Resources>

<ListBox ItemsSource="{Binding Messages}" 
         ItemTemplateSelector="{StaticResource messageTemplateSelector}"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>

ViewModel:

public class ChatViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Message> Messages { get; set; }
    
    public ChatViewModel()
    {
        Messages = new ObservableCollection<Message>
        {
            new TextMessage 
            { 
                Sender = "Іван", 
                Text = "Привіт! Як справи?", 
                Timestamp = DateTime.Now 
            },
            new ImageMessage 
            { 
                Sender = "Марія", 
                ImageUrl = "/images/photo.jpg", 
                Caption = "Подивись на це фото!", 
                Timestamp = DateTime.Now 
            },
            new LinkMessage 
            { 
                Sender = "Олександр", 
                Url = "https://example.com", 
                Title = "Цікава стаття", 
                Timestamp = DateTime.Now 
            }
        };
    }
    
    // ... INotifyPropertyChanged implementation
}

Результат: Кожен тип повідомлення відображається з власним шаблоном — текст на синьому фоні, зображення на фіолетовому, посилання на помаранчевому.


HierarchicalDataTemplate: Рекурсивні шаблони для TreeView

HierarchicalDataTemplate — спеціальний тип DataTemplate для ієрархічних даних (дерева, вкладені структури).

Відмінність від звичайного DataTemplate

АспектDataTemplateHierarchicalDataTemplate
ВикористанняПлоскі списки (ListBox)Ієрархічні дані (TreeView)
ItemsSource❌ Немає✅ Є (для дочірніх елементів)
Рекурсія❌ Не підтримує✅ Автоматична рекурсія
Use CaseСписки контактів, повідомленьФайлова система, організаційна структура

Приклад: Файлова система

Модель:

public class FileSystemItem : INotifyPropertyChanged
{
    private string _name;
    private bool _isExpanded;
    
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }
    
    public bool IsFolder { get; set; }
    
    public bool IsExpanded
    {
        get => _isExpanded;
        set
        {
            _isExpanded = value;
            OnPropertyChanged();
        }
    }
    
    // Дочірні елементи (для папок)
    public ObservableCollection<FileSystemItem> Children { get; set; }
    
    public FileSystemItem()
    {
        Children = new ObservableCollection<FileSystemItem>();
    }
    
    // ... INotifyPropertyChanged implementation
}

ViewModel:

public class FileExplorerViewModel : INotifyPropertyChanged
{
    public ObservableCollection<FileSystemItem> RootItems { get; set; }
    
    public FileExplorerViewModel()
    {
        RootItems = new ObservableCollection<FileSystemItem>
        {
            new FileSystemItem
            {
                Name = "Documents",
                IsFolder = true,
                IsExpanded = true,
                Children = new ObservableCollection<FileSystemItem>
                {
                    new FileSystemItem
                    {
                        Name = "Work",
                        IsFolder = true,
                        Children = new ObservableCollection<FileSystemItem>
                        {
                            new FileSystemItem { Name = "Report.docx", IsFolder = false },
                            new FileSystemItem { Name = "Presentation.pptx", IsFolder = false }
                        }
                    },
                    new FileSystemItem { Name = "Resume.pdf", IsFolder = false }
                }
            },
            new FileSystemItem
            {
                Name = "Pictures",
                IsFolder = true,
                Children = new ObservableCollection<FileSystemItem>
                {
                    new FileSystemItem { Name = "Vacation.jpg", IsFolder = false },
                    new FileSystemItem { Name = "Family.png", IsFolder = false }
                }
            }
        };
    }
    
    // ... INotifyPropertyChanged implementation
}

XAML з HierarchicalDataTemplate:

<Window.Resources>
    <!-- Шаблон для файлів та папок -->
    <HierarchicalDataTemplate DataType="{x:Type local:FileSystemItem}" 
                              ItemsSource="{Binding Children}">
        <StackPanel Orientation="Horizontal">
            <!-- Іконка (папка або файл) -->
            <TextBlock Text="{Binding IsFolder, Converter={local:BoolToIconConverter}}" 
                       FontFamily="Segoe MDL2 Assets" 
                       FontSize="16" 
                       Margin="0,0,5,0"/>
            
            <!-- Назва -->
            <TextBlock Text="{Binding Name}" 
                       FontSize="14"/>
        </StackPanel>
    </HierarchicalDataTemplate>
</Window.Resources>

<TreeView ItemsSource="{Binding RootItems}"/>

Конвертер для іконок:

public class BoolToIconConverter : BaseConverter<BoolToIconConverter>
{
    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool isFolder && isFolder)
            return "\uE8B7";  // Іконка папки (Segoe MDL2 Assets)
        
        return "\uE8A5";  // Іконка файлу
    }
}

Результат: TreeView з ієрархічною структурою — папки можна розгортати/згортати, файли відображаються як листя дерева.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Ключові властивості HierarchicalDataTemplate

1. ItemsSource — прив'язка до дочірніх елементів

<HierarchicalDataTemplate ItemsSource="{Binding Children}">
    <!-- Шаблон для батьківського елемента -->
</HierarchicalDataTemplate>

2. ItemTemplate — шаблон для дочірніх елементів (якщо відрізняється)

<HierarchicalDataTemplate ItemsSource="{Binding Children}">
    <!-- Шаблон для батьківського елемента -->
    <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
    
    <!-- Шаблон для дочірніх елементів -->
    <HierarchicalDataTemplate.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}" FontSize="12"/>
        </DataTemplate>
    </HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>

3. Рекурсія — автоматична для однотипних структур

Якщо дочірні елементи того самого типу, що й батьківські, HierarchicalDataTemplate автоматично застосовується рекурсивно.

Приклад: Організаційна структура

Модель:

public class Employee
{
    public string Name { get; set; }
    public string Position { get; set; }
    public ObservableCollection<Employee> Subordinates { get; set; }
    
    public Employee()
    {
        Subordinates = new ObservableCollection<Employee>();
    }
}

ViewModel:

public class OrganizationViewModel
{
    public ObservableCollection<Employee> TopManagement { get; set; }
    
    public OrganizationViewModel()
    {
        TopManagement = new ObservableCollection<Employee>
        {
            new Employee
            {
                Name = "Іван Петренко",
                Position = "CEO",
                Subordinates = new ObservableCollection<Employee>
                {
                    new Employee
                    {
                        Name = "Марія Коваленко",
                        Position = "CTO",
                        Subordinates = new ObservableCollection<Employee>
                        {
                            new Employee { Name = "Олександр Шевченко", Position = "Senior Developer" },
                            new Employee { Name = "Анна Мельник", Position = "Junior Developer" }
                        }
                    },
                    new Employee
                    {
                        Name = "Петро Бондаренко",
                        Position = "CFO"
                    }
                }
            }
        };
    }
}

XAML:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type local:Employee}" 
                              ItemsSource="{Binding Subordinates}">
        <Border Background="LightBlue" 
                Padding="5" 
                Margin="2" 
                CornerRadius="3">
            <StackPanel>
                <TextBlock Text="{Binding Name}" 
                           FontWeight="Bold" 
                           FontSize="14"/>
                <TextBlock Text="{Binding Position}" 
                           FontSize="12" 
                           Foreground="Gray"/>
            </StackPanel>
        </Border>
    </HierarchicalDataTemplate>
</Window.Resources>

<TreeView ItemsSource="{Binding TopManagement}"/>

Результат: Організаційна структура компанії у вигляді дерева — CEO → CTO → розробники.


Практичні завдання

Рівень 1: Відображення Person у ContentControl

Мета: Навчитися створювати базовий DataTemplate для відображення об'єкта.

Завдання:

Створіть клас Person з властивостями:

  • FirstName (string)
  • LastName (string)
  • Age (int)
  • Email (string)

Створіть DataTemplate, що відображає Person як картку з:

  • Повним ім'ям (FirstName + LastName) великим шрифтом
  • Віком у форматі "Вік: X років"
  • Email синім кольором

Критерії успіху:

  • DataTemplate визначений у ресурсах
  • Використано Implicit DataTemplate (без x:Key)
  • Картка має рамку та фон
  • Всі дані відображаються коректно

Підказка:

<Window.Resources>
    <DataTemplate DataType="{x:Type local:Person}">
        <Border Background="LightGray" Padding="10" BorderBrush="Gray" BorderThickness="1">
            <StackPanel>
                <TextBlock FontSize="16" FontWeight="Bold">
                    <TextBlock.Text>
                        <MultiBinding StringFormat="{}{0} {1}">
                            <Binding Path="FirstName"/>
                            <Binding Path="LastName"/>
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
                <!-- Додайте інші поля -->
            </StackPanel>
        </Border>
    </DataTemplate>
</Window.Resources>

Рівень 2: Список контактів з DataTemplate

Мета: Створити красивий список контактів з аватарами та інформацією.

Завдання:

Створіть додаток "Контакти" з:

Модель Contact:

public class Contact : INotifyPropertyChanged
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Phone { get; set; }
    public string Email { get; set; }
    public string AvatarColor { get; set; }  // Hex колір для аватара
}

Вимоги до UI:

  1. ListBox з мінімум 5 контактами
  2. DataTemplate для кожного контакту:
    • Круглий аватар (Ellipse) з кольором з AvatarColor
    • Ініціали у центрі аватара (перша літера FirstName + LastName)
    • Повне ім'я жирним шрифтом
    • Телефон та email меншим шрифтом
  3. Кнопки "Додати" та "Видалити" контакт
  4. При кліку на контакт — показати деталі справа (Master-Detail pattern)

Критерії успіху:

  • Список відображається коректно з DataTemplate
  • Аватари різних кольорів
  • Додавання/видалення працює (ObservableCollection)
  • Master-Detail pattern реалізований

Підказка для ініціалів:

public string Initials => $"{FirstName?[0]}{LastName?[0]}";

Рівень 3: DataTemplateSelector для різних типів повідомлень

Мета: Створити чат з різними типами повідомлень через DataTemplateSelector.

Завдання:

Створіть додаток "Чат" з підтримкою трьох типів повідомлень:

Базовий клас:

public abstract class Message
{
    public string Sender { get; set; }
    public DateTime Timestamp { get; set; }
    public bool IsOwnMessage { get; set; }  // Своє чи чуже повідомлення
}

Типи повідомлень:

public class TextMessage : Message
{
    public string Text { get; set; }
}

public class ImageMessage : Message
{
    public string ImagePath { get; set; }
    public string Caption { get; set; }
}

public class FileMessage : Message
{
    public string FileName { get; set; }
    public long FileSize { get; set; }
}

Вимоги:

  1. Створіть MessageTemplateSelector з трьома шаблонами
  2. Кожен тип повідомлення має унікальний дизайн:
    • TextMessage — текст у бульбашці
    • ImageMessage — зображення з підписом
    • FileMessage — іконка файлу + назва + розмір
  3. Свої повідомлення справа (синій фон), чужі зліва (сірий фон)
  4. Додайте TextBox для введення та кнопку "Надіслати"
  5. Додайте кнопки для надсилання зображення та файлу

Критерії успіху:

  • DataTemplateSelector коректно вибирає шаблон
  • Свої/чужі повідомлення відображаються по різних боках
  • Можна надсилати всі три типи повідомлень
  • Час відправки відображається у форматі "HH:mm"

Додатково (складно):

  • Додайте підтримку LinkMessage (посилання з preview)
  • Додайте групування повідомлень за датою
  • Додайте індикатор "друкує..." коли користувач вводить текст

Підсумок

Data Templates — це потужний механізм для автоматичної візуалізації C#-об'єктів у WPF. Вони дозволяють повністю розділити дані та їх представлення.

Ключові висновки:

📄 DataTemplate

XAML-шаблон, що визначає, як WPF відображає об'єкти певного типу. Замість ToString() — красивий UI.

🔄 Implicit vs Explicit

Implicit (без x:Key) — автоматичне застосування. Explicit (з x:Key) — вручну через ContentTemplate/ItemTemplate.

📦 ContentTemplate vs ItemTemplate

ContentTemplate для одного об'єкта (ContentControl). ItemTemplate для колекцій (ListBox, ComboBox).

🎯 DataTemplateSelector

Програмний вибір шаблону залежно від даних. Різні типи об'єктів → різні шаблони.

🌳 HierarchicalDataTemplate

Рекурсивні шаблони для ієрархічних даних (TreeView). Автоматична рекурсія для вкладених структур.

🎨 Розділення відповідальності

Модель не знає про UI. UI не знає про бізнес-логіку. DataTemplate — міст між ними.

Коли використовувати DataTemplate:

  • ✅ Відображення об'єктів у ContentControl, ListBox, ComboBox
  • ✅ Створення карток, списків, меню
  • ✅ Консистентний дизайн для об'єктів одного типу
  • ✅ Автоматична візуалізація колекцій

Коли використовувати DataTemplateSelector:

  • ✅ Колекція містить об'єкти різних типів
  • ✅ Умовне форматування (VIP клієнти, пріоритети)
  • ✅ Складна логіка вибору шаблону

Коли використовувати HierarchicalDataTemplate:

  • ✅ Ієрархічні дані (файлова система, організаційна структура)
  • ✅ TreeView з вкладеними елементами
  • ✅ Рекурсивні структури
Best Practice: Створюйте бібліотеку DataTemplate-ів у окремому ResourceDictionary для перевикористання у різних вікнах та проєктах.

Що далі?

  • Collections Binding Part 1 (наступна стаття) — прив'язка колекцій через ObservableCollection
  • Collections Binding Part 2 (стаття 21 Part 2) — фільтрація, сортування, групування через ICollectionView
  • MVVM Pattern (Блок 7) — архітектурний патерн для повного розділення UI та логіки

Словник термінів

DataTemplate — XAML-шаблон, що визначає візуальне представлення об'єктів певного типу.Implicit DataTemplate — DataTemplate без x:Key, що автоматично застосовується до всіх об'єктів вказаного типу у scope ресурсів.Explicit DataTemplate — DataTemplate з x:Key, що застосовується вручну через ContentTemplate або ItemTemplate.ContentTemplate — властивість ContentControl для встановлення DataTemplate для одного об'єкта.ItemTemplate — властивість ItemsControl для встановлення DataTemplate для кожного елемента колекції.DataTemplateSelector — клас, що програмно вибирає DataTemplate залежно від даних через метод SelectTemplate().HierarchicalDataTemplate — спеціальний тип DataTemplate для ієрархічних даних (TreeView). Підтримує ItemsSource для дочірніх елементів.ItemsSource — властивість HierarchicalDataTemplate, що вказує на колекцію дочірніх елементів для рекурсивного відображення.DataContext — об'єкт даних, до якого прив'язуються Binding-и всередині DataTemplate. Для ItemTemplate — це кожен елемент колекції.ResourceDictionary — колекція ресурсів (стилі, шаблони, конвертери), що можуть бути перевикористані у різних частинах додатку.

Додаткові ресурси

📖 Microsoft Docs: Data Templating

Повний огляд Data Templating у WPF з прикладами та best practices.

📖 DataTemplate Class

API документація класу DataTemplate з детальним описом властивостей та методів.

📖 DataTemplateSelector Class

Документація DataTemplateSelector для програмного вибору шаблонів.

📖 HierarchicalDataTemplate Class

API документація HierarchicalDataTemplate для TreeView та ієрархічних даних.

🎓 ItemsControl Overview

Огляд ItemsControl — базового класу для ListBox, ComboBox, ListView.

🔧 WPF Templates Library

Колекція готових DataTemplate-ів на GitHub для використання у проєктах.

📚 Попередня стаття: Value Converters

Повернутися до Value Converters — перетворення типів даних між Source та Target.

📚 Наступна стаття: Collections Binding Part 1

Дізнатися про прив'язку колекцій через ObservableCollection та ItemsControl.