Ah, what’s missing are overrides of several LayeredDigraphLayout methods: NodeMinLayerSpace, NodeMinColumnSpace, and LinkStraightenWeight, which normally depend on LayoutVertex.Node being null to mean that it’s a dummy vertex, not a vertex corresponding to a real Node.  But since there’s aren’t any real Nodes, …
Note that the following code does NOT do the routing that LayeredDigraphLayout normally does when it is operating on real Links.  I’m not sure there’s an easy solution for that.
[code]
<FrameworkElement.Resources>
<go:NodePanel Sizing=“Fixed”
go:Part.SelectionAdorned=“True”
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
go:Node.LocationSpot=“Center”>
<go:NodeShape go:NodePanel.Figure=“RoundedRectangle”
Width="{Binding Path=Data.Width}"
Height="{Binding Path=Data.Height}"
Fill="{Binding Path=Data.Color}" Stroke=“Black” StrokeThickness=“1” />
</go:NodePanel>
<DataTemplate x:Key="LinkTemplate">
  <go:LinkPanel>
    <go:Link.Route>
      <go:Route FromSpot="MiddleRight" ToSpot="MiddleLeft" />
    </go:Link.Route>
    <go:LinkShape Stroke="Black" />
  </go:LinkPanel>
</DataTemplate>
</FrameworkElement.Resources>
  
    
      
      
    
    
      
    
    
      
      
        
      
      
      
        
          
            
              
            
          
        
      
      
      
        
      
      
      
        
      
    
  
[/code]
[code]/* Copyright © Northwoods Software Corporation, 2008-2014. All Rights Reserved. */
// This sample demonstrates one way to implement virtualization of the Nodes and Links
// needed for displaying a very large model.
// It depends on the locations and sizes of the Nodes being known in the model
// before the Nodes are actually created.
// Thus using a standard diagram layout is incompatible with this assumption,
// because a normal layout will need to consider the actual sizes of the Nodes
// in order to assign reasonable Locations for those Nodes, but the Nodes have to
// exist already to do so.
// You need to customize the VirtualizingPartManager.ComputeNodeBounds method to
// account for the actual Node DataTemplates used in your application.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Northwoods.GoXam;
using Northwoods.GoXam.Layout;
using Northwoods.GoXam.Model;
namespace VirtualizingLD {
public partial class VirtualizingLD : UserControl {
public VirtualizingLD() {
InitializeComponent();
  // automatically load all of the data AFTER this UserControl has
  // been displayed in the visual tree of the demo app
  myDiagram.Loaded += (s, e) => {
    myDiagram.Dispatcher.BeginInvoke((Action)(() => { LoadData(); }));
  };
}
// Create some sample data.
private void LoadData() {
  var model = new GraphLinksModel<NodeData, String, String, LinkData>();
  var nodes = GenerateNodes();
  model.NodesSource = nodes;
  var links = GenerateLinks(nodes);
  model.LinksSource = links;
  model.Modifiable = true;
  var vpm = (VirtualizingPartManager)myDiagram.PartManager;
  // in production usage you'll probably want to remove these status updates,
  // which slows down scrolling and zooming
  vpm.Status = () => {
    myStatus.Text = "created " + vpm.NodesCount.ToString() + "/" + nodes.Count.ToString() + " nodes," +
      "  " + vpm.LinksCount.ToString() + "/" + links.Count.ToString() + " links."
      + "  " + myDiagram.Panel.DiagramBounds.ToString();
  };
  // NOTE: regular DiagramLayouts (such as LayeredDigraphLayout) will not work with this virtualizing scheme!
  // You need to make sure VirtualizingPartManager.ComputeNodeBounds returns good values
  // for every NodeData object.
  // just in case you set model.Modifiable = true, this makes sure the viewport is updated after every transaction:
  model.Changed += (s, e) => {
    if (e.Change == ModelChange.CommittedTransaction || e.Change == ModelChange.FinishedUndo || e.Change == ModelChange.FinishedRedo) {
      vpm.UpdateViewport(myDiagram.Panel.ViewportBounds, myDiagram.Panel.ViewportBounds);
    }
  };
  // do the initial layout *before* assigning to the Diagram.Model --
  // this avoids creating all of the Nodes and Links and then throwing away everything
  // outside of the viewport
  Layout(model);
  // initialize the Diagram with the model and its data
  myDiagram.Model = model;
  // remove the "Loading..." status indicator
  var loading = myDiagram.PartsModel.FindNodeByKey("Loading");
  if (loading != null) myDiagram.PartsModel.RemoveNode(loading);
}
Random rand = new Random();
// Takes the random collection of nodes and creates a random graph with them.
private ObservableCollection<LinkData> GenerateLinks(ObservableCollection<NodeData> nodes) {
  var linkSource = new ObservableCollection<LinkData>();
  if (nodes.Count < 2) return linkSource;
  for (int i = 0; i < nodes.Count-1; i++) {
    NodeData from = nodes[i];
    int numto = 1+rand.Next(3)/2;
    for (int j = 0; j < numto; j++) {
      int idx = i+5+rand.Next(10);
      if (idx >= nodes.Count) idx = i+rand.Next(nodes.Count-i);
      NodeData to = nodes[idx];
      linkSource.Add(new LinkData() { From = from.Key, To = to.Key });
    }
  }
  return linkSource;
}
// Creates a collection of randomly colored nodes.
// Respects the maximum and minimum number of nodes.
private ObservableCollection<NodeData> GenerateNodes() {
  var nodeSource = new ObservableCollection<NodeData>();
  int minNodes = 300;
  int maxNodes = 500;
  int numberOfNodes = rand.Next(minNodes, maxNodes + 1);
  for (int i = 0; i < numberOfNodes; i++) {
    nodeSource.Add(new NodeData() {
      Key = "Node" + i.ToString(),
      Color = String.Format("#{0:X}{1:X}{2:X}", 120+rand.Next(100), 120+rand.Next(100), 120+rand.Next(100))
    });
  }
  return nodeSource;
}
// Perform nested layouts of Groups first, then do top-level Diagram.Layout
private void Layout(ILinksModel model) {
  ISubGraphLinksModel sgmodel = model as ISubGraphLinksModel;
  foreach (NodeData d in model.NodesSource) {
    if (sgmodel != null && sgmodel.GetIsGroupForNode(d) && sgmodel.GetGroupForNode(d) == null) {
      LayoutSubGraph(d, sgmodel);
    }
  }
  VirtualizingLayeredDigraphLayout layout = myDiagram.Layout as VirtualizingLayeredDigraphLayout;
  if (layout != null) {
    layout.Model = model;
    layout.DoLayout(null, null);  // no Nodes or Links exist!
  }
  // now need to position all NodeData inside subgraphs accurately
  foreach (NodeData d in model.NodesSource) {
    if (sgmodel != null && sgmodel.GetIsGroupForNode(d) && sgmodel.GetGroupForNode(d) == null) {
      CommitSubGraph(d, d.Location, sgmodel);
    }
  }
}
// We cannot get the Group.Layout property value without first creating the Group,
// which virtualization is trying to avoid.  So you will need to customize this method
// to return a new VirtualizingLayeredDigraphLayout with the properties that you want for the
// given NodeData represented by a Group.
private VirtualizingLayeredDigraphLayout GetLayoutForGroup(NodeData sg) {
  return new VirtualizingLayeredDigraphLayout() {
  };
}
private void LayoutSubGraph(NodeData sg, ISubGraphLinksModel model) {
  // recurse into nested groups first
  foreach (NodeData m in model.GetMemberNodesForGroup(sg)) {
    if (model.GetIsGroupForNode(m)) {
      LayoutSubGraph(m, model);
    }
  }
  // now do this group's layout
  VirtualizingLayeredDigraphLayout layout = GetLayoutForGroup(sg);
  if (layout != null) {
    layout.Model = model;
    layout.SubGraph = sg;
    layout.DoLayout(null, null);  // no Nodes or Links exist!
    // and record the resulting size of the group in the data
    Rect gb = Rect.Empty;
    foreach (NodeData m in model.GetMemberNodesForGroup(sg)) {
      if (m.IsSubGraph) {
        m.Location = new Point(m.Location.X + VirtualizingPartManager.GroupHeaderWidth,
                               m.Location.Y + VirtualizingPartManager.GroupHeaderHeight);
      }
      Rect b = VirtualizingPartManager.ComputeNodeBounds(m);
      if (gb.IsEmpty) {
        gb = b;
      } else {
        gb.Union(b);
      }
    }
    if (gb.IsEmpty) gb = new Rect(0, 0, 100, 20);
    gb.X -= VirtualizingPartManager.GroupPadding;
    gb.Y -= VirtualizingPartManager.GroupPadding;
    gb.Width += 2*VirtualizingPartManager.GroupPadding;
    gb.Height += 2*VirtualizingPartManager.GroupPadding;
    // this specifies the size and location of the subgraph (i.e. GroupPanel), not the whole Group:
    sg.Width = gb.Width;
    sg.Height = gb.Height;
    sg.Location = new Point(gb.X + gb.Width/2, gb.Y + gb.Height/2);
  }
}
private void CommitSubGraph(NodeData sg, Point offset, ISubGraphLinksModel model) {
  offset.X += VirtualizingPartManager.GroupPadding;
  offset.Y += VirtualizingPartManager.GroupPadding;
  foreach (NodeData m in model.GetMemberNodesForGroup(sg)) {
    m.Location = new Point(m.Location.X+offset.X-sg.Width/2, m.Location.Y+offset.Y-sg.Height/2);
    if (model.GetIsGroupForNode(m)) {
      CommitSubGraph(m, m.Location, model);
    }
  }
}
private void CollapseExpandButton_Click(object sender, RoutedEventArgs e) {
  // the Button is in the visual tree of a Node
  Button button = (Button)sender;
  Group sg = Part.FindAncestor<Group>(button);
  if (sg != null) {
    NodeData subgraphdata = (NodeData)sg.Data;
    if (!subgraphdata.IsSubGraph) return;
    // always make changes within a transaction
    myDiagram.StartTransaction("CollapseExpand");
    if (sg.IsExpandedSubGraph) {
      subgraphdata.NormalWidth = subgraphdata.Width;
      subgraphdata.NormalHeight = subgraphdata.Height;
      subgraphdata.Width = 10;
      subgraphdata.Height = 10;
    } else {
      subgraphdata.Width = subgraphdata.NormalWidth;
      subgraphdata.Height = subgraphdata.NormalHeight;
    }
    // toggle whether this node is expanded or collapsed
    sg.IsExpandedSubGraph = !sg.IsExpandedSubGraph;
    myDiagram.CommitTransaction("CollapseExpand");
  }
}
}
// This assumes that we can determine the Bounds, in model coordinates, for each data object,
// without having to apply the DataTemplate to create the FrameworkElements in the visual tree.
// You will need to implement the ComputeNodeBounds method for your application.
// In this sample the Location is computed by a VirtualLayeredDigraphLayout
// and the Width and Height are assumed to come from the NodeData class.
public class VirtualizingPartManager : PartManager {
// The viewport has changed -- create new nodes and links that should now be visible
public void UpdateViewport(Rect oldview, Rect newview) {
  this.ViewportBounds = newview;
  Diagram diagram = this.Diagram;
  IDiagramModel model = diagram.Model;
  if (!this.OffscreenQueued) {
    // don't immediately remove nodes and links that have scrolled out of the viewport
    this.OffscreenQueued = true;
    diagram.Dispatcher.BeginInvoke((Action)RemoveOffscreen);
  }
  // maybe create Nodes
  foreach (Object data in model.NodesSource) {
    var node = AddNodeForData(data, model);
    if (node != null && this.SelectedOffscreenNodeData.Contains(data)) {
      this.SelectedOffscreenNodeData.Remove((NodeData)data);
      node.IsSelected = true;
    }
  }
  ILinksModel lmodel = model as ILinksModel;
  if (lmodel != null) {
    // maybe create Links
    foreach (Object data in lmodel.LinksSource) {
      AddLinkForData(data, lmodel);
    }
  }
  // You might want to delete this statement, for efficiency:
  if (this.Status != null) this.Status();
}
public Rect ViewportBounds { get; set; }
// Customize when the PartManager creates Nodes -- when in the viewport or
// when connecting with a link that is in the viewport
protected override bool FilterNodeForData(object nodedata, IDiagramModel model) {
  NodeData data = nodedata as NodeData;
  if (data == null) return true;
  // don't create Node with unknown Location
  if (Double.IsNaN(data.Location.X) || Double.IsNaN(data.Location.Y)) return false;
  // see if the Node would be in the viewport
  Rect thisb = ComputeNodeBounds(data);
  if (Intersects(this.ViewportBounds, thisb)) return true;
  // or if the Node is connected to with a Link that is in the viewport
  foreach (Object otherdata in model.GetConnectedNodesForNode(nodedata)) {
    Rect linkb = thisb;
    linkb.Union(ComputeNodeBounds(otherdata as NodeData));
    if (Intersects(this.ViewportBounds, linkb)) return true;
  }
  return false;
}
public const double GroupPadding = 10;  // what would be GroupPanel.Padding
public const double GroupHeaderWidth = 0;  // on left side of what would be the GroupPanel
public const double GroupHeaderHeight = 18;  // above what would be the GroupPanel
// Account for actual node size, because it would normally be unknown
// until after the Node was actually created and measured.
public static Rect ComputeNodeBounds(NodeData data) {
  // Remember to take the relative Location into account.
  // This assumes the LocationSpot is at the Center of the whole node.
  if (data.IsSubGraph) {
    return new Rect(data.Location.X - data.Width/2 - VirtualizingPartManager.GroupHeaderWidth,
                    data.Location.Y - data.Height/2 - VirtualizingPartManager.GroupHeaderHeight,
                    data.Width + VirtualizingPartManager.GroupHeaderWidth,
                    data.Height + VirtualizingPartManager.GroupHeaderHeight);
  } else {
    return new Rect(data.Location.X - data.Width/2, data.Location.Y - data.Height/2, data.Width, data.Height);
  }
}
// Customize when the PartManager creates Links
protected override bool FilterLinkForData(object linkdata, IDiagramModel model) {
  LinkData data = linkdata as LinkData;
  if (data == null) return true;
  return IsOnscreen(data, model);
}
private bool IsOnscreen(LinkData data, IDiagramModel model) {
  if (data == null) return false;
  NodeData from = model.FindNodeByKey(data.From) as NodeData;
  if (from == null) return false;
  NodeData to = model.FindNodeByKey(data.To) as NodeData;
  if (to == null) return false;
  Rect b = ComputeNodeBounds(from);
  b.Union(ComputeNodeBounds(to));
  return Intersects(this.ViewportBounds, b);
}
// Customize when the PartManager creates Links if the model is not an ILinksModel
protected override bool FilterLinkForData(object fromnodedata, object tonodedata, IDiagramModel model) {
  Rect b = ComputeNodeBounds(fromnodedata as NodeData);
  b.Union(ComputeNodeBounds(tonodedata as NodeData));
  return Intersects(this.ViewportBounds, b);
}
private bool OffscreenQueued { get; set; }
private HashSet<NodeData> SelectedOffscreenNodeData = new HashSet<NodeData>();
// To reduce memory usage, remove existing Nodes and Links that are no longer within the viewport
private void RemoveOffscreen() {
  this.OffscreenQueued = false;
  IDiagramModel model = this.Diagram.Model;
  var offscreennodes = new List<Node>();
  foreach (Node n in this.Diagram.Nodes) {
    if (!FilterNodeForData(n.Data, model)) offscreennodes.Add(n);
  }
  foreach (Node n in offscreennodes) {
    if (n.IsSelected) {
      this.SelectedOffscreenNodeData.Add((NodeData)n.Data);
    }
    RemoveNodeForData(n.Data, model);
  }
  var offscreenlinks = new List<Link>();
  foreach (Link l in this.Diagram.Links) {
    LinkData data = l.Data as LinkData;
    if (data == null) continue;
    if (!IsOnscreen(data, model)) offscreenlinks.Add(l);
  }
  foreach (Link l in offscreenlinks) {
    RemoveLinkForData(l.Data, model);
  }
  // You might want to delete this statement, for efficiency:
  if (this.Status != null) this.Status();
}
// this property is just for informational feedback -- you can delete this:
public Action Status { get; set; }
// Compute intersection of Rects, handling Infinity and NaN properly.
private static bool Intersects(Rect a, Rect b) {
  double tw = a.Width;
  double rw = b.Width;
  double tx = a.X;
  double rx = b.X;
  if (!Double.IsPositiveInfinity(tw) && !Double.IsPositiveInfinity(rw)) {
    tw += tx;
    rw += rx;
    if (Double.IsNaN(rw) || Double.IsNaN(tw) || tx > rw || rx > tw) return false;
  }
  double th = a.Height;
  double rh = b.Height;
  double ty = a.Y;
  double ry = b.Y;
  if (!Double.IsPositiveInfinity(th) && !Double.IsPositiveInfinity(rh)) {
    th += ty;
    rh += ry;
    if (Double.IsNaN(rh) || Double.IsNaN(th) || ty > rh || ry > th) return false;
  }
  return true;
}
}
// This customized DiagramPanel calls VirtualizingPartManager.UpdateViewport when needed
public class VirtualizingDiagramPanel : DiagramPanel {
// replace this functionality to take into account non-existent Nodes
protected override Rect ComputeDiagramBounds() {
Rect db = Rect.Empty;
foreach (NodeData data in this.Diagram.Model.NodesSource) {
Rect b = VirtualizingPartManager.ComputeNodeBounds(data);
if (db.IsEmpty) {
db = b;
} else {
db.Union(b);
}
}
if (db.IsEmpty) db = new Rect(0, 0, 0, 0);
Thickness pad = this.Padding;
db.X -= pad.Left;
db.Width += pad.Left + pad.Right;
db.Y -= pad.Top;
db.Height += pad.Top + pad.Bottom;
return db;
}
// whenever the viewport changes, maybe create or remove some Nodes and Links
protected override void OnViewportBoundsChanged(RoutedPropertyChangedEventArgs<Rect> e) {
  var vpm = this.Diagram.PartManager as VirtualizingPartManager;
  if (vpm != null) vpm.UpdateViewport(e.OldValue, e.NewValue);
  base.OnViewportBoundsChanged(e);
}
}
// Virtualizing LayeredDigraphLayout classes
// Here we try to ignore all methods and properties that deal with Nodes or Links.
// Instead we use Vertex and Edge classes that know about the model data.
// This layout implementation assumes the use of a GraphLinksModel (i.e. an ILinksModel).
public class VirtualizingLayeredDigraphLayout : LayeredDigraphLayout {
public ILinksModel Model { get; set; }
public NodeData SubGraph { get; set; }  // the containing group's data
public override LayeredDigraphNetwork CreateNetwork() {
  return new VirtualizingLayeredDigraphNetwork();
}
// ignore the arguments, because they assume the existence of Nodes and Links
public override LayeredDigraphNetwork MakeNetwork(IEnumerable<Node> nodes, IEnumerable<Link> links) {
  var net = (VirtualizingLayeredDigraphNetwork)CreateNetwork();
  if (this.Model != null) {
    if (this.SubGraph != null) {  // just add members of the given group
      net.AddSubGraph(this.SubGraph, this.Model as ISubGraphLinksModel);
    } else {  // add all top-level nodes and links
      net.AddTopLevelGraph(this.Model);
    }
  }
  return net;
}
protected override double NodeMinLayerSpace(LayeredDigraphVertex v, bool topleft) {
  var vv = v as VirtualizingLayeredDigraphVertex;
  if (vv != null && vv.Data == null) return 0;
  Rect r = v.Bounds;
  Point p = v.Focus;
  if (this.Direction == 90 || this.Direction == 270) {
    if (topleft)
      return p.Y+10;
    else
      return r.Height-p.Y+10;
  } else {
    if (topleft)
      return p.X+10;
    else
      return r.Width-p.X+10;
  }
}
protected override int NodeMinColumnSpace(LayeredDigraphVertex v, bool topleft) {
  var vv = v as VirtualizingLayeredDigraphVertex;
  if (vv != null && vv.Data == null) return 0;
  Rect r = v.Bounds;
  Point p = v.Focus;
  if (this.Direction == 90 || this.Direction == 270) {
    if (topleft)
      return (int)(p.X/this.ColumnSpacing) + 1;
    else
      return (int)((r.Width-p.X)/this.ColumnSpacing) + 1;
  } else {
    if (topleft)
      return (int)(p.Y/this.ColumnSpacing) + 1;
    else
      return (int)((r.Height-p.Y)/this.ColumnSpacing) + 1;
  }
}
protected override double LinkStraightenWeight(LayeredDigraphEdge edge) {
  var fromVertex = edge.FromVertex as VirtualizingLayeredDigraphVertex ;
  var toVertex = edge.ToVertex as VirtualizingLayeredDigraphVertex;
  if ((fromVertex != null && fromVertex.Data == null) && (toVertex != null && toVertex.Data == null))
    return 8;
  if ((fromVertex != null && fromVertex.Data == null) || (toVertex != null && toVertex.Data == null))
    return 4;
  return 1;
}
}
// Use Virtualizing versions of Vertex and Edge.
public class VirtualizingLayeredDigraphNetwork : LayeredDigraphNetwork {
public override LayeredDigraphVertex CreateVertex() {
return new VirtualizingLayeredDigraphVertex();
}
public override LayeredDigraphEdge CreateEdge() {
  return new VirtualizingLayeredDigraphEdge();
}
private Dictionary<NodeData, VirtualizingLayeredDigraphVertex> NodeDataMap = new Dictionary<NodeData, VirtualizingLayeredDigraphVertex>();
private Dictionary<LinkData, VirtualizingLayeredDigraphEdge> LinkDataMap = new Dictionary<LinkData, VirtualizingLayeredDigraphEdge>();
// a replacement for LayeredDigraphNetwork.AddNodesAndLinks using top-level model data instead of Nodes or Links
public void AddTopLevelGraph(ILinksModel model) {
  if (model == null) return;
  NodeDataMap.Clear();
  LinkDataMap.Clear();
  ISubGraphLinksModel sgmodel = model as ISubGraphLinksModel;
  var nodes = model.NodesSource as IEnumerable<NodeData>;
  foreach (NodeData d in nodes) {
    if (sgmodel == null || sgmodel.GetGroupForNode(d) == null) {
      AddNodeData(d, model);
    }
  }
  var links = model.LinksSource as IEnumerable<LinkData>;
  foreach (LinkData d in links) {
    if (sgmodel == null || sgmodel.GetGroupForLink(d) == null) {
      AddLinkData(d, model);
    }
  }
}
// a replacement for LayeredDigraphNetwork.AddNodesAndLinks using a group's members' model data instead of Nodes or Links
public void AddSubGraph(NodeData sg, ISubGraphLinksModel model) {
  if (sg == null || model == null) return;
  NodeDataMap.Clear();
  LinkDataMap.Clear();
  foreach (NodeData d in model.GetMemberNodesForGroup(sg)) {
    AddNodeData(d, model);
  }
  foreach (LinkData d in model.GetMemberLinksForGroup(sg)) {
    AddLinkData(d, model);
  }
}
public void AddNodeData(NodeData d, ILinksModel model) {
  if (NodeDataMap.ContainsKey(d)) return;
  // create and add VirtualizingLayeredDigraphVertex
  var v = (VirtualizingLayeredDigraphVertex)CreateVertex();
  v.Data = d;
  NodeDataMap.Add(d, v);
  AddVertex(v);
}
public void AddLinkData(LinkData d, ILinksModel model) {
  if (LinkDataMap.ContainsKey(d)) return;
  // find connected node data
  var from = (NodeData)model.FindNodeByKey(d.From);
  var to = (NodeData)model.FindNodeByKey(d.To);
  if (from == null || to == null || from == to) return;  // skip
  // now find corresponding vertexes
  VirtualizingLayeredDigraphVertex f;
  NodeDataMap.TryGetValue(from, out f);
  VirtualizingLayeredDigraphVertex t;
  NodeDataMap.TryGetValue(to, out t);
  if (f == null || t == null) return;  // skip
  // create and add VirtualizingLayeredDigraphEdge
  var e = (VirtualizingLayeredDigraphEdge)CreateEdge();
  e.Data = d;
  e.FromVertex = f;
  e.ToVertex = t;
  AddEdge(e);
}
}
// Associate with NodeData rather than with Node.
public class VirtualizingLayeredDigraphVertex : LayeredDigraphVertex {
public NodeData Data {
get { return _Data; }
set {
_Data = value;
if (_Data != null) {
// use bounds information from the NodeData rather than the Node.Bounds!
this.Focus = new Point(_Data.Width/2, _Data.Height/2);
this.Bounds = VirtualizingPartManager.ComputeNodeBounds(_Data);
}
}
}
private NodeData _Data = null;
public override void CommitPosition() {
  if (this.Data != null) {
    this.Data.Location = this.Center;  // set NodeData.Location instead of Node.Location!
  } else {
    base.CommitPosition();
  }
}
}
// Associate with LinkData rather than with Link.
// NOTE: This does not support custom routing of links that is normally done by LayeredDigraphLayout
public class VirtualizingLayeredDigraphEdge : LayeredDigraphEdge {
public LinkData Data { get; set; }
}
// Model data classes
public class NodeData : GraphLinksModelNodeData {
// assume this won’t change dynamically, so don’t need to call RaisePropertyChanged
public String Color { get; set; }
public double Width {
  get { return _Width; }
  set { if (_Width != value) { double old = _Width; _Width = value; RaisePropertyChanged("Width", old, value); } }
}
private double _Width = 100;
public double NormalWidth { get; set; }
public double Height {
  get { return _Height; }
  set { if (_Height != value) { double old = _Height; _Height = value; RaisePropertyChanged("Height", old, value); } }
}
private double _Height = 50;
public double NormalHeight { get; set; }
}
public class LinkData : GraphLinksModelLinkData<String, String> { }
}[/code]