Предпосылки: понимая, что контейнеры компоновки в WPF не позволяют сделать привязки (Binding) к своим дочерним элементам, решил поэкспериментировать, а как же всё-таки подсунуть данные из View Model для формирования содержимого в эти самые контейнеры компоновки. Позже аналогичное решение было сделано для AvaloniaUI.
Кроме того, я стал регулярно обращать внимание на то, что подобные вопросы появлялись в телеграме в чатах pro.net и AvaloniaUI (RU), поэтому своё решение опубликовал на гитхабе. Но вопросы продолжают появляться регулярно, что и сподвигло меня написать статью на Хабре с пошаговым разбором, что делать.
Итак, если Вас эта тема заинтересовала, добро пожаловать под кат.
Базовое решение на самом деле достаточно простое: достаточно посмотреть, в какой момент возникает свойство ItemsSource: это ItemsControl. Этот ItemsControl предлагает также свойство ItemsPanel — указывает панель (то есть контейнер компоновки), который должен будет использоваться для размещения элементов, притом значением по умолчанию является StackPanel.
Давайте поставим задачу следующим образом: делаем максимально простую View Model. Пускай это будет набор квадратов разного цвета и текстом, которые мы хотим спозиционировать по Canvas-у. Для самого Canvas-а при этом вычисляется размер исходя из размеров элементов. Пока без динамики, чтобы не засорять код.
internal class ViewModel { public List<Item> Items { get; } = new List<Item>() { new Item {X = 100, Y = 200, Size=100, Color = Colors.Cyan, Text = "First"}, new Item {X = 500, Y = 300, Size=200, Color = Colors.Yellow, Text = "Second"}, new Item {X = 300, Y = 500, Size=150, Color = Colors.Red, Text = "Third"}, }; public int Width => Items.Max(x => x.X + x.Size); public int Height => Items.Max(x => x.Y + x.Size); } internal class Item { public int X { get; init; } public int Y { get; init; } public int Size { get; init; } public Color Color { get; init; } public string Text { get; init; } }
Создадим представление:
<Window ...> <Window.DataContext> <local:ViewModel /> </Window.DataContext> <Viewbox Stretch="Uniform"> <ItemsControl ItemsSource="{Binding Items}" Width="{Binding Width}" Height="{Binding Height}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Background="Silver" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type local:Item}"> <Rectangle Width="{Binding Size}" Height="{Binding Size}"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Color}" /> </Rectangle.Fill> </Rectangle> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Viewbox> </Window>
Всё по классике: создали ItemsControl, привязали свойства, заменили ItemsPanel, сделали DataTemplate для элемента коллекции. Однако как сделать так, чтобы элементы позиционировались в Canvas‘е? И вот тут начались приключения.
По идее надо всего-то задать прикреплённые свойства Canvas.Left и Canvas.Top. Но для какого элемента это нужно сделать? Если задать для Rectangle в DataTemplate, то работать не будет, пробовал.
Подсказка к решению этой проблемы скрывается в том, как трансформируется дерево элементов, когда есть привязка списка.
А если конкретнее, то каждый элемент списка оборачивается в элемент ContentPresenter и его содержимое уже связывается с соответствующим элементом данных. Собственно, в этот момент и становится ясно, что нужно сделать: через стили сконфигурировать этот самый ContentPresenter.

Собственно, выходим на решение:
<Window ...> <Window.DataContext> <local:ViewModel /> </Window.DataContext> <Viewbox Stretch="Uniform"> <ItemsControl ItemsSource="{Binding Items}" Width="{Binding Width}" Height="{Binding Height}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Background="Silver" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.Resources> <Style TargetType="ContentPresenter"> <Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Top" Value="{Binding Y}" /> </Style> <DataTemplate DataType="{x:Type local:Item}"> <Rectangle Width="{Binding Size}" Height="{Binding Size}"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Color}" /> </Rectangle.Fill> </Rectangle> </DataTemplate> </ItemsControl.Resources> </ItemsControl> </Viewbox> </Window>
С Авалонией всё примерно то же самое с точностью до имён некоторых свойств (найдите десять отличий, ага):
<Window ... > <Design.DataContext> <vm:MainWindowViewModel/> </Design.DataContext> <ItemsControl Items="{Binding Items}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.Styles> <Style Selector="ItemsControl ContentPresenter"> <Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Top" Value="{Binding Y}" /> </Style> </ItemsControl.Styles> <ItemsControl.DataTemplates> <DataTemplate DataType="{x:Type vm:Item}"> <Rectangle Width="{Binding Size}" Height="{Binding Size}"> <Rectangle.Fill> <SolidColorBrush Color="{Binding Color}" /> </Rectangle.Fill> </Rectangle> </DataTemplate> </ItemsControl.DataTemplates> </ItemsControl> </Window>
Собственно, всё. Точно так же можно подсовывать данные в любой контейнер компоновки, элементы которого требуют конфигурирования через присоединённые свойства: Grid, DockPanel и любой другой.
На этом у меня всё, надеюсь, информация оказалась полезной.
ссылка на оригинал статьи https://habr.com/ru/post/686438/
Добавить комментарий