65.9K
CodeProject 正在变化。 阅读更多。
Home

Traceract

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.69/5 (9投票s)

2005年9月3日

9分钟阅读

viewsIcon

102764

downloadIcon

1583

一个添加了维度的原型调试跟踪器。

引言

Traceract 是一个原型调试消息跟踪器。这个名字是“tesseract”(超立方体)和“trace”(跟踪)这两个词的组合,因为它为通常的调试跟踪输出添加了“维度”。特别是,Traceract 提供了一个简单的命令接口,指示查看器将跟踪消息输出到特定的窗口。虽然它仍处于早期阶段,但我发现它本身就很有价值,尽管随着时间的推移,我希望向其添加无数功能。将其公之于众的一个原因是,我也希望获得有关其他人希望看到哪些功能的反馈。

特点

Traceract 功能简单明了

  • 嵌入在调试输出字符串中的简单命令,用于将调试输出定向到特定的视图窗口。
  • 使用魏芬·罗(Weifen Luo)出色的停靠管理器,使用户能够自定义和控制调试窗口的布局。
  • Traceract 目前在详细模式下使用 ListView 来显示调试编号、时间戳和消息。这有点局限,尤其是在行长方面。Traceract 目前会自动(而且相当愚蠢地)换行。

Commands

Traceract 使用“!”(感叹号)字符作为调试字符串的第一个字符来标识命令。如果字符串的其余部分不符合所需格式,Traceract 将像往常一样输出调试字符串。

初始化

您的应用程序负责定义它将向其定向调试字符串的输出窗口。这是通过初始化命令完成的

!!<tag>=<name>

tag 是您将在调试字符串文本中使用的简称,用于标识该字符串输出到由该标签引用的窗口。name 是您分配给输出窗口的人类可读名称。默认情况下,所有窗口都显示在文档选项卡条上。

例如

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

定向调试字符串

调试字符串使用以下格式定向到相应的输出窗口

!<tag>:<msg>

tag 是初始化期间指定的简称。msg 是通常的调试字符串。

例如

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

其他命令

Traceract 目前仅支持 ClearClearAll 命令

!!Clear:<tag>

这将清除由 tag 名称指定的输出窗口中的调试字符串。

!!ClearAll

这将清除所有输出窗口中的所有调试字符串。

幕后

我主要将在这里说明的代码是调试消息字符串处理。我还将简要描述声明式 UI 和停靠面板的序列化。然而,关于停靠面板管理器的完整讨论(它是魏芬·罗的 DockPanel Suite 的包装器)将在另一篇文章中介绍,因为我想说明如何同时使用他的工具包和 Infragistics 停靠管理器,以及如何编写一个包装器,允许在不改变应用程序代码的情况下在应用程序中使用任一停靠管理器。

调试消息

调试消息监视器

Traceract 的核心是在工作线程中拦截调试消息。主循环

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);
  }
}

读取一个字符串,将其拆分为离散行,并调用 DbgHandler 事件。

事件处理程序

事件处理程序对调试字符串进行了一些暴力解码,以检查标签和命令。然后,它将 DebugMessageEventArgs 实例添加到由单独的工作线程监视的队列中。这样做的原因是调试监视事件线程无法执行 Form.Invoke,也无法向 Windows 消息队列发布消息。我还没有真正研究为什么(我在这方面的知识是空白的)。您会注意到,哈希表 handlerToContentMap 用于获取实际处理与调试字符串中指定的标签关联的调试消息显示的 DebugListView 实例。

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;
    }
  }
}

队列处理

如上所述,调试监视器线程不能直接执行 Form.Invoke 或向应用程序发布消息。相反,线程将消息添加到队列中。同样,代码实现相当粗暴——我可以使用信号量,但选择了一个更简单的方法

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);
  }
}

我已经能听到尖叫声了!休眠线程!好吧,我能说什么呢。肯定有改进的空间。

消息处理

队列工作线程最终将调试消息发布到应用程序的线程中,从那里我们可以安全地更新 ListView 控件

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);
  }
}

当遇到新标签时,上述代码将动态创建一个窗口。此外,它会将窗口名称设置为先前指定的人类可读标题,否则它将使用标签文本。最后,它调用 DebugMessage 事件。此事件由所有输出窗口挂钩,每个窗口都有机会根据过滤器(标签)显示调试消息。我选择这种方法是为了将来,一个窗口可以被告知“监视”多个标签。同样,这里的一些优化会很有帮助——只调用与特定标签关联的处理程序。

DebugMessage 处理程序

DebugListView 类实现了处理程序

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);
    }
  }
}

它检查过滤器是否适用和正确,如果是,则显示调试字符串。

用户界面

正如您可能猜到的,用户界面是使用 MyXaml 解析器(2.0 测试版)声明性定义的。在我继续之前

MyXaml 是一个通用的声明式实例化引擎。它的语法看起来与微软的 XAML 相似,主要是因为两者都直接将 XML 元素映射到 .NET 类,将 XML 属性映射到 .NET 属性。然而,MyXaml 并不是微软 XAML 的仿真。两者截然不同。例如,MyXaml 的命名空间映射不同,MyXaml 不支持复合属性语法或隐式集合。

UI 定义由四部分组成

  • 表单
  • 菜单
  • 停靠管理器定义
  • 停靠窗口内容模板

关于停靠管理器以及我为支持魏芬·罗的 DockPanel Suite 编写的包装器的完整讨论将是另一篇文章,因为该文章还将说明包装器如何抽象停靠管理器,并且还可以用于其他第三方停靠管理器,例如 Infragistic 的停靠管理器。在本文中,我将只简要描述声明性部分。与所有声明性 XML 一样,它以一个 xmlns 到 .NET 命名空间映射开头

<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">

ref”和“defxmlns 标签由解析器内部使用。

表单

表单定义很简单

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

这将实例化 DockableForm 类并设置一些属性。

菜单

菜单对象图包括连接事件处理程序。我在这里使用 MxMenu 程序集,以便将来我可以向菜单添加图标(原始实现由 Chris Becket 编写)。与所有 MyXaml 对象图一样,这遵循“类-属性-类”的父-子-孙格式。

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

停靠管理器定义

DockableForm 类扩展了 Form,并且只添加了一个属性,即 DockManager,它被初始化为 DockingManager 的一个实例。

<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>

DockingManager 类管理两件事——停靠站点和停靠窗口。一个 DockWindow 停靠在特定的站点上,窗口的内容来自单独的模板文件,由属性 ContentFileContentName 引用。有了这些信息,我们可以构建初始的表单布局,这是在初始化期间以命令式方式完成的

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

当需要一个新的调试消息窗口时,命令式代码复制“newHandler”模板并实例化一个新的停靠窗口

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

因此,“newHandler”XML 定义决定了新的调试消息窗口最初放置的位置——在本例中,是在文档选项卡条中。

停靠窗口内容模板

在单独文件中定义的模板决定了 DockWindow 实例的内容。因此,如果您想要不同的内容,您不仅要定义内容模板,还要定义一个初始的 DockWindow 实例,在该实例中显示该内容。默认定义的两个模板是

<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>

应用程序初始化

对于声明式编程的新手,我将描述应用程序初始化,即命令式代码和声明式代码的交汇点。这是初始化代码

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);

MyXaml 2.0 解析器是“平台中立的”,这意味着它不包含 System.Windows.Forms 命名空间,也不知道在窗体初始化期间应该做哪些特殊的事情。这由 MyXaml.WinForms 扩展器处理,该扩展器挂钩解析器中的事件。特别是,它实现了 InstantiateBeginInstantiateEnd 方法的处理程序,这些方法又为任何 Control 类型的实例调用 SuspendLayoutResumeLayout

在声明性部分,事件被连接到“app”实例。上面的代码说明了如何将“app”实例告知解析器。

此外,解析器可以使用在声明性解析期间实例化的实例初始化字段。在主应用程序中,存在一个这样的字段

[MyXamlAutoInitialize] DockManager dockingManager=null;

MyXamlAutoInitialize 属性告诉解析器,只有使用此属性修饰的字段才期望(并要求)被初始化。

序列化

Traceract 可以保存现有布局并加载它(尽管这似乎仍然有点问题)。布局也序列化为 XML。例如,如果您将布局更改为如下所示(缩小以便适合屏幕截图)

表单布局的序列化(存储在 layout.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>

您会注意到使用 GUID 来确保为新内容窗口创建唯一的名称。

窗口内容的序列化/反序列化很复杂。我使用过的每个停靠管理器都对反序列化顺序及其控制窗口平铺的方式非常挑剔。正如我之前提到的,我将在关于停靠管理器的一般文章中更深入地讨论这个问题。

即将推出的功能

以下是根据时间和需要我计划开发的功能列表。如果您有其他喜欢的功能,请随时告诉我。

  • 改进代码!
  • 用更合适的组件替换 ListView
  • 着色,
  • 更好的换行处理,
  • 选择调试消息时自动将所有窗口定位到相邻的调试消息,
  • 保存/恢复应用程序屏幕位置和大小,
  • 自动重新加载上次配置,
  • MRU 配置列表,
  • 用于保存/加载配置的文件选择,
  • 通过调试消息字符串自动配置选择,
  • 远程调试支持,
  • 复制行/范围到剪贴板,
  • 打印,
  • 附加配置选项,例如始终输出到主调试窗口、换行等,
  • 布局序列化错误修复。

参考文献

条款和条件

Traceract 旨在供您个人使用。您可以根据需要修改它,借用代码等,只要是用于您自己的个人使用。未经我的明确许可,Traceract 或其衍生产品不得作为独立的商业应用程序或作为商业应用程序的一部分分发。Traceract 使用 GPL 许可的 MyXaml 程序集。此处下载中包含的源代码和程序集不授予在商业应用程序中使用 MyXaml 程序集的任何权利。

更新和源代码

您可以获取最新的代码发布、更新的源代码,如果您愿意,可以通过获取我的 CVS 服务器帐户来为项目做出贡献。任何对此感兴趣的人都应联系 Marc Clifton,我将为您设置一个帐户。我也会更新文章下载,但可能不会那么频繁。

© . All rights reserved.