0

I have a problem plotting a collection of lines into a canvas. Google showed me several sources but i couldn't find a real solution for this one. I hope you guys have a hint for me.

My stucture is as follows:

public class CanvasLine
{
    public Double X1 { get; set; }
    public Double X2 { get; set; }
    public Double Y1 { get; set; }
    public Double Y2 { get; set; }
    public Brush StrokeColor { get; set; }
    public Double StrokeThickness { get; set; }
    public DoubleCollection StrokeDashArray { get; set; }
}

public class CanvasObject
{
    public String Name { get; set; }
    public ObservableCollection<CanvasLine> CanvasLines { get; set; }
}

public class ViewModel
{
    ...
    public ObservableCollection<CanvasObject> CanvasObjects;
    ...
}

XAML:

<Window x:Class="XXX.Views.MainWindow"
    xmlns:vm="clr-namespace:XXX.Viewmodels"
    xmlns:converter="clr-namespace:XXX.Converter"
    Title="XXX" Height="480" Width="640">
<Window.DataContext>
    <vm:ViewModel/>
</Window.DataContext>
<Grid>
    <ItemsControl Grid.Column="1" Grid.Row="2" Margin="0" ItemsSource="{Binding CanvasObjects, UpdateSourceTrigger=PropertyChanged}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Line DataContext="{Binding CanvasLines}" Stroke="{Binding StrokeColor}" StrokeDashArray="{Binding StrokeDashArray}" StrokeThickness="{Binding StrokeThickness}">
                    <Line.X1>
                        <MultiBinding Converter="{StaticResource MultiplicationConverter}">
                            <Binding Path="X1"/>
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Canvas}"/>
                        </MultiBinding>
                    </Line.X1>
                    <Line.X2>
                        <MultiBinding Converter="{StaticResource MultiplicationConverter}">
                            <Binding Path="X2"/>
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Canvas}"/>
                        </MultiBinding>
                    </Line.X2>
                    <Line.Y1>
                        <MultiBinding Converter="{StaticResource MultiplicationConverter}">
                            <Binding Path="Y1"/>
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Canvas}"/>
                        </MultiBinding>
                    </Line.Y1>
                    <Line.Y2>
                        <MultiBinding Converter="{StaticResource MultiplicationConverter}">
                            <Binding Path="Y2"/>
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Canvas}"/>
                        </MultiBinding>
                    </Line.Y2>
                </Line>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

This is for demonstration, i might have deleted too much.

My problem is that only the first CanvasLine for each CanvasObject is shown in the canvas. It works fine if i give up the CanvasObject and the DataContext="{Binding CanvasLines}" and directly bind the ItemsControl to a ObservableCollection<CanvasLine> but in the next steps i need to add more objects and also i'm trying to avoid a huge list of lines an to keep some kind of object structure.

Since i'm pretty new to this MVVM Binding stuff i'm happy about any thoughts you like to share.

Greetings.

Sven.L
  • 45
  • 1
  • 9

1 Answers1

1

You must not bind a Line's DataContext inside the DataTemplate. It does already hold a reference to the appropriate collection element.

What you actually need are nested ItemsControls, an outer one for the CanvasObjects collection, and an inner one for CanvasLines:

<ItemsControl ItemsSource="{Binding CanvasObjects}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ItemsControl ItemsSource="{Binding CanvasLines}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Line Stroke="{Binding StrokeColor}"
                              StrokeDashArray="{Binding StrokeDashArray}"
                              StrokeThickness="{Binding StrokeThickness}">
                            ...
                        </Line>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Since you are apparently drawing your lines in relative coordinates, I'd like to suggest the following changes. Instead of X1, Y1, X2, Y2 let yout CanvasLine have two Points, P1 and P2:

public class CanvasLine
{
    public Point P1 { get; set; }
    public Point P2 { get; set; }
    public Brush Stroke { get; set; }
    public double StrokeThickness { get; set; }
    public DoubleCollection StrokeDashArray { get; set; }
}

Now make your XAML use a Path with a LineGeometry instead of a Line. Then assign the Transform property to an appropriate ScaleTransform in the outer ItemsControl's resources:

<ItemsControl ItemsSource="{Binding CanvasObjects}">
    <ItemsControl.Resources>
        <ScaleTransform x:Key="lineTransform"
            ScaleX="{Binding ActualWidth,
                     RelativeSource={RelativeSource AncestorType=ItemsControl}}"
            ScaleY="{Binding ActualHeight,
                     RelativeSource={RelativeSource AncestorType=ItemsControl}}"/>
    </ItemsControl.Resources>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ItemsControl ItemsSource="{Binding CanvasLines}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Path Stroke="{Binding Stroke}"
                              StrokeDashArray="{Binding StrokeDashArray}"
                              StrokeThickness="{Binding StrokeThickness}">
                            <Path.Data>
                                <LineGeometry
                                    Transform="{StaticResource lineTransform}"
                                    StartPoint="{Binding P1}"
                                    EndPoint="{Binding P2}"/>
                            </Path.Data>
                        </Path>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Great! Thanks, i think thats pushing me into the right direction. I just tried it and it seems that all lines are shown. Most of them in wrong positions and with wrong styles but thats another problem that just needs a bit more work... I think the `` into the nested ItemsControl is wrong, right? At least i needed to delete it to see lines. – Sven.L Mar 01 '18 at 14:58
  • The problem with your wrong positions was that you removed the inner Canvas. ItemsControl then uses a StackPanel by default, which obviously wont work the way you want. The inner Canvas however never gets a size set, so your ActualWidth bindings in the MultiBindings always returned zero. IMO the approach with transformed LineGeometries is less complicated. – Clemens Mar 01 '18 at 15:12
  • Thanks again, that seems reasonable! You are right, when putting the inner canvas in again, actualWidth is 0. So i'm going to implement your LineGeometry solution next. Just for my Information: Is there a way to use a RelativeSource and point to the "second Ancestor"? In this case the outer ItemsControls canvas? – Sven.L Mar 01 '18 at 15:27
  • There is an AncestorLevel which you may set to 2. – Clemens Mar 01 '18 at 15:27