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

动态序列图可视化控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (37投票s)

2020年2月10日

CPOL

13分钟阅读

viewsIcon

26493

downloadIcon

1114

用于动态可视化UML序列图的Windows .NET控件

引言

序列图是UML图的基本类型之一。它们侧重于对系统动态的建模。它们允许随时间描述系统与环境的参与者之间的交互,或系统内部参与者之间的交互。其清晰的图形布局有助于快速直观地理解系统的行为。

本文介绍了一个Windows .NET控件,用于动态可视化对象及其交互作为序列图。该控件包含了类似调试器的中断功能。此外,它提供了一个实际应用示例,其中应用程序的执行被拦截并实时可视化。

为什么要使用它?

以下是该控件可能派上用场的一些场景:

分布式系统的端到端跟踪

您有一个由不同计算机上的独立组件(如微服务)组成的分布式应用程序,并且在跟踪整个执行过程时遇到困难。您可以将每个微服务的日志事件转发到一个收集器,以便在单个序列图中可视化整个流程,从而更轻松地进行分析和调试。

该控件最初是为这种情况创建的,并且是其中的一部分。为了通用性,它被重新打包了。

调试器

您已经开发了一个软件工具,该工具通过使用AOP(面向切面编程)技术拦截任何.NET应用程序的方法调用,现在您想将捕获的调用跟踪可视化为进程图。

在文章接近结尾处,我将提供一个用于此类用法的概念验证。

背景

序列图显示了对象行为。因此,时间概念以及对象之间的依赖关系都会出现在序列图中。这反过来又使得序列图能够显示系统中“发生了什么”。

目前大多数序列图的用法仅限于静态模型:图基于模型定义或静态代码。它们充当流程的蓝图,用于沟通系统内的交互。

但序列图也可以用于可视化系统的实时活动。这种活动可能是计算机程序中实时的调用、分布式应用程序组件内的通信等等。动态分析与静态分析的区别在于,前者需要一个活动的系统来进行建模。也就是说,动态分析是在正在运行的系统上执行的,而静态分析则是在系统构件(例如源代码)上执行的。

Using the Code

示例

SequenceDiagram解决方案包含两个项目:SequenceDiagramLib包含实际控件,而SequenceDiagramTestApp项目包含演示控件功能的各种示例。

除了一个示例外,所有示例都在主线程上完全运行。底部的“基本示例(多线程)”示例适用于在不同线程中修改序列的情况。

下面是一个简单的序列图定义,涉及两个参与者以及它们之间的消息传递。

Sequence sequence = this.sequenceControl.Sequence;

Participant alice = sequence.Participants.Create("Alice");
Participant bob = sequence.Participants.Create("Bob");
sequence.Messages.Add("AuthenticationRequest", alice, bob);
sequence.Tick();

sequence.Messages.Add("AuthenticationResponse", bob, alice, dashStyle: DashStyle.Dash);
sequence.Tick();

sequence.Messages.Add("Another authentication request", alice, bob);
sequence.Tick();

sequence.Messages.Add("Another authentication Response", bob, alice);
sequence.Tick();

此序列由SequenceDiagramControl渲染为

该控件通过水平线表示时间的流逝。每条水平线代表一个时间点,而当前时间点则表示为一条红线。

我们SequenceDiagramControl的第二个、更复杂的序列图定义。

Sequence sequence = this.sequenceControl.Sequence;

Participant user = sequence.Participants.Create("User");
Participant a = sequence.Participants.Create("A");
Participant b = sequence.Participants.Create("B");
Participant c = sequence.Participants.Create("C");
sequence.Messages.Add("DoWork", user, a);
a.Activate();
sequence.Tick();

sequence.Messages.Add("<< createRequest >>", a, b);
b.Activate();
sequence.Tick();

sequence.Messages.Add("DoWork", b, c);
c.Activate();
sequence.Tick();

sequence.Messages.Add("WorkDone", c, b, dashStyle: DashStyle.Dot);
c.Deactivate();
c.Destroy();
sequence.Tick();

sequence.Messages.Add("RequestCreated", b, a, dashStyle: DashStyle.Dot);
b.Deactivate();
sequence.Tick();

sequence.Messages.Add("Done", a, user);
a.Deactivate();
sequence.Tick();

以及SequenceDiagramControl的渲染结果。

如何使用控件

  1. 创建一个名为Form1的Windows窗体。
  2. 向窗体添加一个名为sequenceDiagramSequenceDiagramControl
  3. 向窗体添加两个按钮。将它们命名为runButtoncontinueButton
  4. 为这两个按钮设置事件处理程序。

    runButton的事件处理程序创建一个具有两个参与者和两个时间步的序列。

    private void runButton_Click(object sender, EventArgs e)
    {
        Sequence sequence = this.sequenceDiagram.Sequence;
    
        Participant a = sequence.Participants.CreateOrGet("A");
        Participant b = sequence.Participants.CreateOrGet("B");
        sequence.Messages.Add("Create request", a, b);
        sequence.Tick();
    
        sequence.Messages.Add("Return", b, a);
        sequence.Tick();
    }

    当序列处于等待状态时,continueButton的事件处理程序可用。它的功能是恢复应用程序的执行。

    private void continueButton_Click(object sender, EventArgs e)
    {
        Sequence sequence = this.sequenceDiagram.Sequence;
        sequence.Continue();
    }
  5. 定义序列的OnEnter()OnExit()事件处理程序。

    序列在中断开始时调用OnEnterBreak()事件处理程序,在执行即将恢复时调用OnExitBreak()事件处理程序。

    private void Sequence_OnEnterBreak()
    {
        this.runButton.Enabled = false;
        this.continueButton.Enabled = true;
    }
    
    private void Sequence_OnExitBreak()
    {
        this.runButton.Enabled = true;
        this.continueButton.Enabled = false;
    }
  6. 窗体的构造函数初始化两个按钮的启用/禁用状态并设置序列事件处理程序。
    public Form1()
    {
        InitializeComponent();
    
        this.runButton.Enabled = true;
        this.continueButton.Enabled = false;
    
        Sequence sequence = this.sequenceDiagram.Sequence;
        sequence.OnEnterBreak += Sequence_OnEnterBreak;
        sequence.OnExitBreak += Sequence_OnExitBreak;
    }

工作原理

该控件包含对Sequence类的引用,该类封装了序列的全部信息。参与者、激活、消息、时间步等元素都存储在该类的实例中。

Tick() 方法

在同一时间范围内发生的事件以任意顺序添加到序列中。sequence.Tick()调用尤为重要:正是在这些调用中,

  1. 发生了一个中断事件,SequenceDiagramControl会暂停程序执行,直到用户采取行动恢复。
  2. 序列的逻辑时钟增加1

断点功能的实现细节如下:

  1. 每当调用sequence.Tick()方法时,底层的Sequence对象就会调用其注册的OnEnterBreak()处理程序。这使得其父窗体能够执行诸如启用continueButton等任务。
  2. 序列通过此方法保持在等待状态。
    private void ResponsiveWait()
    {
        this.wait = true;
    
        for (;;)
        {
            if (!this.wait)
                break;
    
            if (this.exit)
                break;
    
            System.Windows.Forms.Application.DoEvents();
                System.Threading.Thread.Sleep(200);
        }
    }

Application.DoEvent()允许在不阻塞自身或其所在应用程序的情况下暂停执行流程。Application.DoEvents()的使用不被推荐,倾向于使用其他技术,如线程。但对于这个项目的需求(例如对被调试应用程序的最小干扰),我发现它是最合适的方法。

  1. 当用户单击continueButton时,将调用sequence.Continue()方法,等待状态结束。
  2. Sequence类调用注册的OnExitBreak()事件处理程序。这使得其父窗体能够执行诸如禁用continueButton等任务,因为执行将继续直到下一个中断。

元素的渲染

元素的视觉渲染发生在SequenceDiagramControl类中。每个UML元素都有一个方法和一个名为p0的参照点变量。

API 参考

Sequence类是SequenceDiagramControl数据模型的基础对象。关于序列的所有信息都存储在这个类中。它包含可用的参与者、激活、消息和框的集合。

Box是用于对相关参与者进行分组的可选元素。

Participant拥有一个激活的集合,每个激活都有一个名为tag的名称/值对集合。参与者通过Message进行通信。

序列

Sequence类封装了序列中的所有数据。它侧重于消息在参与者之间的时序或时间顺序,以及消息的发送顺序。序列的重点是先发生什么,然后发生什么,依此类推。

API 用法


sequence.Clear();

清除当前状态和序列的所有成员。序列返回到其初始状态。


sequence.Tick();

序列图的重点是可视化系统的变化以及元素随时间通信。X轴显示序列的成员(称为参与者),Y轴代表时间。

SequenceDiagramControl模型中,时间点被表示为离散值。每当发生时间点时,序列就会前进到下一个时间步值。时间点是发生中断并可供用户响应的点。


参与者

参与者是序列中代表或参与的对象。它们通常放置在图的顶部。参与者通常表示为矩形,其名称放置在框内。根据UML规范,此名称可以加下划线,表示参与者代表序列图中类的特定实例。UML样板等其他信息也可以包含在参与者矩形中。

参与者的生命线显示为从参与者底部开始的垂直虚线。它们代表参与者随时间的生活和交互。

Participant foo1 = sequence.Participants.Create("Foo1", type: EParticipantType.Actor);
Participant foo2 = sequence.Participants.Create("Foo2", type: EParticipantType.Boundary);
Participant foo3 = sequence.Participants.Create("Foo3", type: EParticipantType.Control);
Participant foo4 = sequence.Participants.Create("Foo4", type: EParticipantType.Entity);
Participant foo5 = sequence.Participants.Create("Foo5", type: EParticipantType.Database);
Participant foo6 = sequence.Participants.Create("Foo6", type: EParticipantType.Collections);
sequence.Messages.Add("To boundary", foo1, foo2);
sequence.Tick();

sequence.Messages.Add("To control", foo1, foo3);
sequence.Tick();

sequence.Messages.Add("To entity", foo1, foo4);
sequence.Tick();

sequence.Messages.Add("To database", foo1, foo5);
sequence.Tick();

sequence.Messages.Add("To collections", foo1, foo6);
sequence.Tick();

API 用法

Participant participant = sequence.Participants.Create(string name, bool underlined = false, 
Color? color = null, Color? textColor = null, EParticipantType? type = null, Box box = null, 
bool createNow = false);

它创建一个新的参与者并将其放置在序列中。如果序列已具有指定名称的参与者,则调用失败。

  • name:参与者姓名。
  • underlined:参与者姓名是否显示为下划线文本。UML中的下划线文本表示类的特定实例。
  • color:参与者的背景颜色。
  • textColor:参与者的文本颜色。
  • type:参与者类型。它会影响参与者的显示方式(框、边界、控制、实体、数据库、集合)。
  • box:参与者所属的框。此值可能为null
  • createNow:通常省略此值,导致参与者位于图的顶部。设置此值会强调参与者正在创建,其生命周期从当前时间步开始。

Participant participant = sequence.Participants.CreateOrGet
(string name, bool underlined = false, Color? color = null, Color? textColor = null, 
EParticipantType? type = null, Box box = null, bool createNow = false);

如果具有给定名称的参与者存在,则返回该参与者。

如果具有给定名称的参与者不存在,则创建一个新参与者,将其放置在序列中,然后返回它。

  • name:参与者姓名。
  • underlined:参与者姓名是否显示为下划线文本。UML图中的下划线文本表示类的特定实例。
  • color:参与者的背景颜色。
  • textColor:参与者的文本颜色。
  • type:参与者类型。它会影响参与者的显示方式(框、边界、控制、实体、数据库、集合)。
  • box:参与者所属的框。此值可能为null
  • createNow:省略此值,导致参与者位于图的顶部。设置此值会强调参与者正在创建,其生命周期从当前时间步开始。

Participant participant = sequence.Participants[name];

返回具有给定名称的参与者。如果具有给定名称的参与者不存在,则调用失败。

  • name:参与者姓名。

participant.Destroy();

结束参与者的当前激活。


消息

消息表示在两个参与者之间传输的信息。参与者有可能向自己发送消息。消息可以是同步的或异步的,它们可能反映操作的开始和执行,或信号的发送和接收。

Participant bob = sequence.Participants.Create("Bob");
Participant alice = sequence.Participants.Create("Alice");
sequence.Messages.Add("hello", bob, alice, color: Color.Red);
sequence.Tick();

sequence.Messages.Add("ok", alice, bob, color: Color.Blue);
sequence.Tick();
Participant alice = sequence.Participants.Create("Alice");
sequence.Messages.Add("signal to self", alice);
sequence.Tick();

API 用法

Message message = sequence.Messages.Add(string name, Participant from, 
Participant to, Color? color = null, DashStyle? dashStyle = null);

在源参与者和目标参与者之间创建一条新消息。

  • name:消息名称。
  • from:消息的源参与者。
  • to:消息的目标参与者。
  • color:消息的渲染颜色。
  • arrowHead:消息的箭头。不同的箭头表示不同类型的消息。
  • dashStyle:消息线的虚线样式。不同的虚线样式表示不同类型的消息。

Message message = sequence.Messages.Add(string name, Participant self, 
Color? color = null, DashStyle? dashStyle = null);

创建一条新的自消息。参与者向自己发送消息。

  • name:消息名称。
  • self:消息的所有者参与者。
  • color:消息的渲染颜色。
  • arrowHead:消息的箭头。不同的箭头表示不同类型的消息。
  • dashStyle:消息线的虚线样式。不同的虚线样式表示不同类型的消息。

激活

激活(也称为控制元素/执行发生点)是参与者执行操作的期间。对象忙于执行进程或等待回复的时间表示为其生命线上垂直放置的矩形。矩形的顶部和底部分别与启动时间和完成时间对齐。激活可以是递归的。

Participant user = this.sequence.Participants.Create("User");
Participant a = this.sequence.Participants.Create("A");
Participant b = this.sequence.Participants.Create("B");
sequence.Messages.Add("DoWork", user, a);
a.Activate(color: Color.FromArgb(0xff, 0xbb, 0xbb));
sequence.Tick();

sequence.Messages.Add("Internal call", a);
a.Activate(color: Color.DarkSalmon);
sequence.Tick();

sequence.Messages.Add("<< createRequest >>", a, b);
b.Activate();
sequence.Tick();

sequence.Messages.Add("RequestCreated", b, a, dashStyle: DashStyle.Dash);
b.Deactivate();
a.Deactivate();
sequence.Tick();

sequence.Messages.Add("Done", a, user);
a.Deactivate();
sequence.Tick();

API 用法

participant.Activate(string name = null, Color? color = null);

为参与者启动激活。激活可以是递归的。

  • name:激活名称。
  • color:激活的背景颜色。

participant.Deactive();

取消激活参与者的当前激活。


框有助于组织序列图。相关参与者可以分组在一个框内。

Box box = sequence.Boxes.Create("Internal Service");
box.Color = Color.LightBlue;
Participant bob = sequence.Participants.Create("bob", box: box);
Participant alice = sequence.Participants.Create("alice", box: box);
Participant other = sequence.Participants.Create("other");
sequence.Messages.Add("hello", bob, alice);
sequence.Tick();

sequence.Messages.Add("hello", alice, other);
sequence.Tick();

API 用法

Box box = sequence.Boxes.Create(string name, Color? color = null);

它创建一个新框并将其放置在序列中。如果序列已具有指定名称的框,则调用失败。

  • name:框名称。
  • color:框的背景颜色。

Box box = sequence.Boxes.CreateOrGet(string name, Color? color = null);

如果具有给定名称的参与者存在,则返回该参与者。

如果具有给定名称的参与者不存在,则创建一个新参与者,将其放置在序列中,然后返回它。

  • name:框名称。
  • color:框的背景颜色。

Box box = sequence.Boxes[name];

返回具有给定名称的框。如果具有给定名称的框不存在,则调用失败。

  • name:参与者姓名。

SequenceDiagramControl + AOP库 = 一个调试器

SequenceDiagramDebugger不是SequenceDiagramControl的一部分。相反,它是一个用于演示SequenceDiagramControl具体、真实用法的扩展。此外,对现有的AOP(面向切面编程)示例存在持续的批评,即它们几乎都涉及方法调用的日志记录或访问控制。在本例中,AOP用于一个替代需求:执行可视化。

调试器示例作为概念验证,并存在局限性,例如仅支持使用单个线程的应用程序。

AOP允许拦截应用程序的方法。然后,这些被拦截的方法调用由SequenceDiagramControl可视化。通过控件附加的内置中断功能,这使得可以调试应用程序的执行流程。

应用程序与序列图元素之间的映射实现如下:

  • 应用程序 -> 序列
  • 应用程序类 -> 参与者
  • 类方法持续时间 -> 激活
  • 方法调用 -> 消息

一个重要目标是实现以对宿主应用程序最小的更改进行调试。这通过向被调试应用程序添加一行代码来实现,以便我们的工具可以与之集成。

public MainForm()
{
    InitializeComponent();

    SequenceDiagramDebugger.Init(this, "TestApp.");
}

SequenceDiagramDebugger.Init(this, "TestApp");有两个目的:

  1. 它织入“TestApp”命名空间中被调试应用程序的所有方法。
  2. 它打开我们的DebuggerForm,其中包含一个SequenceDiagramControl

在执行过程中,AOP库通过通知者告知我们每一次相关的方法进入和退出事件。这些被拦截的方法进入和退出然后根据SequenceDiagramControlSequenceDiagramDebugger中进行可视化。

在示例中,您只需单击TestApp的“Calculate”按钮一次,然后单击DebuggerForm的“Continue”按钮,直到执行结束。

 

关注点

未实现的序列图功能

UML序列图规范的某些部分(例如表示循环、分支和替代执行流程的元素)只有在图用于记录流程时才有意义。这些功能在SequenceDiagramControl中被省略了,因为该控件侧重于对已发生事件的动态可视化,而不是对可能发生的事件。

PlantUML

PlantUML是一个开源工具,它有助于从符合其图表定义语法的文本文件中创建各种UML和非UML图表。您以该工具的文本格式指定UML图定义,与SequenceDiagramControl不同,它创建图表的静态图像表示。它支持序列图、用例图、类图、活动图、组件图、状态图、对象图、部署图和时序UML图,以及许多非UML图。

我长期以来一直使用PlantUML工具进行技术文档编写,并且对其易于使用的术语印象深刻。因此,我的设计目标之一是尽可能遵循PlantUML的元素命名,并比较PlantUML和SequenceDiagramControl对同一序列的渲染。我提供的所有功能示例都在PlantUml工具序列图创建页面上有一个对应的示例。

在本节中,我将提供我在文章开头描述的两个功能示例的PlantUML文本定义等效项及其输出,以便您可以比较它们的异同。

第一个功能示例的PlantUML文本定义等效项。

@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response

Alice -> Bob: Another authentication Request
Alice <-- Bob: Another authentication Response
@enduml

当您将此文本输入PlantUML解析器时,您将获得以下(静态)序列图:

这是我上面描述的第二个功能示例的PlantUML等效项。

@startuml
participant User

User -> A: DoWork
activate A

A -> B: << createRequest >>
activate B

B -> C: DoWork
activate C
C --> B: WorkDone
destroy C

B --> A: RequestCreated
deactivate B

A -> User: Done
deactivate A

@enduml

PlantUML输出是:

NConcern和CNeptune

NConcernCNeptune是在SequenceDiagramDebugger示例中用于拦截外部应用程序方法调用的AOP库。这些捕获的方法调用在SequenceDiagramControl中可视化。这些库坚固且设计良好:我能够以最小的努力集成它们。

NConcern提供了AOP(如通知者)的功能和API。

CNeptune是基于mono.cecil的实用工具,用于重写.NET程序集,使其可注入。

历史

  • 1.0版(2020年2月10日):初始发布

致谢

我想对Yunus Emre Selcuk副教授表示特别感谢,他给了我参与这个控件所在项目的机会。没有他的贡献,就不可能将这里的一切整合在一起。

© . All rights reserved.