Reputation: 173
I'm trying to create a WPF canvas control which can Draw shapes, curves, and textblocks, selecting and deleting them, so the best way I can think of this working is using MVVM binding a list of view models with Canvas Children.
However, Canvas Children can't be bind directly. So I'm kinda of stuck. Had look around internet but haven't found direct answers. I found someone suggesting the following way to achieve binding Canvas Children, but I couldn't understand what kind of type ViewModels "Nodes" are, and how to convert different Shapes, Curves and TextBlocks to Nodes.
Any help will be appreciated. Thanks in advance.
<ItemsControl ItemsSource="{Binding Path=Nodes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
<Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
Upvotes: 4
Views: 3041
Reputation: 128042
Your view model should contain a base class Node
that defines the XPos
and YPos
properties, and derived classes for the specific node types, e.g. TextNode
and ShapeNode
:
public class Node
{
public double XPos { get; set; }
public double YPos { get; set; }
}
public class TextNode : Node
{
public string Text { get; set; }
}
public class ShapeNode : Node
{
public Geometry Geometry { get; set; }
public Brush Stroke { get; set; }
public Brush Fill { get; set; }
}
public class ViewModel
{
public ObservableCollection<Node> Nodes { get; } = new ObservableCollection<Node>();
}
In XAML, you would add DataTemplates for the specific nodes types like shown below. See the Data Templating Overview article on MSDN for details.
<ItemsControl ItemsSource="{Binding Path=Nodes}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:TextNode}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ShapeNode}">
<Path Data="{Binding Geometry}" Stroke="{Binding Stroke}" Fill="{Binding Fill}"/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
<Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
You might now add different nodes to an instance of the main view model class and set the window's DataContext to that instance:
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
vm.Nodes.Add(new TextNode
{
XPos = 50,
YPos = 100,
Text = "Hello, World."
});
vm.Nodes.Add(new ShapeNode
{
XPos = 100,
YPos = 200,
Geometry = new EllipseGeometry { RadiusX = 50, RadiusY = 50 },
Fill = Brushes.Red
});
DataContext = vm;
}
If you want your view to react on property changes of the nodes, the Node class should implement the INotifyPropertyChanged interface.
If the items should be selectable, you should replace the ItemsControl by a ListBox. The TargetType of the ItemContainerStyle would then be ListBoxItem, and you would bind its IsSelected
property to an appropriate property on your Node class.
Upvotes: 6