Calendar

<<  mars 2010  >>
lumamejevesadi
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar
Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010

(octobre 31, 2008 13:43)

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;
 
//To ease the writing of our graph, we can use type aliases
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>();
 
         //1. Let's declare our vertices (ie our states)
         TVertex init = "Initial State";
         TVertex cancelled = "Cancelled";
         TVertex deleted = "Deleted";
         TVertex scheduled = "Scheduled";
         TVertex expected = "Expected";
 
         //2. Let's add them to our graph
         graph.AddVertex(init);
         graph.AddVertex(cancelled);
         graph.AddVertex(deleted);
         graph.AddVertex(scheduled);
         graph.AddVertex(expected);
 
         //3. Let's add the edges between our states
         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);
   }
 
   //Other implementation remains unchanged
}

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() )
   {
      //We'll launch dot.exe in command line
      process.StartInfo.FileName = @"C:\Program Files\Graphviz 2.21\bin\dot.exe";
      //Let's give the type we want to generate to, and a charset
      //to support accent
      process.StartInfo.Arguments = string.Format("-T{0} -Gcharset=latin1", imageType.ToString());
      //We'll receive the bitmap thru the standard output stream
      process.StartInfo.RedirectStandardOutput = true;
      //We'll need to give the dot structure in the standard input stream
      process.StartInfo.RedirectStandardInput = true;
      process.StartInfo.UseShellExecute = false;
      process.Start();
      //Let's sent the dot structure and close the stream to send the data and tell
      //we won't give any more
      process.StandardInput.Write(dot);
      process.StandardInput.Close();
      //Wait the process is finished and get back the image (binary format)
      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);
 
   //Save the bitmap to the response stream
   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() )
      {
         //We'll launch dot.exe in command line
         process.StartInfo.FileName = @"C:\Program Files\Graphviz 2.21\bin\dot.exe";
         //Let's give the type we want to generate to, and a charset
         //to support accent
         process.StartInfo.Arguments = "-Tgif -Gcharset=latin1";
         process.StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
 
         //We'll receive the bitmap thru the standard output stream
         process.StartInfo.RedirectStandardOutput = true;
         //We'll need to give the dot structure in the standard input stream
         process.StartInfo.RedirectStandardInput = true;
         process.StartInfo.UseShellExecute = false;
         process.Start();
         standardOutput = process.StandardOutput.BaseStream;
         standardOutput.BeginRead(buffer, 0, buffer.Length, StandardOutputReadCallback, null);
         //Let's sent the dot structure and close the stream to send the data and tell
         //we won't give any more
         process.StandardInput.Write(dot);
         process.StandardInput.Close();
         //Wait the process is finished and get back the image (binary format)
         process.WaitForExit();
         return Encoding.Default.GetString(memoryStream.ToArray());
      }
   }
 
   private void StandardOutputReadCallback(IAsyncResult result)
   {
      int numberOfBytesRead = standardOutput.EndRead(result);
      memoryStream.Write(buffer, 0, numberOfBytesRead);
 
      // Read next bytes.   
      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.

Billets liés

Commentaires

décembre 17. 2008 06:23

i will give it a try based on the step given above.

Busby SEO

décembre 17. 2008 08:19

Do not hesitate letting a comment in case of problem. We have it running in production right now.

pe.dautreppe

avril 29. 2009 15:40

I am a beginner with C#.NET and I recently used the .NET IDE. And in my work, I must implement a program that generates the graph associated to an assembly of components. I find that these tools are interesting (Quickgraph and Graphviz) and this tutorial is very important and usefull. But, I asked how to configure the.NET IDE library to support Quickgraph to work with.

I would be delighted to help. Thank you!

AMINA

mai 17. 2009 14:31

Makes me think of a reference graph or something like that. Am I correct ?
If it is the case, you should have a look to Reflector.NET's addins or to NDepend that already do that out of the box.

pe.dautreppe

décembre 10. 2009 22:04

Hello Pierre Emmanuel, I always have been interested in graph drawing by the busines I work which deals with electrical harness drawing. Your blog is very (^n) interesting. How to transform the code for WinForms?
I thank you very much for sharing knowledge
I am always wowwww gaga in front of people who take time to share knowledge

Hichem CHTARA

décembre 11. 2009 09:57

Hello Hichem,

I have never port the code for winforms but I guess there should not be so much difference.
With this code, you receive a string containing the bitmap content. As a consequence, you can just write it to a bitmap object.

In your case, I would be trying to add a container on my winform (like a panel or something) and to get a Graphics object from it. Then I would simply try to use the "DrawBitmap" methods on it. Just like that, I don't remember if you can pass a string directly or if you will need to give a stream object but anyway you could use a StringReader to make.

Just let me know if you succeed to make it work with this clues. Otherwise, I will make a try

Pierre-Emmanuel Dautreppe

décembre 17. 2009 10:14

I have a problem with the image. When I add few vertexes and run the page I get half (image or) graph. Is it image format problem or I need to set size on the image, and if...where can set the maximum image size of the graph.

Thanks.

Gogo

décembre 18. 2009 12:28

Ohhh that's a very strange problem.
If you run directly the dot tools (outside of the WebApp) giving him your dot structure, does it work correctly ?

Pierre-Emmanuel Dautreppe

janvier 12. 2010 09:05

You need to make cnahge on the code in Run():

process.StandardInput.Close();
//Wait the process is finished and get back the image (binary format)
process.WaitForExit(1000);

with

process.WaitForExit(1000);
process.StandardInput.Close();
process.Close();

Otherwise you will get half image of graph.

Gjorgi

février 2. 2010 08:23

I like the tips for generating the graphs. Thanks a lot for that you do not know how badly I want this.

zoom whitening

février 3. 2010 10:17

Thanks for the sample, it's exactly what I was looking for. I'd concur with Gjorgi though, the Run() method should be tweaked a bit to avoid incomplete images. In addition to that, the code will be trapped in an infinite loop in StandardoutputReadCallback if something goes wrong. Following is how I fixed it:

public string Run(GraphvizImageType imageType, string dot, string outputFileName) {
....
process.StandardInput.Close();
process.WaitForExit();
while (standardOutput.CanRead) {
&nbsp;&nbsp;&nbsp;System.Threading.Thread.Sleep(50); // Wait for StandardOut to finish reading
}
...

private void StandardOutputReadCallback(IAsyncResult result) {
&nbsp;&nbsp;&nbsp;int numberOfBytesRead = standardOutput.EndRead(result);

&nbsp;&nbsp;&nbsp;if (numberofBytesRead > 0) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memoryStream.Write(buffer, 0, numberOfBytesRead);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Read next bytes.
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;standardOutput.BeginRead(buffer, 0, buffer.Length, StandardOutputReadCallback, null); }
&nbsp;&nbsp;&nbsp;else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;standardOutput.Close(); // Nothing left to be read, terminate the async
&nbsp;&nbsp;&nbsp;}
}

I'd also honor outputFileName and try to write out the bits as well.

Jack

février 3. 2010 10:23

Trying one more time w/o nbsp;, got fooled by "Live preview"...

public string Run(GraphvizImageType imageType, string dot, string outputFileName) {
....
process.StandardInput.Close();
process.WaitForExit();
while (standardOutput.CanRead) {
System.Threading.Thread.Sleep(50); // Wait for StandardOut to finish reading
}
...
}

private void StandardOutputReadCallback(IAsyncResult result) {
int numberOfBytesRead = standardOutput.EndRead(result);

if (numberofBytesRead > 0) {
memoryStream.Write(buffer, 0, numberOfBytesRead);
// Read next bytes.
standardOutput.BeginRead(buffer, 0, buffer.Length, StandardOutputReadCallback, null);
}
else {
standardOutput.Close(); // Nothing left to be read, terminate the async
}
}

Jack

février 11. 2010 12:02

Hi Pierre-Emmanuel Dautreppe
Thank you for this Exemple of using QuickGraph, But when I tried to implement this exemple I have some problemes
first of all, QuickGraph.NamedEdge<string> not accepted
seconde i don't know what i should to write in run methode

I tried to modifiy this code but also I' haven't any result


this is my code

using System.Web;
using QuickGraph;
using QuickGraph.Algorithms;
using QuickGraph.Graphviz;
using QuickGraph.Graphviz.Dot;

//To ease the writing of our graph, we can use type aliases
//using TEdge = QuickGraph.Edge<string>;
using TVertex = System.String;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace WebApplication1
{


public class GraphGeneratorAndRenderer : IHttpHandler
{
private AdjacencyGraph<TVertex, Edge<string>> CreateGraph()
{
AdjacencyGraph<TVertex, Edge<string>> graph = new AdjacencyGraph<TVertex, Edge<string>>(true);

//1. Let's declare our vertices (ie our states)
TVertex init = "Initial State";
TVertex cancelled = "Cancelled";
TVertex deleted = "Deleted";
TVertex scheduled = "Scheduled";
TVertex expected = "Expected";

//2. Let's add them to our graph

graph.AddVertex(init);
graph.AddVertex(cancelled);
graph.AddVertex(deleted);
graph.AddVertex(scheduled);
graph.AddVertex(expected);

//3. Let's add the edges between our states


Edge<string> i_c = new Edge<string>(init, cancelled);
Edge<string> i_s = new Edge<string>(init, scheduled);
Edge<string> d_c = new Edge<string>(deleted, scheduled);
Edge<string> e_c = new Edge<string>(expected, init);

graph.AddEdge(i_c);
graph.AddEdge(i_s);
graph.AddEdge(d_c);
graph.AddEdge(e_c);



return graph;
}


public bool IsReusable
{
get { return false; }
}

private string GenerateBitmap(AdjacencyGraph<TVertex, Edge<string>> graph)
{
GraphvizAlgorithm<TVertex, Edge<string>> algo = new GraphvizAlgorithm<TVertex, Edge<string>>(graph);
algo.FormatEdge += delegate(object sender, FormatEdgeEventArgs<TVertex, Edge<string>> e)
{
e.EdgeFormatter.Label.Value = e.Edge.ToString();
};
algo.FormatVertex += delegate(object sender, FormatVertexEventArgs<TVertex> e)
{
e.VertexFormatter.Label = e.Vertex;
};
string output = algo.Generate(new BitmapGeneratorDotEngine(), "ignored"Wink;
return output;
}

public void ProcessRequest(HttpContext context)
{
AdjacencyGraph<TVertex, Edge<string>> graph = CreateGraph();
string bitmap = GenerateBitmap(graph);
} //Other implementation remains unchanged
}

public class BitmapGeneratorDotEngine : IDotEngine
{
#region IDotEngine Members

public string Run(GraphvizImageType imageType, string dot, string outputFileName)
{ return outputFileName;
}

#endregion
}
}


Pending a response, please accept Sir, the assurances of my most respectful greetings.

Achraf

février 12. 2010 09:02

Hello Achraf,

yes indeed you are right, in the latest version of the library, the class NamedEdge has been removed.
I have updated the code above to use the Quickgraph.TaggedEdge<string, string> class.

Note that doing this, you should also correct the GenerateBitmap method to use the "e.Edge.Tag" instead of "e.Edge.Name".

For the "Run" method, you will see full code in the post.

Pierre-Emmanuel Dautreppe

février 19. 2010 16:32

Hello Pierre
Thank you for your help last time . Now, I have some questions to ameliorate the appearance of my generated graph.

My project is to draw an automaton (deteministic autoamaton/
undeterministic autoamaton). The graph generated , must contains
a marked states and initial states .A marked state is
represented by two circles .

So my questions are :

-How can i personalize my graph ( color of vertex / edge ..
Styles ??
- how can i draw a vertex with two circles ?
- how can i specify the color of my vertexs and edges ?
- how can i add an arrow to a vertex to indicate that this state is an initial state ??
- what are eventually the others options of draw ?

While waiting for an answer , I took you sir, accept the assurances of my most respectful greetings

Ben Said Achraf

Ajouter un commentaire


 

  Country flag





Live preview

mars 12. 2010 15:35

Powered by BlogEngine.NET 1.2.0.0 | Theme by Pierre-Emmanuel Dautreppe