wp7, silverlight, Why event fires twice? - silverlight

In this case when user click some UIElement I want this element yo be surrounded by dashed border. If there is some other object surrounded then this code removes it first.
Everything is fine but "private void activateElem" fires twice and I have no idea why. Maybe somebody can help, I really have no more ideas.
Anybody knows some ideas to debug issues like this? Maybe there is some way to print out all events queue. There is silverlight spy but I think it doesn't work for windows phone 7.
My code:
public class ManipulationEngine
{
private Canvas sheet;
private static FrameworkElement current_active_element;
public ManipulationEngine(Canvas sheet)
{
this.sheet = sheet;
foreach (FrameworkElement elem in sheet.Children)
{
elem.MouseLeftButtonUp += new MouseButtonEventHandler(activateElem);
}
}
private void activateElem(object sender, MouseButtonEventArgs e)
{
FrameworkElement elem = sender as FrameworkElement;
if (current_active_element != null)
{
desactivateElem();
}
Grid grid = new Grid();
Rectangle recentagle = new Rectangle();
grid.SetValue(Canvas.TopProperty, (double)elem.GetValue(Canvas.TopProperty) - 10);
grid.SetValue(Canvas.LeftProperty, (double)elem.GetValue(Canvas.LeftProperty) - 10);
DoubleCollection stroke = new DoubleCollection();
stroke.Add(4);
stroke.Add(2);
recentagle.StrokeDashArray = stroke;
grid.Children.Add(recentagle);
sheet.Children.Remove(elem);
elem.Margin = new Thickness(10);
grid.Children.Add(elem);
sheet.Children.Add(grid);
current_active_element = elem;
}
private void desactivateElem()
{
if (current_active_element != null)
{
Grid grid = VisualTreeHelper.GetParent(current_active_element) as Grid;
grid.Children.Remove(current_active_element);
sheet.Children.Remove(grid);
current_active_element.SetValue(Canvas.TopProperty, (double)grid.GetValue(Canvas.TopProperty) + 10);
current_active_element.SetValue(Canvas.LeftProperty, (double)grid.GetValue(Canvas.LeftProperty) + 10);
current_active_element.Margin = new Thickness(0);
sheet.Children.Add(current_active_element);
current_active_element = null;
}
}

I'd really advise looking into the Parts and States model. You may be able to do this with a button, or perhaps a radio button.
Usually if you're coding changes to the visual tree, you're not doing it right.
Karen Corby dealt with this very clearly at MIX08, take a look!
http://archive.visitmix.com/blogs/2008Sessions/T20/
Luke

Related

WPF No NameScope found to register the Name

I am trying to create a "debug" TextBlock, in the sense it is just like a MessageBox, but doesn't interrupt like it. Rather, it is like a statusbar, which gives output information silently. Here is my current code
private void Debug(string data)
{
TextBlock tb = componentContainer.FindName("Debugbox") as TextBlock;
if (tb == null)
{
MessageBox.Show("yo");
tb = new TextBlock() {Foreground = Brushes.Orange };
if (NameScope.GetNameScope(tb) == null)
NameScope.SetNameScope(tb, new NameScope());
componentContainer.RegisterName("Debugbox", tb);
componentContainer.Children.Add(tb);
}
tb.Text = data;
}
However, it is giving me "No NameScope found to register the Name". I tried replacing
componentContainer.RegisterName("Debugbox", tb);
with
Namescope.GetNameScope(tb).RegisterName("Debugbox", tb);
and it works well. But if this function is used multiple times, the TextBlock is simply overwritten which makes the required output hard to see.
NOTE : componentContainer is a Canvas
Does anyone know what I am doing wrong? Or is there any better way to do something similar?
Why not simply keep the TextBlock instance in a private field:
private TextBlock debugBox;
private void Debug(string data)
{
if (debugBox == null)
{
debugBox = new TextBlock { Foreground = Brushes.Orange };
componentContainer.Children.Add(debugBox);
}
debugBox.Text = data;
}

WPF VirtualizingWrapPanel Selected Item Change cause scroll

I have implemented a virtualizing panel that works pretty well, the panel implements IScrollInfo. I am in the process of getting it to work using a keyboard. The Panel is used in a ListView as the ListView.ItemsPanel. When the user presses down the selected item correctly changes but no scrolling occurs and when you reach the last visible item you can not press down any longer. I am trying to figure out how to have the panel aware of the selected item and scroll appropriately. I have tried searching around but I cant seem to find anything related to what I want to do. A point in the right direction would be greatly appreciated.
Edit: Here is the code for the VirtualizingWrapPanel
So I found a solution that works fairly well, I am not sure if it is the best route to take but it seems to work fine.
In the ArrangeOverride method while looping over the children I do the following.
var listViewItem = child as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
listViewItem.Selected += ListViewItemSelected;
}
In MeasureOverride I make a call to CleanUpItems in here we will need to unsubscribe from the selected event.
var listViewItem = children[i] as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
}
I also added the following three functions.
private void ListViewItemSelected(object sender, RoutedEventArgs e)
{
var listViewItem = sender as ListViewItem;
if(listViewItem == null) return;
var content = listViewItem.Content as CollectionViewGroup;
if(content != null) return; //item is a group header dont click
var items = ItemContainerGenerator as ItemContainerGenerator;
if(items == null) return;
BringIndexIntoView(items.IndexFromContainer(listViewItem));
listViewItem.Focus();
}
protected override void BringIndexIntoView(int index)
{
var offset = GetOffsetForFirstVisibleIndex(index);
SetVerticalOffset(offset.Height);
}
private Size GetOffsetForFirstVisibleIndex(int index)
{
int childrenPerRow = CalculateChildrenPerRow(_extent);
var actualYOffset = ((index / childrenPerRow) * ChildDimension.Height) - ((ViewportHeight - ChildDimension.Height) / 2);
if (actualYOffset < 0)
{
actualYOffset = 0;
}
Size offset = new Size(_offset.X, actualYOffset);
return offset;
}
The GetOffsetForFirstVisibleIndex function will probably vary depending on your implementation but that should be enough info if anyone else is having trouble coming up with a solution.

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

How to make the last column in WPF datagrid take all the left space, always?

Standard WPF 4 Datagrid.
Let' say I have datagrid 200 pixels wide, and 2 columns. I would like the columns take always entire space, meaning if the user resizes the first column to 50 pixels, the last one would be 150.
Initially I set width 100 pixels for the 1st column, and * for the last one (in XAML).
I thought the problem is with removing the virtual, 3rd column, as explained here:
http://wpf.codeplex.com/Thread/View.aspx?ThreadId=58939
but there is no difference really -- still, when resizing the columns, I get some extra space on right -- with virtual column, it is a virtual column (white color by default), without it, it is empty space (gray by default).
QUESTION: how to enforce the constraint, that no matter how the user resizes the columns,
sum(columns width)==datagrid width
?
Edits
Yes, I use WPF 4.
WORKAROUND
I marked one of the answers as solution, but actually it is not a solution due to WPF design. It is simply what WPF can do at best, and it is not very good -- first of all the option CanUserResize for column means really IsResizeable and this option when turned on contradicts Width set to *. So without some really smart trick you end up with:
datagrid which last column in superficially resizable but in fact it is not, and little space on right is shown (i.e. the virtual column is not resizable) -- for last column: CanUserResize=true, Width=*
datagrid which last column cannot be resized by user and it is shown accordingly, initially no space on right is shown, but it can be shown when user resizes any element of datagrid -- for last column: CanUserResize=false, Width=*
So far I can see two problems with WPF datagrid:
misleading naming
contradiction of features
I am still all ears to how really solve this issue.
Set the width for the data grid to "Auto". You're allowing the columns to resize correctly within the grid itself, but you've hard-wired the width to 200.
UPDATE: Base on #micas's comment, I may have misread. If that's the case, try using 100 for the left column's width and 100* for the right column (note the asterisk). This will default the width for the right column to 100 but allow it to resize to fill the grid.
You can set a column width to star on code.
In your constructor, add:
Loaded += (s, e) => dataGrid1.Columns[3].Width =
new DataGridLength(1, DataGridLengthUnitType.Star);
I've just implemented this as an attached behavior. The problem is when you set the DataGrid's last column to *, it does resize to fit, but then all the auto-fitting of the other cells messes up. To resolve this, the attached behaviour does a manual auto-fit of other (non last) cells.
This also works when resizing the other columns - once loaded, you can resize and the last column will always fill. Note this behavior works once on the Loaded event
// Behavior usage: <DataGrid DataGridExtensions.LastColumnFill="True"/>
public class DataGridExtensions
{
public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));
public static void SetLastColumnFill(DataGrid element, bool value)
{
element.SetValue(LastColumnFillProperty, value);
}
public static bool GetLastColumnFill(DataGrid element)
{
return (bool)element.GetValue(LastColumnFillProperty);
}
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
}
private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
var lastColumn = dataGrid.Columns.LastOrDefault();
if(lastColumn != null)
lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
// Autofit all other columns
foreach (var column in dataGrid.Columns)
{
if (column == lastColumn) break;
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
}
}
}
Be forewarned: It's a hack....
I registered to the "AutoGeneratedColumns" event in the "OnLastColumnFillChanged" method of Dr. ABT's class and copied the Loaded method into it, and it works. I haven't really thoroughly tested it yet, so YMMV.
My change:
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
}
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
var lastColumn = dataGrid.Columns.LastOrDefault();
if (lastColumn != null)
lastColumn.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
// Autofit all other columns
foreach (var column in dataGrid.Columns)
{
if (column == lastColumn) break;
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
}
}
Oh, and don't forget to add the namespace to the XAML declaration! :)
Up top:
xmlns:ext="clr-namespace:TestProject.Extensions"
And then in the DataGrid declaration:
ext:DataGridExtensions.LastColumnFill="True"
Update:
I said that the mileage would vary! Mine certainly did.
That whole "autofit columns" bit caused some of my columns in a DataGrid with a variable number of columns to only be as wide as the column header. I removed that portion, and now it seems to be working on all of the DataGrids in the application.
Now I have:
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
UpdateColumnWidths(dataGrid);
}
private static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
UpdateColumnWidths(dataGrid);
}
private static void UpdateColumnWidths(DataGrid dataGrid)
{
var lastColumn = dataGrid.Columns.LastOrDefault();
if (lastColumn == null) return;
lastColumn.Width = new DataGridLength(1.0d, DataGridLengthUnitType.Star);
}
Here's a very simple answer, all performed in code behind. :-)
All columns will be auto-sized; the final column will fill all remaining space.
// build your datasource, e.g. perhaps you have a:
List<Person> people = ...
// create your grid and set the datasource
var dataGrid = new DataGrid();
dataGrid.ItemsSource = people;
// set AutoGenerateColumns to false and manually define your columns
// this is the price for using my solution :-)
dataGrid.AutoGenerateColumns = false;
// example of creating the columns manually.
// there are obviously more clever ways to do this
var col0 = new DataGridTextColumn();
col0.Binding = new Binding("LastName");
var col1 = new DataGridTextColumn();
col1.Binding = new Binding("FirstName");
var col2 = new DataGridTextColumn();
col2.Binding = new Binding("MiddleName");
dataGrid.Columns.Add(col0);
dataGrid.Columns.Add(col1);
dataGrid.Columns.Add(col2);
// Set the width to * for the last column
col2.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
I might be a bit late, but you can try my code from this question. I extended original grid and added method for the last column stretching:
private void StretchLastColumnToTheBorder()
{
if (ViewPortWidth.HasValue)
{
var widthSum = 0d;
for (int i = 0; i < Columns.Count; i++)
{
if (i == Columns.Count - 1 && ViewPortWidth > widthSum + Columns[i].MinWidth)
{
var newWidth = Math.Floor(ViewPortWidth.Value - widthSum);
Columns[i].Width = new DataGridLength(newWidth, DataGridLengthUnitType.Pixel);
return;
}
widthSum += Columns[i].ActualWidth;
}
}
}
where ViewPortWidth is:
public double? ViewPortWidth
{
get
{
return FindChild<DataGridColumnHeadersPresenter>(this, "PART_ColumnHeadersPresenter")?.ActualWidth;
}
}
So, you have to find the visual child (answer from here) of type DataGridColumnHeadersPresenter, which has the width of the viewport and calculate the width of the last column. To do it automatically, you can fire this method on LayoutUpdated event. Additionally, you can add a DependencyProperty, indicating, whether automatical stretching of the last column should be performed.
Based on the update pennyrave gave to DR.ABT's answer I made a further update to get this to work better. It's still a hack, but it seems to work better than either of their answers when I'm constantly updating the DataGrid's ItemsSource property. If I try to use a star or auto width anywhere, WPF insists that all the columns are only 20 pixels wide, so I hardcode them all based off the auto values they were set to.
I've added an invoke to the AutoGeneratedColumns event to make it delay a bit. Without this delay, all the columns insist that they are only 20 pixels wide. They still sometimes do, but I've got a check for that and it seems to work, (but the columns are rendered wrong, then corrected a millisecond later.)
Ideally we'd apply the column sizes after WPF figures out what the automatic sizes would be, and before the DataGrid is rendered, but I can't find any way to get my code to run there. It's either too early or too late.
public class DataGridExtensions
{
public static readonly DependencyProperty LastColumnFillProperty = DependencyProperty.RegisterAttached("LastColumnFill", typeof(bool), typeof(DataGridExtensions), new PropertyMetadata(default(bool), OnLastColumnFillChanged));
public static void SetLastColumnFill(DataGrid element, bool value)
{
element.SetValue(LastColumnFillProperty, value);
}
public static bool GetLastColumnFill(DataGrid element)
{
return (bool)element.GetValue(LastColumnFillProperty);
}
private static void OnLastColumnFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var dataGrid = d as DataGrid;
if (dataGrid == null) return;
dataGrid.Loaded -= OnDataGridLoaded;
dataGrid.Loaded += OnDataGridLoaded;
dataGrid.AutoGeneratedColumns -= OnDataGrid_AutoGeneratedColumns;
dataGrid.AutoGeneratedColumns += OnDataGrid_AutoGeneratedColumns;
}
private static void OnDataGrid_AutoGeneratedColumns(object sender, EventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => afterInvoke(dataGrid)));
}
private static void afterInvoke(DataGrid dataGrid)
{
bool nonMin = false;
foreach (var col in dataGrid.Columns)
{
if (col.ActualWidth != col.MinWidth)
{
nonMin = true;
}
}
if(nonMin)
{
OnDataGridLoaded(dataGrid, null);
}
}
public static void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
var dataGrid = sender as DataGrid;
if (dataGrid == null) return;
// set size of columns
double sizeSoFar = 0;
for(int i =0; i < dataGrid.Columns.Count; i++)
{
var column = dataGrid.Columns[i];
//if last column
if (i == dataGrid.Columns.Count-1)
{
sizeSoFar = dataGrid.ActualWidth - sizeSoFar - 2;//2 pixels of padding
if(column.ActualWidth != sizeSoFar)
{
column.MinWidth = sizeSoFar;
column.Width = new DataGridLength(sizeSoFar);
}
}
else //not last column
{
double beforeWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToCells);
double sizeCellsWidth = column.ActualWidth;
column.Width = new DataGridLength(1, DataGridLengthUnitType.SizeToHeader);
double sizeHeaderWidth = column.ActualWidth;
column.MinWidth = Math.Max(beforeWidth, Math.Max(sizeCellsWidth, sizeHeaderWidth));
sizeSoFar += column.MinWidth; //2 pixels of padding and 1 of border
}
}
}
}
Remember to add something like xmlns:Util="clr-namespace:MyProject.Util" to your window tag at the top of your xaml and then you can use Util:DataGridExtensions.LastColumnFill="True" in your DataGrid tag.

How to draw connecting lines between two controls on a grid WPF

I am creating controls (say button) on a grid. I want to create a connecting line between controls.
Say you you do mousedown on one button and release mouse over another button. This should draw a line between these two buttons.
Can some one help me or give me some ideas on how to do this?
Thanks in advance!
I'm doing something similar; here's a quick summary of what I did:
Drag & Drop
For handling the drag-and-drop between controls there's quite a bit of literature on the web (just search WPF drag-and-drop). The default drag-and-drop implementation is overly complex, IMO, and we ended up using some attached DPs to make it easier (similar to these). Basically, you want a drag method that looks something like this:
private void onMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UIElement element = sender as UIElement;
if (element == null)
return;
DragDrop.DoDragDrop(element, new DataObject(this), DragDropEffects.Move);
}
On the target, set AllowDrop to true, then add an event to Drop:
private void onDrop(object sender, DragEventArgs args)
{
FrameworkElement elem = sender as FrameworkElement;
if (null == elem)
return;
IDataObject data = args.Data;
if (!data.GetDataPresent(typeof(GraphNode))
return;
GraphNode node = data.GetData(typeof(GraphNode)) as GraphNode;
if(null == node)
return;
// ----- Actually do your stuff here -----
}
Drawing the Line
Now for the tricky part! Each control exposes an AnchorPoint DependencyProperty. When the LayoutUpdated event is raised (i.e. when the control moves/resizes/etc), the control recalculates its AnchorPoint. When a connecting line is added, it binds to the DependencyProperties of both the source and destination's AnchorPoints. [EDIT: As Ray Burns pointed out in the comments the Canvas and grid just need to be in the same place; they don't need to be int the same hierarchy (though they may be)]
For updating the position DP:
private void onLayoutUpdated(object sender, EventArgs e)
{
Size size = RenderSize;
Point ofs = new Point(size.Width / 2, isInput ? 0 : size.Height);
AnchorPoint = TransformToVisual(node.canvas).Transform(ofs);
}
For creating the line class (can be done in XAML, too):
public sealed class GraphEdge : UserControl
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Source { get { return (Point) this.GetValue(SourceProperty); } set { this.SetValue(SourceProperty, value); } }
public static readonly DependencyProperty DestinationProperty = DependencyProperty.Register("Destination", typeof(Point), typeof(GraphEdge), new FrameworkPropertyMetadata(default(Point)));
public Point Destination { get { return (Point) this.GetValue(DestinationProperty); } set { this.SetValue(DestinationProperty, value); } }
public GraphEdge()
{
LineSegment segment = new LineSegment(default(Point), true);
PathFigure figure = new PathFigure(default(Point), new[] { segment }, false);
PathGeometry geometry = new PathGeometry(new[] { figure });
BindingBase sourceBinding = new Binding {Source = this, Path = new PropertyPath(SourceProperty)};
BindingBase destinationBinding = new Binding { Source = this, Path = new PropertyPath(DestinationProperty) };
BindingOperations.SetBinding(figure, PathFigure.StartPointProperty, sourceBinding);
BindingOperations.SetBinding(segment, LineSegment.PointProperty, destinationBinding);
Content = new Path
{
Data = geometry,
StrokeThickness = 5,
Stroke = Brushes.White,
MinWidth = 1,
MinHeight = 1
};
}
}
If you want to get a lot fancier, you can use a MultiValueBinding on source and destination and add a converter which creates the PathGeometry. Here's an example from GraphSharp. Using this method, you could add arrows to the end of the line, use Bezier curves to make it look more natural, route the line around other controls (though this could be harder than it sounds), etc., etc.
See also
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/dd246675-bc4e-4d1f-8c04-0571ea51267b
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part1.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part2.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part3.aspx
http://www.codeproject.com/KB/WPF/WPFDiagramDesigner_Part4.aspx
http://www.syncfusion.com/products/user-interface-edition/wpf/diagram
http://www.mindscape.co.nz/products/wpfflowdiagrams/

Resources