Overview: I have a XAML Canvas control onto which I have placed several Line controls. The lines appear where they should not, and my conclusion is that the bounding boxes of said lines are misbehaving. I do not access or alter these bounding boxes (as far as I know).
Project details: I'm using WPF, XAML, C# and the MVVM pattern, all within Visual Studio 2010.
More detailed explanation: My project is to create a canvas and have items on that canvas that can be dragged around by the user. Lines are drawn between one item and another, to show a visual link.
To visualise, you can see an image here:
There are five items and in the code the N1 item should be linked by lines to the N3, N4 and N5 items. The N1 to N3 line seems to be fine, but the other two are offset. If you were to move them up, they would link the items together nicely.
The first thing you might consider is the co-ordinates of the lines within the Canvas, and I have done this.
Please view this image:
I added a TextBlock to the XAML within the same region as the Line and bound its Text to the StartingPoint of the Line. It might be difficult to see here if the image is small, but I can tell you that the StartingPont is the same for both lines here. And yet, clearly we can see that the lines are not in the same place.
I also thought that this could be a problem of alignment. I do not set any alignments within my project so I thought maybe this was the problem. I changed my lines to have different alignments (horizontal and vertical) as well as the items themselves, and I observed no difference.
Project in more detail:
First is the resource for the item itself. I don't imagine it makes a difference, but since I am all out of ideas, I can't discount that the problem might be somewhere unseen:
<ResourceDictionary>
<ControlTemplate x:Key="NodeTemplate">
<Border BorderThickness="2" BorderBrush="LightBlue" Margin="2" CornerRadius="5,5,5,5">
<StackPanel>
<TextBlock Text="Test" Background="AntiqueWhite"/>
<TextBlock Text="{Binding Path=NodeText}" Background="Aqua"/>
</StackPanel>
</Border>
</ControlTemplate>
</ResourceDictionary>
Now there is, below that, the Canvas itself with the ItemsControls within:
<Canvas>
<ItemsControl ItemsSource="{Binding NodeList, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding LineList, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Line Stroke="Black" X1="{Binding StartPoint.X}" Y1="{Binding StartPoint.Y}" X2="{Binding EndPoint.X}" Y2="{Binding EndPoint.Y}" />
<!--Path Stroke="Black" Data="{Binding}" Canvas.Left="0" Canvas.Top="0" StrokeMiterLimit="1"/-->
<TextBlock Text="{Binding StartPoint}" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding NodeList, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding CanvasLeft}"/>
<Setter Property="Canvas.Top" Value="{Binding CanvasTop}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Thumb Name="myThumb" Template="{StaticResource NodeTemplate}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="DragDelta">
<cmd:EventToCommand Command="{Binding DragDeltaCommand}" PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Thumb>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
I do have two sections here; two separate ItemsControls which have slightly different purposes. I assume this is okay, although assumptions may be how I ended up with this problem in the first instance.
Now the next part is some of the code behind and, I think, the main part is the OnDragDelta; the event handler for dragging the item around the Canvas:
void OnDeltaDrag(DragDeltaEventArgs e)
{
CanvasLeft += e.HorizontalChange;
CanvasTop += e.VerticalChange;
UpdateLines();
}
And then, of course, 'UpdateLines':
public void UpdateLines()
{
// Assess next nodes. Their lines will need to be changed - their start points will have to move with this node.
for (int i = 0; i < this.NextNodes.Count; i++)
{
this.LineList.ElementAt(i).StartPoint = new Point(this.CanvasLeft, this.CanvasTop);
}
// Assess previous nodes. If they have lines to this node, the end points of those
// lines will need to be moved (more specifically, moved to have the same coords as this).
foreach (NodeViewModel n in this.PreviousNodes)
{
for (int i = 0; i < n.NextNodes.Count; i++)
{
if (n.NextNodes.ElementAt(i) == this)
{
n.LineList.ElementAt(i).EndPoint = new Point(this.CanvasLeft, this.CanvasTop);
}
}
}
}
@xum59 already mentioned the default ItemsPanel for an ItemsControl is a StackPanel,
which is causing the stacking behaviour. The overall implementation of your solution looks awkward, however, I prepared a WpfApp
(using MVVM Light) to show one way of handling this more gracefully.
Node
class holding only data, implementing a clear separation with the (geometry) types belonging in the viewNodeToPathDataConverter
in charge of redrawing the connecting lines, each time the NodeList
PropertyChanged
event is raised.DragDeltaCommand
updates the Node
data behind the Thumb
being dragged, after which NodeList
PropertyChanged
event is raised.StreamGeometry
. Node.cs
public class Node : ObservableObject
{
private double x;
public double X
{
get { return x; }
set { Set(() => X, ref x, value); }
}
private double y;
public double Y
{
get { return y; }
set { Set(() => Y, ref y, value); }
}
public string Text { get; set; }
public List<string> NextNodes { get; set; }
public static ObservableCollection<Node> GetSampleNodes()
{
return new ObservableCollection<Node>()
{
new Node { X = 300, Y = 100, Text = "n1", NextNodes = new List<string> { "n2", "n4", "n5" } },
new Node { X = 150, Y = 200, Text = "n2", NextNodes = new List<string> { "n3" } },
new Node { X = 50, Y = 450, Text = "n3" },
new Node { X = 200, Y = 500, Text = "n4" },
new Node { X = 700, Y = 500, Text = "n5" }
};
}
}
MainWindow.xaml
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"
Title="MainWindow" WindowState="Maximized">
<Window.Resources>
<local:NodeToPathDataConverter x:Key="NodeToPathDataConverter" />
<ControlTemplate x:Key="NodeTemplate">
<Border BorderThickness="2" BorderBrush="LightBlue" Margin="2" CornerRadius="5,5,5,5">
<StackPanel>
<TextBlock Text="Test" Background="AntiqueWhite" />
<TextBlock Text="{Binding Text}" Background="Aqua" TextAlignment="Center" />
</StackPanel>
</Border>
</ControlTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding MainViewModel.NodeList, Source={StaticResource Locator}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas>
<Path Stroke="Black">
<Path.Data>
<MultiBinding Converter="{StaticResource NodeToPathDataConverter}">
<Binding Path="MainViewModel.NodeList" Source="{StaticResource Locator}" />
<Binding />
</MultiBinding>
</Path.Data>
</Path>
<Thumb Template="{StaticResource NodeTemplate}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="DragDelta">
<cmd:EventToCommand
Command="{Binding MainViewModel.DragDeltaCommand, Source={StaticResource Locator}}"
PassEventArgsToCommand="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Thumb>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
/// <summary>
/// Returns Geometry of line(s) from current node to next node(s)
/// </summary>
public class NodeToPathDataConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var nodes = values[0] as ObservableCollection<Node>;
Node node = values[1] as Node;
if (nodes != null && node != null && node.NextNodes != null)
{
// Create a StreamGeometry to draw line(s) from the current to the next node(s).
StreamGeometry geometry = new StreamGeometry();
using (StreamGeometryContext ctx = geometry.Open())
{
foreach (string nextText in node.NextNodes)
{
Node nextNode = nodes.Single(n => n.Text == nextText);
ctx.BeginFigure(new Point(0, 0), false /* is filled */, false /* is closed */);
ctx.LineTo(new Point(nextNode.X - node.X, nextNode.Y - node.Y), true /* is stroked */, false /* is smooth join */);
}
}
// Freeze the geometry (make it unmodifiable) for additional performance benefits.
geometry.Freeze();
return geometry;
}
return Binding.DoNothing;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
private ObservableCollection<Node> nodeList;
public MainViewModel()
{
nodeList = Node.GetSampleNodes();
DragDeltaCommand = new RelayCommand<DragDeltaEventArgs>(e => OnDeltaDrag(e));
}
public ICommand DragDeltaCommand { get; private set; }
public ObservableCollection<Node> NodeList
{
get { return nodeList; }
}
private void OnDeltaDrag(DragDeltaEventArgs e)
{
Thumb thumb = e.Source as Thumb;
if (thumb != null)
{
Node node = (Node)thumb.DataContext;
node.X += e.HorizontalChange;
node.Y += e.VerticalChange;
RaisePropertyChanged(() => NodeList);
}
}
}
Situation at startup
After dragging node n1