TreeView WPF из различных объектов
Как правильно в WPF организовать TreeView для различных объектов? Хотелось бы видеть подобную модель.
Классы:
Основная проблема в том, как сделать подобную привязку в WPF. Находила решения с использованием MuiltiBinding, но они не работают. Сейчас работает при указании в ресурсах TreeView HierarchicalDataTemplate и DataTemplate, но он работает в случае, если в классе TreeObject есть свойство, возвращающее новый IEnumerable, в который добавляются все имеющиеся списки. Мне кажется это нецелесообразно по памяти, т.к. в классе уже есть списки нескольких объектов. Да и хочется попробовать как-то кастомизировать каждый отдельный тип объекта по-разному. Вопрос не имеет какой-то конкретной задачи, просто уже давно интересно и в долгих поисках, как же всё же это люди делают. Спасибо за отклики!
Ответы (1 шт):
Покажу простейший пример работы с TreeView.
Структура классов
Первое что нам понадобится, так это продумать то, как в проекте будут взаимодействовать классы друг с другом, то есть нам надо грамотно спроектировать проект, определив все зависимости, повторяющиеся поведение и др. К примеру, вы можете сделать все на object или боже упаси на dynamic, ок, но будет ли дальше вам удобно с этим всем взаимодействовать? Думаю нет. Поэтому, давайте сделаем нечто удобное, базирующуюся на интерфейсах.
И так, первым делом создадим интерфейс. Что от него требуется? Объединить в себе нечто общее классов и позволить нам хранить в той же коллекции несколько разных типов.
public interface INode { public string Name { get; } }В данном примере, я вынес в интерфейс лишь название ноды, не более, ибо во всех классах оно должно быть.
Далее создадим все необходимые нам типы:
Директория
public class FolderNode : INode { public FolderNode(string name) => Name = name; public string Name { get; set; } public List<INode> Items { get; set; } }Изображение
public class ImageNode : INode { public ImageNode(string name) => Name = name; public string Name { get; set; } public string Size { get; set; } public string Format { get; set; } }Музыка
public class MusicNode: INode { public MusicNode(string name) => Name = name; public string Name { get; set; } public string Author { get; set; } public int Size { get; set; } public TimeSpan Duration { get; set; } }
Как видите, это простые классы, с простыми свойствами и может даже своей логикой. Все зависит от того, что вы хотите и как.
Данные
Имея классы, мы можем теперь заполнять наши коллекции всем необходимым.
Для удобства заполнения давайте сделаем самый простейший "билдер", чтоб немного упростить чтение кода.
public class FolderBuilder { private FolderNode folder; public FolderBuilder(string name) => folder = new FolderNode(name); public FolderBuilder AddItem(INode node) { if (folder.Items is null) folder.Items = new List<INode>(); folder.Items.Add(node); return this; } public FolderNode Build() => folder; }Ну и теперь, заполнение. Помним, что в WPF привязка доступна лишь к публичным свойствам. Также помним, что в случае, если коллекция уже была привязана и нам надо в ней изменить данные (добавить/удалить), то коллекция должна реализовывать
INotifyCollectionChanged, из коробки в C# есть уже готовая для этого -ObservableCollection<T>.Делаем свойство:
public ObservableCollection<INode> Folders { get; set; } = new();Заполняем:
var concertFolder = new FolderBuilder("Записи с концерта") .AddItem(new MusicNode("Концерт №1") { Author = "Группа 1", Duration = TimeSpan.FromHours(5), Size = 84496 }) .AddItem(new MusicNode("Концерт №2") { Author = "Группа 1", Duration = TimeSpan.FromHours(2), Size = 15347 }) .Build(); var musicFolder = new FolderBuilder("Музыка") .AddItem(new MusicNode("Трек #1") { Author = "Вася", Duration = new(0, 2, 37), Size = 5748 }) .AddItem(new MusicNode("Трек #2") { Author = "Катя", Duration = new(0, 1, 52), Size = 1357 }) .AddItem(new MusicNode("Трек #3") { Author = "Петр", Duration = new(0, 4, 43), Size = 4626 }) .AddItem(new ImageNode("Обложка") { Format = "JPEG", Size = "1920x1080" }) .AddItem(concertFolder) .Build(); Folders.Add(musicFolder);
Тут как видите, простая коллекция, вам не мешает кто-либо добавить туда элементы циклом или еще каким-либо способом.
XAML (UI)
Теперь нам остается спроектировать интерфейс, прописав там все эти типы, указав то, как они должны отображаться. В WPF есть 2 предназначенных для этого элемента:
DataTemplate- позволяет указать тип (DataType) объекта и вид, который необходимо ему задать.HierarchicalDataTemplate- аналогично простомуDataTemplateза исключением того, что позволяет еще и задатьItemsSource, которую он будет отображать.
Зная это, давайте пропишем для всех наших типов соответствующий им вид:
Директория (в себе содержит коллекцию, поэтому
HierarchicalDataTemplate)<HierarchicalDataTemplate DataType="{x:Type local:FolderNode}" ItemsSource="{Binding Items}"> <StackPanel Orientation="Horizontal" Margin="0 0 0 3"> <Viewbox Width="18" Height="18"> <Canvas Width="24" Height="24"> <Path Fill="Black" Data="M6.1,10L4,18V8H21A2,2 0 0,0 19,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H19C19.9,20 20.7,19.4 20.9,18.5L23.2,10H6.1M19,18H6L7.6,12H20.6L19,18Z" /> </Canvas> </Viewbox> <TextBlock Text="{Binding Name}" VerticalAlignment="Center" Margin="3 0 0 0" /> </StackPanel> </HierarchicalDataTemplate>Изображение (достаточно простого
DataTemplate)<DataTemplate DataType="{x:Type local:ImageNode}"> <Grid Margin="0 0 0 3"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal"> <Viewbox Width="18" Height="18"> <Canvas Width="24" Height="24"> <Path Fill="#FF17BF9C" Data="M14,2L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2H14M18,20V9H13V4H6V20H18M17,13V19H7L12,14L14,16M10,10.5A1.5,1.5 0 0,1 8.5,12A1.5,1.5 0 0,1 7,10.5A1.5,1.5 0 0,1 8.5,9A1.5,1.5 0 0,1 10,10.5Z" /> </Canvas> </Viewbox> <TextBlock Text="{Binding Name}" VerticalAlignment="Center" Margin="3 0 0 0" /> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock FontSize="10" Text="{Binding Format}" Margin="0 0 3 0"/> <TextBlock FontSize="10" Text="{Binding Size}"/> </StackPanel> </Grid> </DataTemplate>Музыка
<DataTemplate DataType="{x:Type local:MusicNode}"> <Grid Margin="0 0 0 3"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal"> <Viewbox Width="18" Height="18"> <Canvas Width="24" Height="24"> <Path Fill="#FFA4D81F" Data="M14,2L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2H14M18,20V9H13V4H6V20H18M13,10V12H11V17A2,2 0 0,1 9,19A2,2 0 0,1 7,17A2,2 0 0,1 9,15C9.4,15 9.7,15.1 10,15.3V10H13Z" /> </Canvas> </Viewbox> <TextBlock VerticalAlignment="Center" Margin="3 0 0 0" > <Run Text="{Binding Author}"/> <Run Text="-"/> <Run Text="{Binding Name}"/> </TextBlock> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock FontSize="10" Text="{Binding Duration}" Margin="0 0 3 0"/> <TextBlock FontSize="10" Text="{Binding Size, StringFormat={}{0} кб}"/> </StackPanel> </Grid> </DataTemplate>Теперь осталось сделать сам
TreeViewс указанием тамItemsSource<TreeView ItemsSource="{Binding Folders}"/>
Все, вот мы и сделали отдельные классы, со своими данными, которые в UI отображаются по-разному. Как видите, сложного тут чего-либо нету, главное не запутаться) Естественно структура классов, вид и все остальное может быть совершенно разным, хоть на object постройте все, суть это не меняет. Главное, чтоб был объект (класс), ну и пояснить XAML как его обрабатывать, все. Кстати, этот "трюк" с DataTemplate работает везде, хоть выводите объект просто на экран, хоть в ListBox, хоть еще куда.


