Currently I'm working with finite state machine and I was searching for an easy way of rendering them. I have found the free tool Graphviz and I was wondering how we could use it to render a graph in an ASP.NET environment.
Prerequisites : the tool
For the sake of this example, I will use Graphviz. This is a C++ set of tool that can be used to render many types of graphes. Very interesting and it can be questionnable in command line.
Graphviz works from a "dot file" : a text file that represent the graph vertices and edges.
To be able to generate this dot file, I will use Quickgraph. This is a free .NET library create by Jonathan Halleux. It can be used for creating and manipulating graphs, as for doing calculation on graphes (shortest path, ...). For the rendering, it can be used with MSAGL (Microsoft Automatic Graph Layout - ex Microsoft GLEE). As this tool is NOT free, I will combine the two tool to achieve this work.
Generating the graph
-
Let's create a website and add a new Generic Handler
-
Right-clic on the website
-
Choose Add Item
-
Choose Web and then Generic Handler
-
Name it GraphGeneratorAndRenderer
-
Add a reference to QuickGraph.dll and QuickGraph.Graphviz.dll
-
Modify your handler as follow:
using System.Web;
using QuickGraph;
using TEdge = QuickGraph.TaggedEdge<string, string>;
using TVertex = System.String;
namespace WebApplication1
{
public class GraphGeneratorAndRenderer : IHttpHandler
{
private AdjacencyGraph<TVertex, TEdge> CreateGraph()
{
AdjacencyGraph<TVertex, TEdge> graph = new AdjacencyGraph<TVertex, TEdge>();
TVertex init = "Initial State";
TVertex cancelled = "Cancelled";
TVertex deleted = "Deleted";
TVertex scheduled = "Scheduled";
TVertex expected = "Expected";
graph.AddVertex(init);
graph.AddVertex(cancelled);
graph.AddVertex(deleted);
graph.AddVertex(scheduled);
graph.AddVertex(expected);
graph.AddEdge(new TEdge(init, deleted, "Delete"));
graph.AddEdge(new TEdge(init, scheduled, "Reception of a schedule"));
graph.AddEdge(new TEdge(scheduled, cancelled, "CNL message"));
graph.AddEdge(new TEdge(scheduled, expected, "Reception of Flight plan"));
graph.AddEdge(new TEdge(expected, cancelled, "CNL message"));
graph.AddEdge(new TEdge(scheduled, init, "Reinitialization"));
graph.AddEdge(new TEdge(expected, init, "Reinitialization"));
return graph;
}
public void ProcessRequest(HttpContext context)
{
}
public bool IsReusable
{
get { return false; }
}
}
}
Transforming the graph to a dot file
More precisely we do not want to generate a file, but we want to have the dot structure, so we can use it in the future.
Quickgraph expose a GraphvizAlgorithm that will be responsible of the generation of the graph into a dot structure and it will transfer this to a IDotEngine that can be used for extra processing. By default, Quickgraph expose a FileDotEngine that will generate a file containing the dot structure. As this is not satisfying for us, we'll create our own dot engine.
You should of course create the dot engine in a separate file and DLL but for the sake and quickiness of this example, let's put them all together !
Update your generic handler file to add a new class definition :
using QuickGraph.Graphviz;
using QuickGraph.Graphviz.Dot;
public class BitmapGeneratorDotEngine : IDotEngine
{
#region IDotEngine Members
public string Run(GraphvizImageType imageType, string dot, string outputFileName)
{
return "We should return something here !";
}
#endregion
}
and let's add a method to our generic handler and complete the ProcessRequest method.
public class GraphGeneratorAndRenderer : IHttpHandler
{
private string GenerateBitmap(AdjacencyGraph<TVertex, TEdge> graph)
{
GraphvizAlgorithm<TVertex, TEdge> algo = new GraphvizAlgorithm<TVertex, TEdge>(graph);
string output = algo.Generate(new BitmapGeneratorDotEngine(), "ignored");
return output;
}
public void ProcessRequest(HttpContext context)
{
AdjacencyGraph<TVertex, TEdge> graph = CreateGraph();
string bitmap = GenerateBitmap(graph);
}
}
Let's now check the dot structure:
-
Right-Clic on the website and choose Properties
-
Choose Web
-
For the Start Action, set Specific Page, and give the name of your handler : GraphGeneratorAndRenderer.ashx
-
Add a breakpoint in the Run method of you dot engine and press F5
We so have our finite state machine graph rendered in the following dot structure :
digraph G
{
0 [];
1 [];
2 [];
3 [];
4 [];
0 -> 2 [];
0 -> 3 [];
3 -> 1 [];
3 -> 4 [];
3 -> 0 [];
4 -> 1 [];
4 -> 0 [];
}
Customize the dot structure
It's correct, but we would like to add some labels. Let's simply modify our GraphvizAlgorithm as follows:
private string GenerateBitmap(AdjacencyGraph<TVertex, TEdge> graph)
{
GraphvizAlgorithm<TVertex, TEdge> algo = new GraphvizAlgorithm<TVertex, TEdge>(graph);
algo.FormatEdge += delegate(object sender, FormatEdgeEventArgs<TVertex, TEdge> e)
{
e.EdgeFormatter.Label.Value = e.Edge.Tag;
};
algo.FormatVertex += delegate(object sender, FormatVertexEventArgs<TVertex> e)
{
e.VertexFormatter.Label = e.Vertex;
};
string output = algo.Generate(new BitmapGeneratorDotEngine(), "ignored");
return output;
}
If we debug again, we'll have the following dot structure : much better.
digraph G
{
0 [label="Initial State"];
1 [label="Cancelled"];
2 [label="Deleted"];
3 [label="Scheduled"];
4 [label="Expected"];
0 -> 2 [ label="Delete"];
0 -> 3 [ label="Reception of a schedule"];
3 -> 1 [ label="CNL message"];
3 -> 4 [ label="Reception of Flight plan"];
3 -> 0 [ label="Reinitialization"];
4 -> 1 [ label="CNL message"];
4 -> 0 [ label="Reinitialization"];
}
Convert the dot structure to a bitmap
Let's now go back to the dot engine to improve it in order to generate a bitmap instead. To do so, we'll use the tool dot.exe from Graphviz to do the conversion for us. You will also note that as we do not generate any output file, we do not use the parameter "outputFileName".
public string Run(GraphvizImageType imageType, string dot, string outputFileName)
{
using ( Process process = new Process() )
{
process.StartInfo.FileName = @"C:\Program Files\Graphviz 2.21\bin\dot.exe";
process.StartInfo.Arguments = string.Format("-T{0} -Gcharset=latin1", imageType.ToString());
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
process.StandardInput.Write(dot);
process.StandardInput.Close();
process.WaitForExit(1000);
return process.StandardOutput.ReadToEnd();
}
}
Render the bitmap
What we'll want to do now is to generate an HTML image from this binary bitmap. To do so, we'll use our HTTP Handler as an image source.
-
Add a new webform to the website and call it GraphRenderer.aspx
-
Set this page as the default page
-
Add an image on the aspx page and set the URL to the HTTP Handler
<body>
<form id="form1" runat="server">
<div>
<asp:Image runat="server" ID="imgGraph" ImageUrl="~/GraphGeneratorAndRenderer.ashx" />
</div>
</form>
</body>
We can now update our HttpHandler to render the image:
public void ProcessRequest(HttpContext context)
{
AdjacencyGraph<TVertex, TEdge> graph = CreateGraph();
string binaryBitmap = GenerateBitmap(graph);
Stream stream = context.Response.OutputStream;
using ( StreamWriter sw = new StreamWriter(stream, Encoding.Default) )
sw.Write(binaryBitmap);
}
What to know ?
There are several things to know / drawback about his solution. As you may have seen, we have specified a timeout in the WaitForExit method. Indeed, there is a bug in the Graphviz tool that may freeze when generating some "large" graphs. And the graph we took in example is falling into this category. Giving a timeout gives us the opportunity to get the hand on the tool after 1 second. This value is arbitrary and we consider this is enough for the tool to generate the graph.
So the generation will take 1 second after what we should get the gif, even if we abort the process.
Is there any way to avoid this ugly trick ?
Generating files
The first solution is to use graphviz to generate output file. To do so, we can just add some command parameters : "-o"c:\temp\MyTempFile.gif"".
This solution is less elegant in my view as it involves some folder security (giving ASPNET / IIS_WPG the right of writing) and cleaning (you should clear this temp folder after a while).
Reading the standard output asynchronously
Another solution is to read the standard output asynchronously. For a reason I cannot explain, when doing so, no error / freeze can be encountered.
We have two solutions to do that : we can work high-level using the Process methods to do asynchronous reading : BeginOutputReadLine and OutputDataReceived. This will not work. Indeed in the OutputDataReceived's event handler, when accessing the e.Data property to get a line of data, we won't get any line ending (\r, \n or \r\n). This highly critical as each line of a gif image will be ended either by \r or by \n. Changing one of this delimiter will produce an unreadable image !
So how can we achieve that ? Going a bit lower level and accessing directly the underlying stream to read it asynchronously. And this will work like a charm ! Let's see some code to see how to get it work :
public class BitmapGeneratorDotEngine : IDotEngine
{
private Stream standardOutput;
private MemoryStream memoryStream = new MemoryStream();
private byte[] buffer = new byte[4096];
public string Run(GraphvizImageType imageType, string dot, string outputFileName)
{
using ( Process process = new Process() )
{
process.StartInfo.FileName = @"C:\Program Files\Graphviz 2.21\bin\dot.exe";
process.StartInfo.Arguments = "-Tgif -Gcharset=latin1";
process.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
standardOutput = process.StandardOutput.BaseStream;
standardOutput.BeginRead(buffer, 0, buffer.Length, StandardOutputReadCallback, null);
process.StandardInput.Write(dot);
process.StandardInput.Close();
process.WaitForExit();
return Encoding.Default.GetString(memoryStream.ToArray());
}
}
private void StandardOutputReadCallback(IAsyncResult result)
{
int numberOfBytesRead = standardOutput.EndRead(result);
memoryStream.Write(buffer, 0, numberOfBytesRead);
standardOutput.BeginRead(buffer, 0, buffer.Length, StandardOutputReadCallback, null);
}
}
We will store the standard output base stream and use the BeginRead method on it to start getting the output. Each time we'll receive an output - via the read callback - we'll put temporarily the data in a buffer, and then write them in a stream. The read callback will call himself recursively until the process exit.