Large diagrams and layout algorithms in WPF
In a previous article I already showed how large diagrams can benefit from layout algorithms. Netron was at the basis of the pretty pictures in this article and it was fairly easy to port the code to WPF, though WPF has some threading intricacies:
- Although the BackgroundWorker helper class is supposed to help in using non-UI threads in your application, it did not perform as expected on various occasions. Here I was unable to trace the source of the problme…threading issues are in general horrendous to pinpoint.
- The Dispatcher object is your friend when it comes to marshalling modifications from a thread to the UI-thread. The mechanism is easy to use but it can be at times difficult to access the Dispatcher when your architecture is based on a MVC (read: whatever pattern acronym you have chosen to build your WPF application) pattern.
- There seems to be at times a difference between using the Dispatcher you get from the UI elements and the one you access through the Application.Dispatcher property.
- In general, you should minimize the stuff you perform in a BeginInvoke call to keep the Dispatcher queue lightweight.
- Is it possible to access a property rather than a method through the Dispatcher? Can one change the location of a UI-element through the Dispatcher? I haven’t been able to to that, which seems to be a rather crual constratint sometimes.
- The VS2008 just stops sometimes because some debugging optimization was ‘optimized away’! This occurs when you try to peek into properties of some objects in the non-UI thread.
There is no noticeable difference between using layout algorithms in WPF and in GDI, the positioning and handling of coordinates is similar. On the other hand, there is a big difference between the responsiveness of WPF. In the examples below I was able to lay out 1500 nodes in less than five seconds but altering the diagram or scrolling produces grinding sounds. Obviously the cause is in the usage of the Control as a base class and the heavy weight of carrying animation, databinding and other stuff which for large diagrams is usually less an advantage. Using the System.Windows.Shapes namespace of .Net would be a better choice for big diagrams and layout.
When it comes to diagramming there is always a choice between sacrificing features or sacrificing scalability. There is also the balance between making a general purpose control and implementing stuff for a particular target group. For example, if you are interested in HMI diagrams you are not interested in large diagrams and vice versa. As well as the choices in what to deliver as shape libraries. Because it’s really hard to enable users to create shapes without understand the API (or opening the source code) one has to deliver a common set of shape libraries, but of course everyone has particular needs. If you use Visio you can easily add new shape to libraries but I wonder whether the serialized shapes are just groupings of predefined elements or really standalone blocks. In that respect one can consider grouping of basic shapes a good solution for a wide audience. I think, however, that for custom diagramming solutions and integrations one inevitably flows into custom code and custom diagramming controls. Diagramming software cannot be separated from consultancy unless you constraint yourself to the average (common) needs.
But anyway, in April 2006 I produced large diagrams in Netron and here they are again but based on pure WPF code. The first screenshot represents randomly generated nodes on the diagramming surface.
The radial produce in less than three seconds a beautiful set of concentric nodes:
The balloon tree layout produced a rather dispersive diagram is the same time span:
Finally, the force directed layout produced the expected organic layout in tick more than five seconds:
It’s very unusual
Can’t help sharing this one, can’t even believe the whole thing wasn’t set up. Politics
The front fell off… from David Hegarty on Vimeo.
GraphSquare architecture IV
In the previous part we talked a bit about tools and surface mechanisms, this time we’ll talk about the serialization of diagrams, which is a huge topic on its own and a complicated one at times too. You probably know that serialization is related to copy/paste, saving things to files, the property grid, drag-drop… In fact serialization is pretty much everywhere: WCF services, Windows workflows, P2P communication and more. The .Net frameworks gives you a wealth of mechanisms to help you serialize things with various levels of sophistication: attributes (Serializable, DataContract,…), interfaces (ISerializable, IXmlSerializable…) and formatters (binary, soap, xml…) together with whole namespaces full of utility classes. In the next few paragraphs I’ll review first some basics of serialization, explain the internal structure of our (MVC) Model, highlight the problems you encounter when trying to serialize a diagram and, finally, explain the (non-standard) serialization system of GraphSquare.
GraphSquare architecture III
In the previous part we looked at the MVC core, this time we’ll highlight the ideas and the code related to ‘tools’. A tool is basically a series of actions the user performs and which results in a command on the undo-stack. For example:
- selecting some shapes with the typical rubberband is a tool, the series of actions being related to the mouse down, mouse move and mouse up events.
- scaling or rotating a shape, which involves the adorners and the mouse events
- creating a connection
and so on. The abstract tool concept originates from the fact that if you don’t package a series of actions in a separate class you inevitably have a complex logical flow in the overriden mouse-methods of the surface. So, instead of having various if-then-else statements in the MouseDown, MouseMove and MouseUp handlers you simply loop over all registered tools and whichever tool is ‘on’ can react accordingly. This approach has the additional advantage that you can
- add tools with ease
- disable or overrule other tools in the loop
- chain tools, if necessary
Below is a snippet of the mouse handlers which are called by the surface corresponding to the mouse events on this surface. Note that the loop in these handlers are broken as soon as one of the tools have opted to handle the event.
public void MouseDown(MouseEventArgs e) { foreach (ITool tool in toolList) { if (tool is Core.IMouseListener) if ((tool as Core.IMouseListener).MouseDown(e)) return; } } public void MouseMove(MouseEventArgs e) { foreach (ITool tool in toolList) { if (tool is Core.IMouseListener) if ((tool as Core.IMouseListener).MouseMove(e)) return; } } public void MouseUp(MouseEventArgs e) { foreach (ITool tool in toolList) { if (tool is Core.IMouseListener) if ((tool as Core.IMouseListener).MouseUp(e)) return; } } |
Most of the tools have some kind of visual feedback, on creating a rectangle you would see for example a rubberband which previews the end-result. For this purpose a few classes are necessray which specializes the ITool interface (depicted above) and all tools which need a rubberband are inheriting from it. The whole rubberband (sometimes called ‘marching ants’) affair is also taken separately in other constructs (a static helper class) and the process of creating tools becomes in the end a game of putting some blocks together. In fact, a dummy tool with visual feedback would look something like this:
public class DummyTool : RubberCreationToolBase { #region Constructor ///<summary> ///Default constructor ///</summary> public DummyTool():base("Dummy Tool") { } #endregion public override void OnExecute(object sender, System.Windows.Input.ExecutedRoutedEventArgs e) { GhostManager.Surface = sender as DiagramSurface; base.OnExecute(sender, e); } public override void DrawGhost(System.Windows.Rect rec) { GhostManager.DrawRectangularGhost(rec); } public override void OnRubberCreated(System.Windows.Rect rec) { Surface.Controller.Output("The '" +this.Name + "' tool finished but no realshape was added."); } } |
The overridable DrawGhost method allows you to give whatever feedback you wish while the overridable OnRubberCreated allows you to do whatever you wish after the rubber was creates, i.e. after the user released the mouse. It’s precisely in this method that one should package the action (say, the creation of a shape) in a command and push it onto the undo-stack. In the dummy tool above only an informative message is displayed.
Of course, in order to manage the available tools and to connect them to the UI you need some more code in the form of some tool manager. The tool manager is just a wrapped generic list created through Unity inside the surface class. Note, in this context, that the concept of a tool only makes sense on the UI level and is, hence, not re-usable in other surfaces (e.g. a web-interface).
In the next part we’ll discuss the other end of the architecture; data abstraction and the serialization of diagrams.
GraphSquare architecture II
In this second part we will describe in some details the MVC core of GraphSquare (G2). See Part I for the intro and a feature overview.
The surface (or View)
The surface holds and presents the UIElement by means of an inherited Canvas. All diagram elements inherit (sometimes through a long chain) from the Control class, but we will describe later on the hierarchy behind shapes and diagram elements.
The surface is a rather ‘dumb’ piece in the sense that it’s mostly a sink for all actions from the user and a drain for all things coming from the controller:
- it presents a variety of routed commands which allows one to attach buttons and other UI element to act upon the diagram
- it offers a lot of events one can hook on to (OnShapeAdded, OnPageAdded, OnLayerRemoved…)
- it listens to mouse events and pass them on to the Controller
- it hosts the various tools (selection tool, rotate tool…) which only have a meaning on the surface level: the controller doesn’t know about ‘tools’, much less the Model
- it enables zooming, panning and scrolling of the diagram (which is really easy in WPF)
- it presents all the methods you need to manipulate, edit and create diagrams (and all its organizing elements like layers, pages, groups) by means of code
The surface is, hence, just a facade to the outside world and this is precisely why one can replace it with Silverlight or a web-based interface. You can replace here the word ‘facade’ with API if you prefer since the collection of event, proprerties and methods available on the diagram control really represent the programming interface to the diagram the control).
One important piece on top of the list is, however, the presentation factory. The controller and the model are unaware of the layout algorithm and simply hold the raw data of the shape. When a shape (or diagram element in general) is added to the diagram it really starts in the Document and bubbles up to the surface. This occurs by means of standard chained events and this chain end at the surface where either the diagram element is added ‘as is’ or handed over to the presentation factory:
void Controller_OnDiagramElementAdded(object sender, DiagramEntityArgs e) { if (e.Entity is ILayoutShape) { presentationFactory.Present(e.Entity as ITreeShape); } else if (e.Entity is UIElement) this.Children.Add(e.Entity as UIElement); } |
For example, the standard diagram (we call it a ‘Concept map’) doesn’t need extra presentation processing while a mindmap uses the presentation factory to lay shapes in the well-known fashion. The presentation factory is responsible for the layout and this is the place where one can either use the wonderful possibilities of WPF or rely on precise positioning of elements on the Canvas. We have even gone a step beyond this and use the presentation factory to convert shapes on the fly and sometimes ‘wrap’ shapes in containers, which e.g. makes it possible to have grouping or sub-structures which not necessarily have a meaning on the controller or model level.
keep looking »






