Custom Nodes in WPF Diagram

Here we will look how to define custom diagram nodes in the WPF diagram control, how to style them, how to make their properties appear in the property grid and how to save and load them with the diagram’s saveToXml and loadFromXml methods.

Here is a screenshot of our SubjectNode custom node class that is used in an application for a school curriculum:

I. XAML Template

You will need to add a XAML template for the node us you are creating a custom node because you want to have special-looking nodes. Let’s create a node that has 3 text fields and a background. We will declare the template for this node that we call SubjectNode in XAML this way:

<style targettype="local:SubjectNode">
    <Setter Property="Template">
      <Setter.Value>
        <DataTemplate DataType="local:SubjectNode">
          <Grid>

            <Rectangle
		Stroke="{Binding Stroke}"
		Fill="{Binding Background}" />

            <Grid>              

               <StackPanel Margin="4,8,0,0"  Orientation="Vertical" Grid.Column="1">
                <TextBlock Text="{Binding Subject}" FontWeight="800" Foreground="Black" />
                <TextBlock Text="{Binding Teacher}" Foreground="Blue" />
                <TextBlock Text="{Binding Remarks}" FontSize="9" Foreground="Black" />
              </StackPanel>
            </Grid>

          </Grid>
        </DataTemplate>
      </Setter.Value>
    </Setter>
  </style>

That goes in the contents of <ResourceDictionary>…..</ResourceDictionary> in the xaml file where you store this resourrce dictionary.

You see here that we use a gird as the principal layout container. There we add a rectangle, whose Fill property is bound to a property called Background in the SubjectNode. Next we have another grid that holds a StackPanel. The stack panel is with vertical orientation and it arranges the three TextBlock-s for the three custom fields of the node.

II. Declaring the Custom Node Class

When you create a custom node you need to inherit the TemplatedNode class. In the static construcotr you should call OverrideMetadata on the DefaultStyleKeyProperty to make it use the template that we’ve declared in XAML:

public class SubjectNode : TemplatedNode
{
	static SubjectNode()
	{
		DefaultStyleKeyProperty.OverrideMetadata(
			typeof(SubjectNode), new FrameworkPropertyMetadata(typeof(SubjectNode)));
}

public SubjectNode()
{			
}

Then we declare a constructor without any parameters that is required for the node to be created in XAML. If you want users to be able to create instance of the SubjectNode through drag and drop, you need to declare one more constructor:

	// Required for creating nodes by dragging them from the NodeListView
public SubjectNode(SubjectNode prototype) : base(prototype)
{
	Subject = prototype.Subject;
	Teacher = prototype.Teacher;
	Remarks = prototype.Remarks;
}

III. Properties

We declare the properties that we want: Subject, TeacherName, Remarks and Background as dependency properties the standard way:

public Brush Background
{
	get { return (Brush)GetValue(BackgroundProperty); }
	set { SetValue(BackgroundProperty, value); }
}

public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(
	"Background",
	typeof(Brush),
	typeof(SubjectNode),
	new PropertyMetadata(new SolidColorBrush(Color.FromRgb(223, 235, 250))));

and for the text properties:

public string Remarks
{
	get { return (string)GetValue(RemarksProperty); }
	set { SetValue(RemarksProperty, value); }
}

public static readonly DependencyProperty RemarksProperty = DependencyProperty.Register(
	"Remarks",
	typeof(string),
	typeof(SubjectNode),
	new PropertyMetadata(""));

If we want the properties to be listed in a property grid we need to add a new class that inherits from DiagramNodeProperties. In it we do nothing but list the custom properties together with their type:

public class SubjectNodeProperties : DiagramNodeProperties
{
        internal string Subject;
	internal string Teacher;
	internal string Remarks;
	internal Brush Background;
} 

IV. More Options

Standard diagram nodes support undo and redo as well serialization out of the box. If you want your custom class to support those features as well you need to implement a few more methods. The methods to support undo/redo are SaveProperties and RestoreProperties. They take an instance of the DiagramItemProperties class that allows you to transfer data between the instance of the current node and its DiagramItemProperties instance that store the values of the node’s properties:

protected override void RestoreProperties(DiagramItemProperties props)
{
	base.RestoreProperties(props);
	var state = (SubjectNodeProperties)props;
	Subject = state.Subject;
	Teacher = state.Teacher;
	Remarks = state.Remarks;
	Background = state.Background;
}

protected override void SaveToXml(XmlElement xmlElement, XmlPersistContext context)
{
	base.SaveToXml(xmlElement, context);
	context.WriteString(Subject, "Subject", xmlElement);
	context.WriteString(Teacher, "Teacher", xmlElement);
	context.WriteString(Remarks, "Remarks", xmlElement);
	context.WriteBrush(Background, "Background", xmlElement);
}

The Diagram uses XML for serialization, so if you want your node to be saved and loaded correctly through the Diagram‘s saveToXml and loadFromXml methods you should implement SaveToXml and LoadFromXml. There you write the values o the custom properties of SubjectNode to XML elements and read them from XML elements as well:

protected override void SaveToXml(XmlElement xmlElement, XmlPersistContext context)
{
	base.SaveToXml(xmlElement, context);
	context.WriteString(Subject, "Subject", xmlElement);
	context.WriteString(Teacher, "Teacher", xmlElement);
	context.WriteString(Remarks, "Remarks", xmlElement);
	context.WriteBrush(Background, "Background", xmlElement);
}

protected override void LoadFromXml(XmlElement xmlElement, XmlPersistContext context)
{
	base.LoadFromXml(xmlElement, context);
	Subject = context.ReadString("Subject", xmlElement);
	Teacher = context.ReadString("Teacher", xmlElement);
	Remarks = context.ReadString("Remarks", xmlElement);
	Background = context.ReadBrush("Background", xmlElement);
}

You can download the sample that uses custom SubjectNode from https://mindfusion.eu/samples/wpf/diagram/Curriculum.zip

About Diagramming for WPF: This is the right tool to create flowcharts in WPF that always meet your requirements. The library offers more than 100 predefined node shapes, extensive event set and more than 15 exporters and importers. Each diagram that you build has a completely customizable look through styles, themes and appearance properties for each part of the flowchart. The numerous samples and detailed documentation help you learn quickly how to integrate the component into your own application. You can download the trial version, which has no feature restrictions and does not expire from the WPF Diagram Page on MindFusion website.

New Release for the Free JS Chart Library

MindFusion Free JS Chart has a new release with the following new features:

– All Series can accept now simple JavaScript array-s as arguments instead of Collections.List instances
– The ToolTip class is greatly extended with many new properties that allow you to customize the apparance and position of tooltips
– The Color.knownColors field lists all standard CSS color names
– Brush and Pen instances can be created with simple strings that specify the HTML code of the color as argument instead of Color objects.
– The yLabelAlignment property of BiaxialChart specifies horizontal alignment of Y-axis labels.
– Texts are now properly underlined when FontStyle.Underline is set.

Free JS Chart is MindFusion charting library that is offered free of charge for commercial use. No attribution is required.

More about MindFusion Free JS Chart at https://mindfusion.eu/free-js-chart.html

Collaborative drawing with MindFusion.Diagramming and SignalR

In this post we’ll show how to use the ASP.NET MVC diagram library and SignalR to implement collaborative drawing of diagrams. This can be useful in visual planning tools where users work together on a task, such as project management or mind-mapping applications.

The complete sample project is available here –
CollabMindMap.zip

Start by creating an ASP.NET MVC application in Visual Studio. Open Tools -> Library Package Manager -> Package Manager Console and install the MindFusion.Diagramming.Mvc package –

Install-Package MindFusion.Diagramming.Mvc 

While we are there, also install the SignalR package –

install-package Microsoft.AspNet.SignalR

From the project’s context menu, Add submenu, select OWIN startup class and add SignalR to the OWIN pipeline by calling –

app.MapSignalR();

Now lets add a diagram view to the home page at Views/Home/Index.cshtml, load the necessary script files and wire up diagram event handlers that will send change notifications to the hub –

@using MindFusion.Diagramming
@using MindFusion.Diagramming.Mvc

@{
    var diagView = new DiagramView("diagramView")
        .NodeCreatedScript("onNodeCreated")
        .NodeModifiedScript("onNodeModified")
        .NodeTextEditedScript("onNodeTextEdited")
        .LinkCreatedScript("onLinkCreated")
        .LinkModifiedScript("onLinkModified")
        .LinkTextEditedScript("onLinkTextEdited")
        .ControlLoadedScript("onDiagramLoaded")
        .SetAllowInplaceEdit(true);

    diagView.Diagram.DefaultShape = Shapes.Ellipse;
}

@Html.DiagramView(diagView, new { style = "width:700px; height:600px;" })

@section scripts
{
    @Scripts.Render("~/Scripts/jquery.signalR-2.0.0.js")
    @Scripts.Render("~/Scripts/MindMap.js")
    @Scripts.Render("~/signalr/hubs")
}

The hub will synchronize operations done on the diagram by one client by sending a notification to all other connected clients. From the project context menu add a SignalR hub class, naming it DiagramHub. The model class we’ll use to describe node changes looks like this –

public class NodeModel
{
    [JsonProperty("x")]
    public double X { get; set; }

    [JsonProperty("y")]
    public double Y { get; set; }

    [JsonProperty("width")]
    public double Width { get; set; }

    [JsonProperty("height")]
    public double Height { get; set; }

    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("text")]
    public string Text { get; set; }
}

Add these three methods to the hub class to synchronize node creation, move, resize and edit-text operations –

public void NodeCreated(NodeModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).nodeCreated(clientModel);
}
public void NodeModified(NodeModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).nodeModified(clientModel);
}
public void NodeTextEdited(NodeModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).nodeTextEdited(clientModel);
}

The diagram event handlers in MindMap.js fill in the model objects and call respective hub methods –

function onNodeCreated(s, e)
{
    var hubId = $.connection.hub.id;
    e.node.id = hubId + s.getItems().length;

    var r = e.node.bounds;
    var model =
    {
        id: e.node.id,
        x: r.x,
        y: r.y,
        width: r.width,
        height: r.height
    };
    
    diagramHub.server.nodeCreated(model);
}

function onNodeModified(s, e)
{
    var r = e.node.bounds;
    var model =
    {
        id: e.node.id,
        x: r.x,
        y: r.y,
        width: r.width,
        height: r.height
    };
    diagramHub.server.nodeModified(model);
}

function onNodeTextEdited(s, e)
{
    var model =
    {
        id: e.node.id,
        text: e.getNewText()
    };
    diagramHub.server.nodeTextEdited(model);
}

Handle notifications sent from server to clients by updating the diagram from received model objects –

$(function ()
{
    diagramHub = $.connection.diagramHub;
    diagramHub.client.nodeCreated = function (model)
    {
        var node = diagram.factory.createShapeNode(
            model.x, model.y, model.width, model.height);
        node.id = model.id;
    };
    diagramHub.client.nodeModified = function (model)
    {
        var node = findNode(model.id);
        node.setBounds(
            new MindFusion.Drawing.Rect(
                model.x, model.y, model.width, model.height),
            true);
    };
    diagramHub.client.nodeTextEdited = function (model)
    {
        var node = findNode(model.id);
        node.setText(model.text);
    };
    $.connection.hub.start();
});

Finally add these helper functions for finding items and storing a global diagram reference –

function onDiagramLoaded(s, e)
{
    diagram = s;
}

function findNode(id)
{
    for (var i = 0; i < diagram.nodes.length; i++)
    {
        var node = diagram.nodes[i];
        if (id == node.id)
            return node;
    }
    return null;
}

function findLink(id)
{
    for (var i = 0; i < diagram.links.length; i++)
    {
        var link = diagram.links[i];
        if (id == link.id)
            return link;
    }
    return null;
}

Start several copies of the application in separate browser instances on your system (or even on different machines if you publish it on IIS or Azure). Now start drawing nodes, moving them or editing their text – changes done on the diagram in one browser will be immediately reflected in all other browsers connected to the hub. However we aren’t yet synchronizing link operations; lets fix that –

public class LinkModel
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("originId")]
    public string OriginId { get; set; }

    [JsonProperty("destinationId")]
    public string DestinationId { get; set; }

    [JsonProperty("text")]
    public string Text { get; set; }
}

Add following hub methods in server class –

public void LinkCreated(LinkModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).linkCreated(clientModel);
}
public void LinkModified(LinkModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).linkModified(clientModel);
}
public void LinkTextEdited(LinkModel clientModel)
{
    Clients.AllExcept(Context.ConnectionId).linkTextEdited(clientModel);
}

Call them from respective JavaScript handlers of diagram link events –

function onLinkCreated(s, e)
{
    var hubId = $.connection.hub.id;
    e.link.id = hubId + s.getItems().length;

    var model =
    {
        id: e.link.id,
        originId: e.link.getOrigin().id,
        destinationId: e.link.getDestination().id,
    };
    
    diagramHub.server.linkCreated(model);
}

function onLinkModified(s, e)
{
    var hubId = $.connection.hub.id;
    var model =
    {
        id: e.link.id,
        originId: e.link.getOrigin().id,
        destinationId: e.link.getDestination().id,
    };
    diagramHub.server.linkModified(model);
}

function onLinkTextEdited(s, e)
{
    var model =
    {
        id: e.link.id,
        text: e.getNewText()
    };
    diagramHub.server.linkTextEdited(model);
}

Handle link-related client notifications by creating or modifying links –

diagramHub.client.linkCreated = function (model)
{
    var link = diagram.factory.createDiagramLink(
        findNode(model.originId), findNode(model.destinationId));
    link.id = model.id;
};
diagramHub.client.linkModified = function (model)
{
    var link = findLink(model.id);
    link.setOrigin(findNode(model.originId));
    link.setDestination(findNode(model.destinationId));
};
diagramHub.client.linkTextEdited = function (model)
{
    var link = findLink(model.id);
    link.setText(model.text);
};

Now the application will also synchronize link operations across all connected clients. Here’s a small diagram synchronized between three different browsers –
collaborative mind map

The sample above uses MindFusion’s ASP.NET MVC API. Code for other frameworks will look similar as MindFusion maintains same diagramming model for multiple platforms. You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!

Class inheritance diagram in JavaScript

In this post we will show how to use the JavaScript diagram library to generate a class inheritance diagram. The complete example is available here:

InheritanceDiagram.zip

and a live version here:

http://mindfusion.eu/demos/jsdiagram/Inheritance.html

Let’s start by creating shortcuts to some classes from the diagram model:

var Diagram = MindFusion.Diagramming.Diagram;

var DiagramItem = MindFusion.Diagramming.DiagramItem;
var DiagramLink = MindFusion.Diagramming.DiagramLink;
var DiagramNode = MindFusion.Diagramming.DiagramNode;
var ShapeNode = MindFusion.Diagramming.ShapeNode;
var TableNode = MindFusion.Diagramming.TableNode;
var ContainerNode = MindFusion.Diagramming.ContainerNode;
var FreeFormNode = MindFusion.Diagramming.FreeFormNode;
var SvgNode = MindFusion.Diagramming.SvgNode;

var ScrollBar = MindFusion.Diagramming.ScrollBar;
var Rect = MindFusion.Drawing.Rect;
var Font = MindFusion.Drawing.Font;
var TreeLayout = MindFusion.Graphs.TreeLayout;

Next, create a function that takes a Diagram instance and a list of class names as parameters. It will create a TableNode for each class. Each property of the class prototype is listed in a TableNode cell. If the getBaseType function detects a class inherits another one from the list, we’ll create a link between their nodes. Finally, the diagram is arranged using the TreeLayout algorithm.

function createClassDiagram(diagram, classes)
{
    var classConstructors = [];

    // create a table node for each class
    for (var i = 0; i < classes.length; i++)
    {
        var className = classes[i];
        var node = diagram.getFactory().createTableNode(20, 20, 42, 42);
        node.redimTable(1, 0);
        node.setText(className);
        node.setBrush("white");
        node.setCaptionBackBrush("lightgray");
        node.setCaptionFont(
            new Font("sans-serif", 3, true /*bold*/, true /*italic*/));
        node.setScrollable(true);

        var ctor = eval(className);
        for (var property in ctor.prototype)
        {
            node.addRow();
            node.getCell(0, node.rows.length - 1).setText(property);
        }
        classConstructors.push(ctor);
        ctor.classNode = node;
    }
	
    // create a diagram link for each prototype inheritance
    classConstructors.forEach(function(ctor)
    {
        var base = getBaseType(ctor);
        if (base && base.classNode)
        {
            var link = diagram.factory.createDiagramLink(
                base.classNode,
                ctor.classNode);
            link.setHeadShape(null);
            link.setBaseShape("Triangle");
            link.setBaseShapeSize(3);
        }
    });

    // arrange as a tree
    var treeLayout = new TreeLayout();
    treeLayout.linkType = MindFusion.Graphs.TreeLayoutLinkType.Cascading;
    diagram.arrange(treeLayout);
}

The getBaseType implementation checks if a class was registered as a base for the argument using MindFusion.registerClass method or the common prototype inheritance pattern.

function getBaseType(ctor)
{
    // if class registered using MindFusion.registerClass
    if (ctor.__baseType)
        return ctor.__baseType;

    // if  prototypical inheritance with Child.prototype = new Parent()
    if (ctor.prototype && ctor.prototype.constructor != ctor)
        return ctor.prototype.constructor;
	
    return null;
}

The ready handler creates a Diagram instance binding it to a #diagram canvas element. It then calls createClassDiagram with a list of DiagramItem -derived classes as argument:

$(document).ready(function ()
{
    TableNode.prototype.useScrollBars = true;
    ScrollBar.prototype.background = "Lavender";
    ScrollBar.prototype.foreground = "DarkGray";

    // create a Diagram component that wraps the "diagram" canvas
    var diagram = Diagram.create($("#diagram")[0]);

    createClassDiagram(diagram,
    [
        "DiagramItem",
        "DiagramLink",
        "DiagramNode",
        "ShapeNode",
        "TableNode",
        "ContainerNode",
        "FreeFormNode",
        "SvgNode"
    ]);
});

If you run the sample now, you should see this nice visualization of MindFusion classes 🙂

JavaScript class inheritance diagram

For more information on MindFusion JavaScript diagram library, see its help reference and overview page.

Enjoy!