Nested DataRanges in MindFusion Report Application

In this tutorial we are going to build a report with nested data ranges. In our case, we are going to retrieve all categories from the sample Norhtwind database. For each category we will get all products in it, which is the nested data range.

I. Preparing the Project

We create a new WinForms project and add a new DataSet from the Nortwind database. We choose all fields from the Categories and Products tables. Then we add a new item from the menu “Project -> Add -> New Item -> MindFusion Report” from the dialog that appears.

Adding the MindFusion Report item.

Adding the MindFusion Report item.

Drag and drop the nwindDataSet, CategoriesTableAdapter and ProductsTableAdapter on the report form. Finally, in the code behind fill the two adapters:

 productsTableAdapter1.Fill(nwindDataSet1.Products);
 categoriesTableAdapter1.Fill(nwindDataSet1.Categories); 

II. The Categories DataRange

We create the first data range by right clicking on the newly created report. There we see “Create Data Range from Data Source”. We choose the categories table and two fields – CategoryName and Picture. When the data range is generated we resize the picture to make it bigger.

Create DataRange context menu.

Create DataRange context menu.

III. Running the Report

We would use MindFusion ReportViewer to preview what we’ve done so far. We drag it from the Toolbox and place it on the form, which shows when the application runs. We compile the project and see that the Report1 class that we’ve created appears in the Toolbox, under the Data tab. This means we can create instances of our report just by drag and drop. We drag the Report1 icon and drop it on Form1. We have a report11 instance, which we assign to the Report property of the ReportView in the property grid.

Dragging the Report1 item.

Dragging the Report1 item.

Finally, we run the report:

report11.Run();

IV. The Nested DataRange

It’s time to create the second DataRange. We right-click the Report1 form and choose again “Create DataRange from Data Source.” This time we choose the Products table and we choose ProductName, UnitsInStock and UnitPrice fields. This time we check the “Generate Header” checkbox at the bottom. The second DataRange is ready. Nesting it is very easy. We resize the first DataRange to make it wider and just drag and drop the second DataRange in it. What is important is to set the MasterDetailRelation property. It must be the name of the relation between the two tables that provide data for the two DataRange-s. We can see it by clicking on the nwindDataSet -> “Edit DataSet with Designer. There we click on the relation between or two tables and see it is called “CategoriesProducts”. We place this name as a value to the MasterDetailRelation property.

Master detail relationship between the Products and Categories table.

Master detail relationship between the Products and Categories table.

V. Run the report

We run the report and see that everything is in place: the categories are listed with their picture, each category lists all its products.

VI. Style Adjustments

Finally let’s add some appearance optimizations that will make the report look better and thus be easier to read. First, we make the background of the Category label darker.

Then we make the product lines with alternating colors. This is done in the property grid for the dataRange2 object -> AlternatingBackground property.

We add a light gray border to the first data range with the Border property editor and we add a bottom margin of 30 mm. Here is the final look of the report:

Nested DataRange-s: the final report.

Nested DataRange-s: the final report.

You can download the sample from this link:

Download Nested DataRanges MindFusion Reporting Sample

More about MindFusion Reporting for WinForms component can be found here.

Create a musical score writer using MindFusion diagram component.

In this example we’ll show how to use various features of MindFusion.Diagramming API to create a musical score editor:

Custom node types
We’ll create a StaffNode class to represent the staff, and NoteNode class to represent a musical note.

Grouping
NoteNodes will be attached to the StaffNode they were dropped onto (or nearby). If users move the staff around, the notes from the group will follow it.

Custom drawing logic
We’ll show how to draw custom graphics by overriding DrawLocal method of base DiagramNode class.

Using SVG images
We’ll show how to load an SVG image (for the G clef) and draw it as part of staff graphics.

NodeListView control
NodeListView contains prototypical node instances whose clones are added to the diagram using drag-and-drop operations. We’ll add a staff and several notes to the list to let users drag them to the score diagram.

The completed sample project can be downloaded from this link:
ScoreWriter.zip

Let’s start by defining StaffNode class to draw staves in the score diagram, and implement its Draw methods to draw five lines:

public class StaffNode : DiagramNode
{
	public StaffNode()
	{
		var rect = Bounds;
		rect.Width = 200;
		SetBounds(rect, false, false);

		// disable vertical resize
		EnabledHandles =
			AdjustmentHandles.ResizeMiddleLeft |
			AdjustmentHandles.Move |
			AdjustmentHandles.ResizeMiddleRight;
	}

	public StaffNode(StaffNode prototype) : base(prototype)
	{
	}

	public override void DrawLocal(IGraphics graphics, RenderOptions options)
	{
		base.DrawLocal(graphics, options);

		for (int i = 0; i < 5; i++)
		{
			float y = i * Bounds.Height / 4;
			using (var pen = EffectivePen.CreateGdiPen())
				graphics.DrawLine(pen, 0, y, Bounds.Width, y);
		}
	}

	public override void DrawShadowLocal(IGraphics graphics, RenderOptions options)
	{
	}
}

Next, load an SVG drawing representing G clef and draw it at appropriate position. We’ll also override GetRepaintRect method to accommodate for parts of the clef that are drawn outside the staff’s boundaries:

static SvgContent gClef;

static StaffNode()
{
	gClef = new SvgContent();
	gClef.Parse("GClef.svg");
}

public override void DrawLocal(IGraphics graphics, RenderOptions options)
{
	// ...

	var rect = GetLocalBounds();
	rect.Inflate(0, 8);
	rect.X = 2;
	rect.Width = 14;
	gClef.Draw(graphics, rect);
}

public override RectangleF GetRepaintRect(bool includeConnected)
{
	var rect = base.GetRepaintRect(includeConnected);
	rect.Inflate(0, 8);
	return rect;
}

Create an initial StaffNode instance from Form.Load event:

var initialStaff = new StaffNode();
initialStaff.Move(10, 10);
diagram.Nodes.Add(initialStaff);

If you run the project now, you should see the following diagram:
score writer diagram in c#

Next, define the Duration enumeration and NoteNode class to represent musical notes of various durations:

enum Duration
{
	Whole,
	Half,
	Quarter,
	Eighth,
	Sixteenth
}

class NoteNode : DiagramNode
{
	public NoteNode()
	{
		Bounds = new RectangleF(0, 0, 6, 6);
		Duration = Duration.Whole;
	}

	public NoteNode(Duration duration)
	{
		Bounds = new RectangleF(0, 0, 6, 6);
		Duration = duration;
	}

	public Duration Duration { get; set; }

	int position = 0;
}

Implement NoteNode.Draw methods as follows:

public override void DrawLocal(IGraphics graphics, RenderOptions options)
{
	base.DrawLocal(graphics, options);

	var cx = Bounds.Width / 2;
	var cy = Bounds.Height / 2;

	var gs = graphics.Save();
	graphics.TranslateTransform(cx, cy);
	graphics.RotateTransform(-10);
	graphics.TranslateTransform(-cx, -cy);

	var bounds = GetLocalBounds();
	bounds.Inflate(0, -bounds.Width / 10);
	var path = new GraphicsPath();
	path.AddEllipse(bounds);

	if (Duration == Duration.Whole || Duration == Duration.Half)
	{
		bounds.Inflate(-bounds.Width / 8, -bounds.Width / 6);
		path.AddEllipse(bounds);
	}
	graphics.FillPath(Brushes.Black, path);

	graphics.Restore(gs);

	if (position < -1 || position > 8)
	{
		// draw ledger lines if above or below staff
		var pen = EffectivePen.CreateGdiPen();
		var staff = (StaffNode)MasterGroup.MainItem;
		var yoff = staff.Bounds.Y - Bounds.Y;
		int i1 = position < -1 ? position : 9;
		int i2 = position < -1 ? -2 : position;
		for (int i = i1; i <= i2; i++)
		{
			if (i % 2 != 0)
				continue;
			var y = yoff + i * staff.Bounds.Height / 8;
			graphics.DrawLine(pen, -2, y, Bounds.Width + 2, y);
		}
		pen.Dispose();
	}

	if (Duration != Duration.Whole)
	{
		// draw stem
		float x = Bounds.Width;
		float y = Bounds.Height / 2;
		var pen = new System.Drawing.Pen(Color.Black, 0.5f);
		graphics.DrawLine(pen,
				            x - pen.Width / 2, y,
				            x - pen.Width / 2, y - Bounds.Height * 2);
		pen.Dispose();
	}

	if (Duration == Duration.Eighth || Duration == Duration.Sixteenth)
	{
		DrawFlag(graphics,
				    bounds.Width,
				    bounds.Height / 2 - bounds.Height * 2,
				    bounds.Width + 1,
				    bounds.Height);
	}

	if (Duration == Duration.Sixteenth)
	{
		DrawFlag(graphics,
				    bounds.Width,
				    bounds.Height - bounds.Height * 2,
				    bounds.Width + 1,
				    bounds.Height);
	}
}

void DrawFlag(IGraphics graphics, float x, float y, float w, float h)
{
	float sh = h / 2;
	float sw = w / 3;

	var pen = new System.Drawing.Pen(Color.Black, 0.5f);
	x -= pen.Width / 2;
	graphics.DrawBezier(pen,
			            x, y,
			            x, y + sh,
			            x + sw * 1.2f, y + 2 * sh,
			            x + sw, y + 3 * sh);
	pen.Dispose();
}

public override void DrawShadowLocal(IGraphics graphics, RenderOptions options)
{
}

public override RectangleF GetRepaintRect(bool includeConnected)
{
	var r = Bounds;
	r.Y -= r.Height * 2;
	r.Height *= 3;
	r.Width *= 2;
	return r;
}

Now, drag a NodeListView to the form and populate it from Load handler:

nodeListView.AddNode(new StaffNode());

nodeListView.DefaultNodeSize = new SizeF(6, 6);
nodeListView.AddNode(new NoteNode(Duration.Whole));
nodeListView.AddNode(new NoteNode(Duration.Half));
nodeListView.AddNode(new NoteNode(Duration.Quarter));
nodeListView.AddNode(new NoteNode(Duration.Eighth));
nodeListView.AddNode(new NoteNode(Duration.Sixteenth));

Drag and drop will not work just yet. First, we must enable the DiagramView.AllowDrop property to accept drag-and-drop events. Next, the custom classes must implement a copy constructor and serialization methods to be able to instantiate them through OLE drag events:

public NoteNode(NoteNode prototype) : base(prototype)
{
	Duration = prototype.Duration;
}

protected override void SaveTo(System.IO.BinaryWriter writer, PersistContext context)
{
	base.SaveTo(writer, context);
	context.Writer.Write((int)Duration);
}

protected override void LoadFrom(System.IO.BinaryReader reader, PersistContext context)
{
	base.LoadFrom(reader, context);
	Duration = (Duration)context.Reader.ReadInt32();
}

As a final touch for this example, let’s implement aligning notes to staves’ lines and spaces. First lets declare a helper method that returns the nearest StaffNode at specified location in diagram:

static class DiagramExtensions
{
	static public StaffNode NearestStaff(this Diagram diagram, PointF point)
	{
		var staves = diagram.Nodes.OfType();

		StaffNode nearest = null;
		float minDist = float.MaxValue;

		foreach (var staff in staves)
		{
			if (staff.ContainsPoint(point))
				return staff;

			var borderPoint = staff.GetNearestBorderPoint(point);
			var dist = Utilities.Distance(borderPoint, point);
			if (dist < minDist)
			{
				minDist = dist;
				nearest = staff;
			}
		}

		return minDist < 20 ? nearest : null;
	}
}

Next, implement StaffNode.Align method that aligns its argument to a line or space in the staff:

public PointF Align(PointF point, out int position)
{
	// align to pitch line/space

	float h = Bounds.Height / 8;
	float offset = point.Y - Bounds.Y;
	position = (int)Math.Round(offset / h);
	offset = (float)Math.Round(offset / h) * h;
	point.Y = Bounds.Y + offset;
	return point;
}

Add NoteNode.AlignToStaff method that will find nearest StaffNode and align the note’s position to the staff.

public StaffNode AlignToStaff()
{
	position = 0;

	var staff = Parent.NearestStaff(GetCenter());
	if (staff == null)
		return null;

	var alignedPoint = staff.Align(GetCenter(), out position);
	alignedPoint.X -= Bounds.Width / 2;
	alignedPoint.Y -= Bounds.Height / 2;
	Move(alignedPoint.X, alignedPoint.Y);

	return staff;
}

We can align notes after drag-and-drop from NodeListView by handling diagram’s NodeCreated event. We’ll use the same handler to attach notes to that staff, so that if users move a StaffNode, its attached NoteNodes will follow.

private void OnNodeCreated(object sender, NodeEventArgs e)
{
	var note = e.Node as NoteNode;
	if (note != null)
	{
		var staff = note.AlignToStaff();
		if (staff != null)
			note.AttachTo(staff, AttachToNode.TopLeft);

		note.HandlesStyle = HandlesStyle.MoveOnly;
	}
}

Finally, override NoteNode.CompleteModify to align notes after user moves them to a different position on the staff or to another staff in the score:

protected override void CompleteModify(PointF end, InteractionState ist)
{
	base.CompleteModify(end, ist);

	var staff = AlignToStaff();
	if (staff != null)
		AttachTo(staff, AttachToNode.TopLeft);
	else
	{
		Detach();
	}
}

Let’s run the project and compose some music 🙂
.net diagram control

A fully-featured scorewriter software would also allow for drawing rest, sharp and flat symbols, C and F clefs, and some other musical notation features, but these are left as exercise to the reader 😉

The code above uses MindFusion’s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.

You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!

Layout Management with the WinForms Dock Control

In this post we will show you how to build a sample application with customizable layout based on the WinForms Dock control, which is part of MindFusion WinForms pack. You can find further details about the control at its web page.

For the purpose of the demonstration we’ve chosen an entertaining topic – a cooking recipes electronic book. Our book so far has only three recipes, all for sweets. Lets start by looking at the

I. Architecture of the sample.

The sample has two custom classes – Ingredient and Recipe. Each Ingredient has quantity, name and an image, which illustrates it. The Recipe class holds a strongly typed list with Ingredient objects, the title for each recipe, an image for the recipe, an icon for the recipe and preparation instructions.

The application consists of the dock control, which contains four DockItem-s : for the recipes, for their images, for their ingredients and with the preparation instructions. Any dock item can be dragged, dropped, aligned, minimized, hidden etc.

The API of the cook book application

The API of the cook book application

II. The Dock Control.

We create an empty WinForms project and add the Dock control from the toolbox. The dock control fills the entire client area:

this.dockControl1.Dock = System.Windows.Forms.DockStyle.Fill;         
this.dockControl1.Location = new System.Drawing.Point(0, 0);           
this.dockControl1.Size = new System.Drawing.Size(784, 562);

These properties are set in the Property grid.

Each dock item is created with a title and id, which helps us identify it.

titleItem = new DockItem() { Header = "Recipe Name", Id = "Name" };
titleItem.DockStyle = DockStyle.Left;      
titleItem.Content = GetContent("Name");

The DockStyle property is responsible for the initial layout of the dock item. You can choose among various dock styles – fill, bottom, top and more.

The GetContent method is very important. It prepares the controls that will be placed into each dock item and returns the appropriate one according to the parameter.

Once the dock item is created, let’s not forget to add it:

dockControl1.Items.Add(titleItem);

III. Creating the Content.

Let’s see how the content for a dock item is created. Let’s take the grid with the ingredients. It is identified with the “Ingredients” id:

private Control GetContent(string contentType)
{
  Control control = null;
  ....
  else if (contentType == "Ingredients")
         {                
             grid = new DataGridView();
             grid.DefaultCellStyle.BackColor = Color.FromArgb(247, 226, 189);
             grid.Dock = DockStyle.Fill;
             grid.MultiSelect = false;
             grid.DataSource = selectedRecipe.Ingredients;
             grid.RowTemplate.MinimumHeight = 35;
            
             grid.RowHeadersWidthSizeMode =
            DataGridViewRowHeadersWidthSizeMode.DisableResizing;
             grid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
             grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
             grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
             grid.BackgroundColor = Color.FromArgb(253, 244, 247);

             control = grid; 
         }
...
return control;

The idea is clear: you create the control, which will be rendered in the dock item and assign it to DockItem.Content. Here we make different customization for the grid – we change the color of table rows, adjust the height, turn off multiple row select and more.

IV. Changing the Content.

We change the content by handling the SelectedIndexChanged event for the ListView, which lists our recipes. When the user selects a new list item, we extract its Recipe and change the content of the other DockItem-s:

selectedRecipe = recipes[recipesListView.SelectedIndices[0]];
tb.Text = selectedRecipe.Preparation;
grid.DataSource = selectedRecipe.Ingredients;
pictureBox.Image = Image.FromFile(selectedRecipe.ImageUrl);

Here is the final output of the sample application:

MindFusion WinForms Dock COntrol

A Sample electronic cook book based on MindFusion WinForms Dock Control

You can download the sample directly from this link:

MindFusion WinForms Layout Control Sample: Electronic Cook Book in C#

MindFusion WinForms Layout Control Sample: Electronic Cook Book in VB.NET

Enjoy the fast, easy and straight-forwarded manner in which you can create a WinForms application with a flexible layout and multiple panels.

Combine layout algorithms

Apply FractalLayout and SpringLayout to generate a tag cloud

In a series of posts we’ll explore ways to combine graph layout algorithms for various purposes, such as improving layout speed or achieving specific layout constraints.

In this topic we’ll show how to create a tag cloud using FractalLayout and SpringLayout algorithms from MindFusion diagramming API. You can download the complete project here:

TagCloud.zip

The sample code will show several features of the Diagram control:

  • FractalLayout
  • SpringLayout
  • custom node placement
  • text-only nodes

We assume words frequencies are already counted and listed as “word: frequency” entries in a sorted file. The example uses tags extracted from the contents of Wikipedia’s Tag cloud page. Let’s start by parsing the file and creating a node for each word. We’ll assign the word frequency to the Weight property of nodes for future reference. Weight is also used by some layout algorithms (such as TreeMapLayout for creating tree maps) that could visually represent word frequencies as well:

RectangleF defaultBounds = new RectangleF(0, 0, 10, 20);
ShapeNode root;
		
private void MainForm_Load(object sender, EventArgs e)
{
	// read the tags file
	var reader = new StreamReader("words.txt");
	string line;
	while ((line = reader.ReadLine()) != null)
	{
		// each line contains "word: frequency" entries
		var parts = line.Split(new[] { ':' });
		var word = parts[0];
		var frequency = int.Parse(parts[1]);

		// create a diagram node for each word in the file
		var node = diagram.Factory.CreateShapeNode(defaultBounds);
		node.Weight = frequency;
		node.Text = word;

		// set font size corresponding to frequency
		node.Font = new Font(
			"Arial",
			8 + (float)Math.Log(node.Weight, 1.15),
			GraphicsUnit.Point);

		// resize the node to fit text
		var size = TextRenderer.MeasureText(node.Text, node.Font);
		node.Resize(
			2 + (float)MeasureUnit.Pixel.Convert(size.Width, diagram.MeasureUnit, null),
			2 + (float)MeasureUnit.Pixel.Convert(size.Height, diagram.MeasureUnit, null));

		// show only text, hide geometry
		node.Transparent = true;
	}
}

Next, let’s build a tree that will distribute largest nodes roughly uniformly when arranged by FractalLayout, where larger parent nodes are circled by their smaller child nodes:

while ((line = reader.ReadLine()) != null)
{
	// ...
	if (diagram.Nodes.Count == 1)
	{
		// save reference to the first node
		root = node;
	}
	else
	{
		// build a tree where each node has up to six children
		diagram.Factory.CreateDiagramLink(
			diagram.Nodes[diagram.Nodes.Count / 6],
			node);
	}
}

// use FractalLayout for initial placement. if the file entries are sorted, each circular
// branch will contain larger parent node centered between its smaller children
new FractalLayout().Arrange(diagram);

If you run the application now, you should see the following layout:

a tree arranged using fractal layout

FractalLayout allocates some space for links, and we’ll reclaim it by deleting the links and compressing the initial layout of nodes:

// remove the links
while (diagram.Links.Count > 0)
	diagram.Links.RemoveAt(diagram.Links.Count - 1);

// pull all nodes towards the root to eliminate empty space that was occupied by links
var center = root.GetCenter();
foreach (var node in diagram.Nodes.Where(n => n != root))
{
	var relativePos = new Vector(center, node.GetCenter());
	var newPos = center + relativePos / 10;
	node.Move(
		newPos.X - node.Bounds.Width / 2,
		newPos.Y - node.Bounds.Height / 2);
}

Now apply SpringLayout to make distances between closely placed nodes more uniform, running only a few iterations to complete faster:

// run SpringLayout to distribute nodes more uniformly
var sl = new SpringLayout();
sl.Randomize = false;
sl.SplitGraph = false;
sl.NodeDistance = 3;
sl.IterationCount = 40;
sl.Arrange(diagram);

Run the following method to remove any overlaps remaining after SpringLayout. RemoveOverlaps works by starting from specified node and offsetting any nodes that overlap it, continuing by spiraling away while processing other nodes. This method could also be useful in an interactive application if you want to disperse overlapping nodes introduced by the user when they move a node:

void RemoveOverlaps(DiagramNode modifiedNode, float minDist)
{
	var queue = new Queue();
	queue.Enqueue(modifiedNode);

	while (queue.Count > 0)
	{
		var node = queue.Dequeue();
		var nodeCenter = node.GetCenter();
		var overlaps = FindOverlaps(node, minDist);
		foreach (var overlap in overlaps)
		{
			var ovrCenter = overlap.GetCenter();
			var ovrBounds = overlap.Bounds;
			var dx = ovrCenter.X - nodeCenter.X;
			var dy = ovrCenter.Y - nodeCenter.Y;
			if (Math.Abs(dx) > Math.Abs(dy))
			{
				// offset horizontally
				if (dx < 0)
					ovrBounds.X = node.Bounds.Left - ovrBounds.Width - minDist;
				else
					ovrBounds.X = node.Bounds.Right + minDist;
			}
			else
			{
				// offset vertically
				if (dy < 0)
					ovrBounds.Y = node.Bounds.Top - ovrBounds.Height - minDist;
				else
					ovrBounds.Y = node.Bounds.Bottom + minDist;
			}

			// shifting the node might introduce new overlaps, continue processing
			overlap.Bounds = ovrBounds;
			queue.Enqueue(overlap);
		}
	}
}

List FindOverlaps(DiagramNode modifiedNode, float minDist)
{
	var bounds = modifiedNode.Bounds;
	bounds.Inflate(minDist - 1, minDist - 1);

	var overlaps = new List();
	foreach (var node in diagram.Nodes)
	{
		if (modifiedNode == node)
			continue;
		if (bounds.IntersectsWith(node.Bounds))
			overlaps.Add(node);
	}
	return overlaps;
} 

Finally run a few more iterations of SpringLayout to equalize distances again and zoom the diagram to show the whole tag cloud:

// remove any remaining overlaps
RemoveOverlaps(root, 0.1f);
sl.Arrange(diagram);

// show everything inside view
diagram.ResizeToFitItems(5);
diagramView.ZoomToFit();

If you run the application now, you should see the following image:

tag cloud generated using MindFusion diagram control

The code above uses MindFusion’s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.

You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!

Create a Dialogue Editor using MindFusion Diagram

In this post we’ll demonstrate how to create a graphical interface for editing dialogues using MindFusion diagramming API. The sample could be used as a module in different kinds of applications, such as software for creating and conducting surveys, editing interactive voice response systems, designing NPC dialogues in game development tools. You can download the complete project here:

DialogueEditor.zip

The sample code will show several features of Diagram control:

  • TableNode API
  • in-place edit
  • work with groups
  • graph traversal

Questions or IVR messages and their possible answers / responses will be displayed respectively in caption area and cells of TableNode objects. Helper nodes at table bottom will let users add or remove rows. DiagramLink objects connecting table rows to other tables will define the dialogue flow, i.e. what next question / message to display after user selects option from current message.

Let’s start by creating a new .NET Windows Forms project. If you have installed MindFusion diagram control and selected toolbox integration from setup wizard screen, you should now see a Diagram component and DiagramView control in the toolbox. If they are not available, right click and select Choose Items, navigate to installation folder and select the mindfusion.diagramming and mindfusion.diagramming.winforms assemblies. Now drag a Diagram component to the form, and set its name to “diagram”. This automatically adds a diagramming.dll reference to the project. Drag a DiagramView (which add diagramming.winforms reference) and name it diagramView. Set its Diagram property to “diagram”, selecting it from the drop-down in property grid.

We will allow creation only of tables and links, so let’s set DiagramView.Behavior property to LinkTables. Now if users draw on the diagram canvas, the component will create TableNode if the mouse pointer is over unoccupied part of the diagram, or a DiagramLink if the mouse points a table. If we wanted to support more type of nodes, we could add their prototypes to a NodeListView instance and let users create new instances via drag-and-drop.

Add the following fields and constructor code to set up appearance and behavior of diagram elements.

public MainForm()
{
	InitializeComponent();

	// set up initial table appearance
	diagram.TableNodeStyle.Brush = new MindFusion.Drawing.SolidBrush(Color.LightGray);
	diagram.TableRowCount = 1;
	diagram.TableColumnCount = 1;
	diagram.TableCaption = "question";
	diagram.NodeEffects.Add(new GlassEffect());

	// highlight a row when clicked
	diagram.AutoHighlightRows = true;

	// find link routes automatically
	diagram.RouteLinks = true;

	// allow edit texts by double click
	diagramView.AllowInplaceEdit = true;

	// row anchor points
	rightOutput = new AnchorPattern("Right");
	rightOutput.Points.Add(new AnchorPoint(100, 50, false, true));

	// table anchor points
	input = new AnchorPattern("Input");
	input.Points.Add(new AnchorPoint(50, 0, true, false));
	input.Points.Add(new AnchorPoint(50, 100, true, true));
}

AnchorPattern rightOutput;
AnchorPattern input;

Select the diagram component in form editor and double click its NodeCreated event to add event handler. NodeCreated is raised when the user draws a new node. Add following code to associate a question / IVR message with the table, and initialize some default texts. The BeginEdit method automatically opens in-place editor to let user edit caption text immediately after drawing. We also create + and – ShapeNodes that will act as button widgets attached to the table’s bottom-right corner.

private void OnNodeCreated(object sender, MindFusion.Diagramming.NodeEventArgs e)
{
	var table = e.Node as TableNode;
	if (table != null)
	{
		int tableId = (1 + diagram.Nodes.Count / 3);
		table.Id = tableId;
		table.RowAnchorPattern = rightOutput;
		table.AnchorPattern = input;
		table.ConnectionStyle = TableConnectionStyle.Both;
		table[0, 0].Text = "option 1";
		table.Caption = "question " + tableId;
		diagramView.BeginEdit(table);

		// create + button for adding new rows
		var r = table.Bounds;
		var p = new PointF(r.Right - 14, r.Bottom - 8);
		var s = new SizeF(6, 6);
		var plus = diagram.Factory.CreateShapeNode(p, s, Shapes.Cross);
		plus.Brush = new MindFusion.Drawing.SolidBrush(Color.Green);
		plus.AttachTo(table, AttachToNode.BottomRight);
		plus.Tag = "+";

		// create - button for deleting selected row
		p.X += 7;
		p.Y += 2;
		s.Height -= 4;
		var minus = diagram.Factory.CreateShapeNode(p, s, Shapes.Rectangle);
		minus.Brush = new MindFusion.Drawing.SolidBrush(Color.Red);
		minus.AttachTo(table, AttachToNode.BottomRight);
		minus.Tag = "-";

		plus.Locked = minus.Locked = true;
		table.SubordinateGroup.AutoDeleteItems = true;
	}
}

Now add a NodeClicked handler that adds or deletes rows. If the + button is clicked, the code inserts a new row before current highlighted row. If the – button is clicked, the handler deletes current highlighted row.

private void OnNodeClicked(object sender, NodeEventArgs e)
{
	if ("+".Equals(e.Node.Tag))
	{
		var table = (TableNode)e.Node.MasterGroup.MainItem;
		if (table.HighlightedRow == -1)
			table.RowCount++;
		else
			table.InsertRow(table.HighlightedRow);
	}

	if ("-".Equals(e.Node.Tag))
	{
		var table = (TableNode)e.Node.MasterGroup.MainItem;
		if (table.HighlightedRow != -1)
			table.DeleteRow(table.HighlightedRow);
	}
}

Finally, lets create export function that will traverse the dialogue graph and export it to a custom-format XML file, which could then be passed on to a system processing the dialogues, such as IVR service.

private void btnExport_Click(object sender, EventArgs e)
{
	var fileDlg = new SaveFileDialog();
	if (fileDlg.ShowDialog() == DialogResult.OK)
	{
		var doc = new XmlDocument();
		var root = doc.CreateElement("Dialogue");
		doc.AppendChild(root);

		foreach (var node in diagram.Nodes)
		{
			var table = node as TableNode;
			if (table != null)
			{
				var questionElement = doc.CreateElement("Question");
				root.AppendChild(questionElement);

				int id = (int)table.Id;
				questionElement.SetAttribute("Id", table.Id.ToString());
				questionElement.SetAttribute("Text", table.Caption);

				for (int r = 0; r < table.Rows.Count; r++)
				{
					string answer = table[0, r].Text;
					var answerElement = doc.CreateElement("Answer");
					questionElement.AppendChild(answerElement);
					answerElement.SetAttribute("Text", answer);
					if (table.Rows[r].OutgoingLinks.Count > 0)
					{
						var link = table.Rows[r].OutgoingLinks[0];
						var nextQuestion = (TableNode)link.Destination;
						answerElement.SetAttribute("Text", answer);
						answerElement.SetAttribute("NextId", nextQuestion.Id.ToString());
					}
				}
			}
		}

		doc.Save(fileDlg.FileName);
	}
}

If you run the application now and draw several tables and links, you should see a similar screen:

dialogue editor created with mindfusion diagram control for .NET

The code above uses MindFusion’s .NET API and can be used with Windows Forms, WPF, Silverlight and ASP.NET diagramming components. The Java API for Android and desktop Swing application will look similar, with setter method calls instead of property assignments.

You can download the trial version of any MindFusion.Diagramming component from this page.

Enjoy!