Save/Load link routing information

I would like for my links to avoid other links as well as nodes. I would prefer that they go around other links instead of jumping over them or crossing them. The Routing=“AvoidsNodes” property seems to work just fine for nodes, but links continue to cross one another unnecessarily.

How can I get link routes to avoid other links as well as nodes?

I understand that setting go:Part.Reshapable=“True” on the go:LinkPanel DataTemplate gives the user some ability to re-route links without changing their endpoints. I thought I might be able to leverage this to get around my problem (not as nice of a solution, but…), but this solution leads to another question - how can I save and restore the route information for my links?

Again, what I really need is a way to make my links route around other links where possible…

A general solution for that problem isn’t possible, but even a solution that tries hard to avoid link crossings isn’t among GoXam’s current features. We have been working on something towards these goals, but it won’t be ready for this next release (version 1.1).

You can get or set the Points for a link via the Link.Route.Points property.

Your code to read your diagram has to make sure it sets all of the link points only after all of the nodes have been created; otherwise the realization of a node will result in the connected links being re-routed, discarding the points you had just restored.

We hope to improve this particular aspect of GoXam before version 1.1 ships.

Thanks very much for your feedback.

I am successfully saving the Link.Route.Points data out and loading it back in within my Diagram.LayoutCompleted
EventHandler. However, I must be missing some kind of Redraw() call as the diagram is not updated until I modify it (e.g. when I move a node, the loaded link routing takes effect).

I’ve tried several methods to force the redraw - Diagram.UpdateLayout(), Diagram.InvalidateArrange(), Diagram.PartManager.InvalidateArrange(), Link.InvalidateArrange() on the link whose route was modified, etc - but so far either the redraw doesn’t seem to happen or it happens with recomputed routes!

How do I force a redraw without changing the routing data? Perhaps I’m missing some invalidation calls when I’m loading in the route points?

This is an area where we still plan to make improvements. The basic problem is that the Route.Points property cannot be data-bound.

Anyway, I’ll investigate this situation and see if there’s a good work-around for now.

Here’s one way to do it. I have modified the StateChart sample to save/load the Route.Points list of Points in the XML.

There are two parts of this puzzle. One is adding the Points information to the link data, in this sample the Transition class. Presumably you have already done this, although it isn’t clear if you have extended your link data class with the set of link points.

The second part is to copy the points information to/from the link data to the Link.Route. The tricky point here is making sure you do so after all of the nodes have been realized.

So for the first part, we add a property to the Transition link data class:

[code] public class Transition : GraphLinksModelLinkData<String, String> {
. . .

public List<Point> Points { get; set; }

// write the extra property on the link data
public override XElement MakeXElement(XName n) {
  XElement e = base.MakeXElement(n);
  e.Add(XHelper.Attribute("Curviness", this.Curviness, Double.NaN));
  e.Add(XHelper1.Attribute("Points", this.Points, new List<Point>()));
  return e;
}

// read the extra property on the link data
public override void LoadFromXElement(XElement e) {
  base.LoadFromXElement(e);
  this.Curviness = XHelper.Read("Curviness", e, Double.NaN);
  this.Points = new List<Point>(XHelper1.Read("Points", e, new List<Point>()));
}

}[/code]
In this case I’d like to use standard conversion methods to convert a sequence of Points to a String and vice-versa. Those methods are normally static methods of the XHelper class, but this particular one (IEnumerable) doesn’t exist in version 1.0. So I’ve extracted those methods into a separate XHelper1 class:

// static methods taken from version 1.1 XHelper class: public static class XHelper1 { public static XAttribute Attribute(XName n, IEnumerable<Point> v, IEnumerable<Point> defval) { return XHelper.Attribute<IEnumerable<Point>>(n, v, defval, ToString); } public static IEnumerable<Point> Read(XName n, XElement e, IEnumerable<Point> defval) { return XHelper.Read<IEnumerable<Point>>(n, e, defval, ToIEnumerableOfPoint); } public static String ToString(IEnumerable<Point> v) { StringBuilder sb = new StringBuilder(); bool first = true; foreach (Point p in v) { if (!first) sb.Append(" "); first = false; sb.Append(XmlConvert.ToString(p.X)); sb.Append(" "); sb.Append(XmlConvert.ToString(p.Y)); } return sb.ToString(); } public static IEnumerable<Point> ToIEnumerableOfPoint(String s) { if (s == null) return Enumerable.Empty<Point>(); char[] separators = { ' ' }; String[] nums = s.Split(separators, StringSplitOptions.RemoveEmptyEntries); Point[] v = new Point[nums.Length/2]; for (int i = 0; i < nums.Length; i++) { double x = XmlConvert.ToDouble(nums[ i ]); i++; double y = ((i < nums.Length) ? XmlConvert.ToDouble(nums[ i ]) : 0); v[i/2] = new Point(x, y); } return v; } }
So, to summarize, the above changes add a Points property to the link data class and augment methods to read and write that data from and to XML.

For the second part of the task, here are the modifications to the Load and Save button click methods, with the extra property and method that the load functionality depends on:

[code] // save and load the model data as XML, visible in the “Saved” tab of the Demo
private void Save_Click(object sender, RoutedEventArgs e) {
var model = myDiagram.Model as GraphLinksModel<State, String, String, Transition>;
if (model == null) return;
foreach (Link link in myDiagram.Links) {
Transition transition = (Transition)link.Data;
if (transition == null) continue;
transition.Points = new List(link.Route.Points);
}
XElement root = model.Save<State, Transition>(“StateChart”, “State”, “Transition”);
Demo.MainPage.Instance.SavedXML = root.ToString();
LoadButton.IsEnabled = true;
model.IsModified = false;
}

private void Load_Click(object sender, RoutedEventArgs e) {
  var model = myDiagram.Model as GraphLinksModel<State, String, String, Transition>;
  if (model == null) return;
  try {
    XElement root = XElement.Parse(Demo.MainPage.Instance.SavedXML);
    model.Load<State, Transition>(root, "State", "Transition");
    this.NeedsLoadedRoutes = true;
  } catch (Exception ex) {
    MessageBox.Show(ex.ToString());
  }
  model.IsModified = false;
}

private bool NeedsLoadedRoutes { get; set; }

private void UpdateRoutes(object sender, DiagramEventArgs e) {
  if (!this.NeedsLoadedRoutes) return;
  this.NeedsLoadedRoutes = false;
  foreach (Link link in myDiagram.Links) {
    Transition transition = (Transition)link.Data;
    if (transition == null) continue;
    link.Route.Points = transition.Points;
  }
}[/code]

The only change to Save_Click is to explicitly copy the Link.Route.Points data to the Transition link data objects. The same is true for Load_Click, except that instead of changing the Route.Points property right away, it does so only after everything else has settled down.

It uses the Diagram.LayoutCompleted event as one way to execute this code late enough so that the construction of the nodes does not (inadvertantly) cause the connected link routes to be recomputed. The NeedsLoadedRoutes property is just to ensure that the code is only executed once after a Load_Click.

Don’t forget to register that event handler:

    myDiagram.LayoutCompleted += UpdateRoutes;

Thank you very much. Your solution works well, and I appreciate your thorough explanation. I hope that Northwoods will improve the routing algorithm in the future, but saving and loading the route points provides a reasonable solution for now. I’ve been very impressed by the support I’ve been provided in this forum. Thank you again for your efforts.

Hello Walter,

im searching for a possibility to update the link.Routes property using data binding. Im using the mvvm-pattern so i cant access myDiagram or the Links property of diagram in my view model or code behind. Is there a workaround for this Problem?

First off, this post is very old – in version 1.1 we added some mechanisms to make the above work more complete. But as you can see in several of the samples, you still need to restore the Link.Route.Points at or after a Diagram.InitialLayoutCompleted event.
A basic assumption of data binding is that the order in which bindings are evaluated doesn’t matter. Unfortunately, that’s not true for nodes and links, because link routes very much depend on where and how big the nodes are. You would expect a change in a node due to data binding might cause the node to change position or size, in which case the links would need to be rerouted. But if the data binding of Link.Route.Points (if that were possible – it’s not a DependencyProperty) were to be evaluated before the data binding of the node property, then you would lose the link data’s route.

This is particularly true because of layout animation – nodes may move quite a bit before settling down at their intended location. Even without layout animation, the construction and initialization and measurements and arrangement of nodes might cause any connected links to be rerouted.

A further complication is that the Route.Points is a collection that may be modified. Making the Route.Points property a DependencyProperty would not help with keeping track of such changes.

So the implementation of having the link routes only be set in at an InitialLayoutCompleted event, or later, is really the only solution.

I suspect that it’s possible to come up with a way to achieve what you want, but it isn’t clear to me at the moment.