Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Windows Forms
Article

Traceract

Rate me:
Please Sign up or sign in to vote.
3.69/5 (9 votes)
3 Sep 20059 min read 98.3K   1.6K   37   18
A prototype debug tracer with an added dimension.

Image 1

Introduction

Traceract is a prototype debug message tracer. The name is a combination of the words "tesseract" and "trace", as it adds "dimension" to the usual debug trace output. In particular, Traceract provides a simple command interface, directing the viewer to output trace messages to a particular window. Although it is still in its infancy, I find it quite valuable simply as is, although there are a myriad of features I would like to add to it over time. One reason to put this in the public domain is that I'd also like to get feedback as to what kind of features other people would like to see.

Features

Traceract features are straightforward:

  • Simple commands embedded as debug output string to direct debug output to specific view windows.
  • Weifen Luo's excellent docking manager is used to enable the user to customize and control the layout of the debug windows.
  • Traceract currently uses a ListView in detail mode to display the debug number, time, and message. This is a bit limiting, especially with regards to line length. Traceract currently wraps lines automatically (and quite dumbly).

Commands

Traceract uses the "!" (exclamation) character, positioned as the first character in the debug string, to identify a command. If the remaining portion of the string does not match the required format, Traceract outputs the debug string as normal.

Initialization

Your application is responsible for defining the output windows to which it will be directing debug strings. This is done with the initialization command:

!!<tag>=<name>

tag is the shorthand name you will be using in the debug string text to identify that the string is output to the window referenced by the tag. name is the human readable name you are assigning to the output window. By default, all Windows appear on the document tab strip.

For example:

C#
Debug.WriteLine("!!sql=SQL");
Debug.WriteLine("!!tr=Trans. Rec.");
Debug.WriteLine("!!err=Errors"); 
Debug.WriteLine("!!br=Bus. Rules");

Directing debug strings

A debug string is directed to the appropriate output window using the format:

!<tag>:<msg>

tag is the shorthand name specified during initialization. msg is the usual debug string.

For example:

C#
Debug.WriteLine("!sql:Open");
Debug.WriteLine("!sql:"+stmt.ToString());
Debug.WriteLine("!sql:Commit");
Debug.WriteLine("!sql:Close");

Other commands

The only commands that Traceract currently supports are Clear and ClearAll:

!!Clear:<tag>

This clears the debug strings in the output window specified by the tag name.

!!ClearAll

This clears all the debug strings in all output windows.

Under the hood

The code that I'm primarily going to illustrate here is the debug message string handling. I will also briefly describe the declarative UI and serialization of the dock panels. However, a full discussion of the dock panel manager that is a wrapper for Weifen Luo's DockPanel Suite will be presented in a separate article, as I'd like to illustrate using both his toolkit and the Infragistics docking manager, and how one writes a wrapper that allows one to use either docking manager in an application without changing the application code.

Debug messages

The Debug Message Monitor

At its core, Traceract intercepts the debug messages in a worker thread. The main loop:

C#
public void Run()
{
  running=true;
  Events.SetEvent(ack);

  while(running)
  {
    Events.WaitForSingleObject(ready, -1);

    if (!running)
    {
      break;
    }

    if (DbgHandler != null)
    {
      UInt32 nApp=
         (UInt32)Marshal.ReadInt32(sharedAddress);
      long n=sharedAddress.ToInt64()+4;
      string str=
         Marshal.PtrToStringAnsi(new IntPtr(n)); 
      string[] lines=str.Split('\n');

      foreach(string line in lines)
      {
        string l2=line.Trim();
      
        if (l2 != String.Empty)
        {
          // Ignore completely blank lines, but 
          // if not blank, preserve whitespace.
          DebugDataEventArgs args=
                 new DebugDataEventArgs(nApp, line);
          DbgHandler(this, args);
        }
      }
    }
  Events.SetEvent(ack);
  }
}

reads a string, splits it into discrete lines, and invokes the DbgHandler event.

The Event Handler

The event handler does some brute-force decoding of the debug string to check for tags and commands. It then adds a DebugMessageEventArgs instance to a queue that is monitored by a separate worker thread. The reason for this is that the debug monitoring event thread cannot perform a Form.Invoke, nor can it post messages to the Windows message queue. I haven't really looked into why (my knowledge here is ignorant). You will note that a hashtable handlerToContentMap is used to obtain the DebugListView instance that actually handles the display of the debug messages associated with the tag specified in the debug string.

C#
private void OnDebugString(object sender, 
                                 DebugDataEventArgs e)
{
  long ticks=HiResTimer.Ticks;
  double tick=((double)(ticks - start))/
                    (double)HiResTimer.TicksPerSecond;
  string handler=null;
  string[] msgs=e.Data.Split('\r');
  
  foreach(string m in msgs)
  {
    string msg=m.Replace("\t", 
                       spaces.Substring(0, tabWidth));
    msg=msg.TrimEnd();

    if (msg.Length > 1)
    {
      if (msg[0] == '!')
      {
        if (msg.IndexOf(':') != -1)
        {
          handler=StringHelpers.Between(msg, '!', ':');
          msg=StringHelpers.RightOf(msg, ':');
        }
        else if (msg[1] == '!')
        {
          msg=StringHelpers.RightOf(msg, '!', 2);
          string[] vals=msg.Split('=');

          if (vals.Length==2)
          {
            string tabName=vals[1];
            handlerToNameMap[vals[0]]=tabName;
          }
          else
          {
            if (vals[0].ToLower()=="clearall")
            {
              ClearAll();
            }
            else if (vals[0].ToLower()=="clear")
            {
              string cmd=vals[0];
              vals=cmd.Split(':');

              if (vals.Length==2)
              {
                if (handlerToContentMap.Contains(vals[1]))
                {
                  Content content=
                      (Content)handlerToContentMap[vals[1]];
                  DebugListView dlv=
                      (DebugListView)content.Controls[0];
                  dlv.Items.Clear();
                }
              }
            }
          }
        }
      }
    }

    if (msg.Length > 0)
    {
      // Uses a queue because we can't do a form.Invoke
      // here (nor does posting a message work).

      int n=0;
      int len=msg.Length;
      DebugMessageEventArgs dmea;

      while (len > 0)
      {
        if (n > 0)
        {
          dmea=new DebugMessageEventArgs(count, tick, handler," "+
                msg.Substring(n, len > maxWidth ? maxWidth : len),
                excludeFromFullOutput);
        }
        else
        {
          dmea=new DebugMessageEventArgs(count, tick, handler, 
                msg.Substring(n, len > maxWidth ? maxWidth : len),
                excludeFromFullOutput);
        }
        n+=maxWidth;
        len-=maxWidth;

        lock(queue)
        {
          queue.Enqueue(dmea);
        }
      }

      ++count;
    }
  }
}

Queue processing

As mentioned above, the debug monitor thread cannot directly perform a Form.Invoke or post messages to the application. Instead, the thread adds the messages to a queue. Again, the code implementation is rather brute force--I could have used semaphores but chose a much simpler approach:

C#
public void ProcessMessages()
{
  stop=false;
  while (!stop)
  {
    while (!stop && (queue.Count > 0) )
    {
      DebugMessageEventArgs dmea=
                    (DebugMessageEventArgs)queue.Dequeue();
      form.Invoke(new SendMessageDlgt(SendMessage), 
                                      new object[] {dmea});
    }
    Thread.Sleep(100);
  }
}

I can hear the screams already! Sleeping the thread! Well, what can I say. There's definitely room for improvement.

Message processing

The queue worker thread finally gets the debug message posted into the application's thread, from which we can safely update the ListView control:

C#
private void SendMessage(DebugMessageEventArgs dmea)
{
  if (dmea.Handler != null)
  {
    if (!handlerToContentMap.Contains(dmea.Handler))
    {
      string dwName=Guid.NewGuid().ToString();
      dockingManager.CopyDockWindowTemplate(dwName, 
                                                "newHandler");
      Content content=dockingManager.CreateDockWindow(dwName);
      string contentName=dmea.Handler;
      
      if (handlerToNameMap.Contains(dmea.Handler))
      {
        contentName=(string)handlerToNameMap[dmea.Handler];
      }

      content.Caption=contentName;
      DebugListView dlv=(DebugListView)content.Controls[0];
      dlv.Filter=dmea.Handler;
      handlerToContentMap[dmea.Handler]=content;
      contentToHandlerMap[content]=dmea.Handler;
      content.ContentClosed+=new 
         MyXaml.DockingManager.Content.ClosedDlgt(OnContentClosed);
    }
  }

  if (DebugMessage != null)
  {
    DebugMessage(this, dmea);
  }
}

The above code will create a window on-the-fly when a new tag is encountered. Also, it will set the window's name to a previously specified human-readable caption, otherwise it uses the tag text. Lastly, it invokes the DebugMessage event. This event is hooked by all output windows, and each window gets the opportunity to display the debug message as determined by the filter (the tag). I chose this approach so that in the future, a single window can be told to "watch" for multiple tags. Again, some optimization would be helpful here--only invoking the handler associated with a specific tag.

The DebugMessage handler

The DebugListView class implements the handler:

C#
private void OnDebugMessage(object sender, 
                            DebugMessageEventArgs e)
{
  bool display=true;

  if (filter != String.Empty)
  {
    display=e.Handler == filter;
  }
  else
  {
    display=(!e.ExcludeFromFullOutput) || (e.Handler==null);
  }

  if (display)
  {
    string count=e.Count.ToString();
    string tick=e.Tick.ToString("#0.00000");
    ListViewItem lvi=new ListViewItem(new string[] 
                                  {count, tick, e.Message});
    Items.Add(lvi);

    if (autoScroll)
    {
      EnsureVisible(Items.Count-1);
    }
  }
}

which checks if the filter is applicable and correct and if so, displays the debug string.

The user interface

As you can probably guess, the user interface is defined declaratively, using the MyXaml parser (the 2.0 beta version). Before I continue:

MyXaml is generic declarative instantiation engine. It's syntax looks similar to Microsoft's XAML primarily because both directly map XML elements to .NET classes and XML attributes to .NET properties. However, MyXaml is not an emulation of Microsoft's XAML. The two are distinctly different. For example, MyXaml's namespace mapping is different, and MyXaml does not support compound property syntax or implicit collections.

The UI definition consists of four parts:

  • the form
  • the menu
  • the docking manager definition
  • the dock window content templates

A complete discussion of the docking manager and the wrapper that I've written to support Weifen Luo's DockPanel Suite is going to be a separate article, as the article will also illustrate how the wrapper abstracts the docking manager and can be used also for other third party docking managers, such as Infragistic's. In this article, I will only briefly describe the declarative part. As with all declarative XML, it begins with an xmlns to .NET namespace map:

XML
<MyXaml
    xmlns="System.Windows.Forms, System.Windows.Forms,
        Version=1.0.5000.0, 
        Culture=neutral,
        PublicKeyToken=b77a5c561934e089"
    xmlns:dm="WinFormsUIDockingManager"
    xmlns:mxdm="MyXaml.DockingManager"
    xmlns:menu="MyXaml.MxMenu"
    xmlns:def="Definition"
    xmlns:ref="Reference">

and the "ref" and "def" xmlns tags are internally used by the parser.

The form

The form definition is straightforward:

XML
<mxdm:DockableForm def:Name="AppMainForm"
    Text="Traceract - Trace Viewer"
    ClientSize="800, 600"
    StartPosition="CenterScreen"
    FormBorderStyle="Sizable">

This instantiates the DockableForm class and sets a few properties.

The menus

The menu object graph includes wiring up the event handlers. I'm using the MxMenu assembly here so that in the future I can add icons to the menus (the original implementation was written by Chris Becket). As with all MyXaml object graphs, this follows a "class-property-class" parent-child-grandchild format.

XML
<Menu>
  <menu:MxMainMenu>
    <MenuItems>
      <MenuItem Text="&amp;File">
        <MenuItems>
          <menu:MxMenuItem Text="&amp;Load Layout" 
                                          Click="{app.OnLoadLayout}"/>
          <menu:MxMenuItem Text="&amp;Save Layout" 
                                          Click="{app.OnSaveLayout}"/>
          <menu:MxMenuItem Text="-"/>
          <menu:MxMenuItem Text="&amp;Test" Click="{app.OnTest}"/>
          <menu:MxMenuItem Text="-"/>
          <menu:MxMenuItem Text="E&amp;xit" Click="{app.OnExitApp}"/>
        </MenuItems>
      </MenuItem>
      <MenuItem Text="&amp;View">
        <MenuItems>
          <menu:MxMenuItem Text="&amp;Clear All" Click="{app.OnClearAll}"/>
        </MenuItems>
      </MenuItem>
        <MenuItem Text="&amp;About" Click="{app.OnAbout}">
      </MenuItem>
    </MenuItems>
  </menu:MxMainMenu>
</Menu>

Docking Manager definition

The DockableForm class extends Form and adds only one property, a DockManager, which is initialized to an instance of a DockingManager.

XML
<DockManager>
  <dm:DockingManager def:Name="dockingManager"
    SerializeExtraAttributes="{app.OnSerializeExtraAttributes}"
    DeserializeExtraAttributes="{app.OnDeserializeExtraAttributes}">
    <DockSites>
      <mxdm:DockSite def:Name=
               "dockLeft" Edge="Left" Width="300" AutoHide="false"/>
      <mxdm:DockSite def:Name="dockRight" Edge="Right" Width="200"/>
      <mxdm:DockSite def:Name="dockTop" Edge="Top"/>
      <mxdm:DockSite def:Name="dockBottom" Edge="Bottom" Height="200"/>
      <mxdm:DockSite def:Name="document" Edge="Document"/>
    </DockSites>
    <DockWindows>
      <mxdm:DockWindow def:Name="fullOutput" SiteName="dockBottom" 
                       Caption="Full Output" 
                       ContentFile="content.myxaml" 
                       ContentName="fullOutputContent"/>
      <mxdm:DockWindow def:Name="newHandler" SiteName="document"
                       ContentFile="content.myxaml" 
                       ContentName="newHandlerContent"/>
    </DockWindows>
  </dm:DockingManager>
</DockManager>

The DockingManager class manages two things--dock sites and dock windows. A DockWindow is docked to a particular site and the content for the window comes from a separate template file, referenced by the properties ContentFile and ContentName. With this information we can construct the initial form layout, which is done imperatively during initialization:

C#
Content content=dockingManager.CreateDockWindow("fullOutput");

When a new debug message window is required, the imperative code copies the "newHandler" template and instantiates a new dock window:

C#
string dwName=Guid.NewGuid().ToString();
dockingManager.CopyDockWindowTemplate(dwName, "newHandler");
Content content=dockingManager.CreateDockWindow(dwName);

Thus, the "newHandler" XML definition determines where the new debug message windows are initially positioned--in this case, in the document tab strip.

Dock window content templates

The templates, which are defined in a separate file, determine the content of the DockWindow instances. Therefore, if you want a different content, you not only define the content template but also an initial DockWindow instance in which that content is displayed. The two templates defined by default are:

XML
<mxdm:Content def:Name="fullOutputContent">
  <Controls>
    <tr:DebugListView Dock="Fill" View="Details" 
                      FullRowSelect="true" GridLines="true">
      <Columns>
        <ColumnHeader Text="#"/>
        <ColumnHeader Text="Time"/>
        <ColumnHeader Text="Output" Width="600"/>
      </Columns>
    </tr:DebugListView>
  </Controls>
</mxdm:Content>

<mxdm:Content def:Name="newHandlerContent" 
            ContentCreated="{app.OnContentCreated}">
  <Controls>
    <tr:DebugListView Dock="Fill" View="Details" 
                 FullRowSelect="true" GridLines="true">
      <Columns>
        <ColumnHeader Text="#"/>
        <ColumnHeader Text="Time"/>
        <ColumnHeader Text="Output" Width="600"/>
      </Columns>
    </tr:DebugListView>
  </Controls>
</mxdm:Content>

Application initialization

For those new to declarative programming, I'll describe the application initialization, where imperative and declarative code meet. Here's the initialization code:

C#
Parser.AddExtender("MyXaml.WinForms", 
                   "MyXaml.WinForms", "WinFormExtender");
parser=new Parser();
parser.AddReference("app", this);
form=(Form)parser.Instantiate("traceract.myxaml", "*");
form.Closing+=new CancelEventHandler(OnFormClosing);
form.Show();
parser.InitializeFields(this);

The MyXaml 2.0 parser is "platform neutral", meaning that it doesn't include the System.Windows.Forms namespace, nor does it know about special things that ought to be done during form initialization. This is handled by the MyXaml.WinForms extender which hooks events in the parser. In particular, it implements handlers for the InstantiateBegin and InstantiateEnd methods, which in turn call SuspendLayout and ResumeLayout for any instances of Control type.

In the declarative portion, events were wired up to the "app" instance. The above code illustrates how the parser is told about the "app" instance.

Also, the parser can initialize fields with the instance instantiated during the declarative parsing. In the main application, there is one such field:

C#
[MyXamlAutoInitialize] DockManager dockingManager=null;

The MyXamlAutoInitialize attribute tells the parser that only fields decorated with this attribute expect (and require) to be initialized.

Serialization

Traceract can save an existing layout and load it (although, this still seems a bit buggy). The layout is serialized to XML as well. For example, if you change the layout to something like this (shrunk so it fits in a screenshot):

Image 2

The serialization of the form layout (stored in layout.xml) looks something like this:

XML
<WindowLayout>
  <DockSites>
    <DockSite Name="Bottom1" Edge="Bottom" Width="600"
              Height="200" Location="0, 0" Size="600, 200" />
    <DockSite Name="Left2" Edge="Left" Width="200"
              Height="600" Location="0, 0" Size="200, 600" />
    <DockSite Name="Right2" Edge="Right" Width="200"
              Height="600" Location="0, 0" Size="200, 600" />
    <DockSite Name="Document3" Edge="Document" Width="0"
              Height="0" Location="0, 0" Size="0, 0" />
  </DockSites>
  <DockWindows>
    <DockWindow Caption="Database" ContentName="newHandlerContent"
                ContentFile="content.myxaml" 
                Name="3b8aeef3-6218-4f35-a053-86ffb5dc3ee0"
                SiteName="Left2" Filter="db" AutoScroll="True" />
    <DockWindow Caption="Full Output" ContentName="fullOutputContent" 
                ContentFile="content.myxaml" 
                Name="fullOutput" SiteName="Bottom1" Filter="" 
                AutoScroll="True" />
    <DockWindow Caption="Business Layer" ContentName="newHandlerContent" 
                ContentFile="content.myxaml" 
                Name="6aec5b1c-8fc6-4fbc-a547-53cbf7f0a800" 
                SiteName="Document3" Filter="bl" AutoScroll="True" />
    <DockWindow Caption="GUI" ContentName="newHandlerContent" 
                ContentFile="content.myxaml" 
                Name="12413f44-8858-4b5f-ad29-7e097bd2768f" 
                SiteName="Right2" Filter="ui" AutoScroll="True" />
  </DockWindows>
</WindowLayout>

You will note that a GUID is used to ensure that a unique name is created for new content windows.

Serialization/deserialization of the window content is complicated. Each docking manager I've worked with is very finicky about deserialization order and how it controls the tiling of the windows. As I've mentioned before, I'll be discussing this in greater depth in an article on docking managers in general.

Upcoming features

The following is a list of features I'm planning to work on as time permits and need requires. Feel free to let me know of other features you'd like.

  • improve the code!
  • replace the ListView with something more decent,
  • colorization,
  • better line wrap handling,
  • automatic positioning of all windows to adjacent debug messages when selecting a debug message,
  • save/restore application screen position and size,
  • automatic reloading of last configuration,
  • MRU list of configurations,
  • file selection for save/load of configurations,
  • automatic configuration selection through debug message string,
  • remote debugging support,
  • copy line/range to clipboard,
  • print,
  • additional configuration options, such as always output to main debug window, line wrap, etc.,
  • layout serialization bug fixes.

References

Terms and conditions

Traceract is intended to be used for your personal use. You can modify it, borrow from the code, etc., as much as you wish, as long as this is for your own personal use. Traceract or its derivatives cannot be distributed as a stand-alone commercial application or as part of a commercial application without my express permission. Traceract uses the GPL'd MyXaml assemblies. Including the source code and assemblies in the download here does not convey any rights to use the MyXaml assemblies in a commercial application.

Updates and source code

You can obtain the latest code releases, updated source code, and if you wish, contribute to the project by obtaining an account for my CVS server. Anyone interested in this should contact Marc Clifton and I will set you up with an account. I will also be updating the article download, but probably not as frequently.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionStill current? Pin
roscler25-Aug-14 3:53
professionalroscler25-Aug-14 3:53 
AnswerRe: Still current? Pin
Marc Clifton25-Aug-14 6:05
mvaMarc Clifton25-Aug-14 6:05 
GeneralBroken link Pin
martin_hughes3-Sep-07 6:49
martin_hughes3-Sep-07 6:49 
GeneralRe: Broken link Pin
Humble Programmer2-Jul-08 10:17
Humble Programmer2-Jul-08 10:17 
QuestionHow do you get it to work? Pin
jonroberts2421-Mar-07 0:49
jonroberts2421-Mar-07 0:49 
QuestionTraceract for tracing a service? Pin
AjayVaidya17-Oct-05 18:37
AjayVaidya17-Oct-05 18:37 
GeneralThread safety and log4net suggestion Pin
John Rayner6-Sep-05 11:58
John Rayner6-Sep-05 11:58 
GeneralRe: Thread safety and log4net suggestion Pin
Marc Clifton6-Sep-05 12:07
mvaMarc Clifton6-Sep-05 12:07 
GeneralRe: Thread safety and log4net suggestion Pin
Marc Clifton6-Sep-05 14:32
mvaMarc Clifton6-Sep-05 14:32 
GeneralRe: Thread safety and log4net suggestion Pin
John Rayner7-Sep-05 10:04
John Rayner7-Sep-05 10:04 
Marc,

There is no GUI that ships with log4net. It's aim is to get the right log messages to wherever you want them (e.g. a database, a remoting sink, a file, an email message, etc - these are "appenders" in log4net terminology). I had thought that it would be interesting to get messages generated through log4net displayed by Traceract (perhaps by using messages sent through the RemotingAppender?).

Leaving logging messages in a release build is indeed one of the major points against log4net. In its defence, the log4net developers are fully aware of this and state that they have optimised the library for situations where it doesn't actually log any messages (e.g. most release builds). In my experience I've always found log messages in a release build to be useful, but I understand that not everyone feels the same.

Previously I've mainly used two different appenders on log4net:

  • MemoryAppender - collects log messages in memory (actually in an ArrayList). I then wrote a form in my application to display these messages via a timer.
  • DatabaseAppender - pushes log messages into a database. And then we wrote an ASPX page to display (and filter) the contents of the database table.
The coolest feature IMO was the way you can get log4net to monitor the config file and to reconfigure itself whenever the file is modified. So you can then increase the verbosity of the log messages in specific parts of the application without requiring restarts. Very, very useful when your code has gotten into some kind of "stuck" state (e.g. a deadlock) and you want to find out what it thinks it's doing - you simply set the log level of the particular class(es) to Debug in the config file, save it and then sift through all the messages.

Cheers,
John
GeneralSuggestions Pin
leppie4-Sep-05 2:05
leppie4-Sep-05 2:05 
GeneralRe: Suggestions Pin
Marc Clifton4-Sep-05 2:20
mvaMarc Clifton4-Sep-05 2:20 
GeneralThread.Sleep Pin
leppie4-Sep-05 1:38
leppie4-Sep-05 1:38 
GeneralRe: Thread.Sleep Pin
Marc Clifton4-Sep-05 2:16
mvaMarc Clifton4-Sep-05 2:16 
QuestionExcellent Pin
Michael P Butler3-Sep-05 23:10
Michael P Butler3-Sep-05 23:10 
AnswerRe: Excellent Pin
Marc Clifton4-Sep-05 2:15
mvaMarc Clifton4-Sep-05 2:15 
QuestionTrace listener? Pin
Ashley van Gerven3-Sep-05 16:04
Ashley van Gerven3-Sep-05 16:04 
AnswerRe: Trace listener? Pin
Marc Clifton4-Sep-05 2:21
mvaMarc Clifton4-Sep-05 2:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.