oct. 31

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.

Tags: | |

Commentaires

Busby SEO

Posted on mercredi, 17 décembre 2008 07:23

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

pe.dautreppe

Posted on mercredi, 17 décembre 2008 09:19

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

AMINA

Posted on mercredi, 29 avril 2009 16: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!

pe.dautreppe

Posted on dimanche, 17 mai 2009 15: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.

Hichem CHTARA

Posted on jeudi, 10 décembre 2009 23: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

Pierre-Emmanuel Dautreppe

Posted on vendredi, 11 décembre 2009 10: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

Gogo

Posted on jeudi, 17 décembre 2009 11: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.

Pierre-Emmanuel Dautreppe

Posted on vendredi, 18 décembre 2009 13: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 ?

Gjorgi

Posted on mardi, 12 janvier 2010 10: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.

Jack

Posted on mercredi, 3 février 2010 11: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

Posted on mercredi, 3 février 2010 11: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
   }
}

Achraf

Posted on jeudi, 11 février 2010 13: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");
            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.

pe.dautreppe

Posted on vendredi, 12 février 2010 09:29

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.

Kang

Posted on vendredi, 12 novembre 2010 14:31

Help.
I keep getting this error:

Error  1  Could not load file or assembly 'QuickGraph.Contracts, Version=3.3.50603.0, Culture=neutral, PublicKeyToken=f3fb40175eec2af3' or one of its dependencies. Strong name signature could not be verified.  The assembly may have been tampered with, or it was delay signed but not fully signed with the correct private key. (Exception from HRESULT: 0x80131045)    

sri

Posted on mardi, 19 juillet 2011 05:15

Thanks for the great tutorial. But when i build the code, i getting this error :

The type or namespace name 'TEdge' could not be found (are you missing a using directive or an assembly reference?)  E:\MY EXERCISE\WebApplication2\WebApplication2\GraphGeneratorAndRenderer.ashx.cs  23  63  WebApplication2


help me, please. Thanks

aan

Posted on mardi, 19 juillet 2011 07:31

i have many error in code...could you give me your complete source code. thank you very much

aan

Posted on mardi, 19 juillet 2011 08:41

XML Parsing Error: no element found
Location: http://localhost:4904/GraphGeneratorAndRenderer.ashx
Line Number 1, Column 1:

bebe

Posted on mardi, 19 juillet 2011 11:29

thanks for the tutorial, sir. but, I've been following in accordance with the steps you give, but when in debug, does not give results, only a blank page. Could you help me? Thanks

buy an iphone

Posted on mardi, 8 novembre 2011 14:28

Online mobile shopping much better looks nice! It was very useful for UK based mobile shopping; this website helped me to compare Mobile Phone Deals

self defence

Posted on jeudi, 10 novembre 2011 00:03

Are you concerned about your personal self defense? Your enemy or any mugger can attack you on sudden. You certainly will try to save yourself by fighting with them. But can you really save yourself without knowing how to fight with them methodically? In fact, martial arts trainingteach you different self defense strategies and tricks to overpower your attacker and save yourself. There are several types of training in martial arts Sydney. Some of them are freehand fighting and some of them involves tool in fighting like knife fighting. Besides self defense, martial arts offers various benefits. Physical and mental fitness is one of the considerable benefits of this training.

profumeria

Posted on dimanche, 25 décembre 2011 20:14

thanks for useful post

dental implants cost

Posted on jeudi, 29 décembre 2011 15:29

many thanks for this useful post

Juvederm training

Posted on samedi, 14 janvier 2012 19:15

Would you recommend to trained up by online training? Yes, because there is one which is Dermal Filler Training Review offering challenging service to the doctors.

Ajouter un commentaire




biuquote
  • Commentaire
  • Aperçu immédiat
Loading