Winter Winter
Winter Winter

Reputation: 173

Can WPF Canvas Children Bind ObservableCollection contain ViewModel for different shapes and TextBlocks?

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

Answers (1)

Clemens
Clemens

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

Related Questions