自定义工具和工具箱

你必须为你想要使用户添加到其模型的元素定义工具箱项。 有两种类型的工具:元素工具和连接工具。 在生成的设计器中,用户可以选择元素工具以将形状拖动到关系图中,也可以选择连接工具以在形状之间绘制链接。 通常,元素工具允许用户向其模型添加域类的实例,而连接工具允许他们添加域关系的实例。

如何定义工具箱

在 DSL 资源管理器中,展开“编辑器”节点及其下面的节点。 通常,你将看到如下所示的层次结构:

Editor
     Toolbox Tabs
        MyDsl          //a tab
           Tools
               ExampleElement      // an element tool
               ExampleRelationship // a connection tool

在 DSL 资源管理器的此部分中,你可以执行以下操作:

  • 创建新选项卡。 选项卡定义工具箱中的节标题。

  • 创建新工具。

  • 复制和粘贴工具。

  • 在列表中上移或下移工具。

  • 删除选项卡和工具。

重要

若要在 DSL 资源管理器中添加或粘贴项,请右键单击新节点的祖父级。 例如,若要添加工具,请右键单击选项卡,而不是“工具”节点。 若要添加选项卡,请右键单击“编辑器”节点。

每个工具的“工具箱图标”属性都引用一个 16x16 位图文件。 这些文件通常保留在“Dsl\Resources”文件夹中。

每个元素工具的“类”属性都引用具体域类。 默认情况下,该工具将创建此类的实例。 但是,你可以编写代码以具有用于创建元素组或不同类型的元素的工具。

连接工具的“连接生成器”属性引用连接生成器,该生成器定义工具可连接的元素类型,以及它在元素之间创建的关系。 连接生成器将定义为 DSL 资源管理器中的节点。 在定义域关系时将自动创建连接生成器,但你可以编写代码来自定义它们。

将工具添加到工具箱

  1. 通常在已创建形状类并将其映射到域类后创建元素工具。

    通常在已创建连接符类并将其映射到引用关系后创建连接符工具。

  2. 在 DSL 资源管理器中,展开“编辑器”节点和“工具箱选项卡”节点。

    右键单击“工具箱选项卡”节点,然后单击“添加新元素工具”或“添加新连接工具”。

  3. 将“工具箱图标”属性设置为引用 16x16 位图。

    如果要定义新图标,则在解决方案资源管理器中,在“Dsl\Resources”文件夹中创建一个位图文件。 该文件应具有以下属性值:“生成操作” = “内容” ;“复制到输出目录” = “不复制” 。

  4. 对于元素工具:将工具的“类”属性设置为引用映射到形状的具体域类。

    对于连接符工具:将工具的“连接生成器”属性设置为下拉列表中提供的项之一。 在将连接符映射到域关系后自动创建连接生成器。 如果最近创建过连接符,则通常选择关联的连接生成器。

  5. 若要测试 DSL,请按 F5 或 CTRL+F5,并在 Visual Studio 的实验实例中,打开示例模型文件。 新工具应显示在工具箱上。 将它拖动到关系图上以验证它是否将创建新元素。

    如果该工具未显示,请停止该实验 Visual Studio。 在 Windows 的“开始”菜单中,键入“重置 Visual Studio”,然后运行与 Visual Studio 版本匹配的“重置 Microsoft Visual Studio 试验实例”命令。 在“生成”菜单上,单击“重新生成解决方案” 。 然后,再次测试 DSL。

自定义元素工具

默认情况下,该工具将创建指定类的单个实例,但是可通过两种方式改变这种情况:

  • 在其他类上定义元素合并指令,从而允许它们接受此类的新实例,并允许它们在创建新元素后创建其他链接。 例如,你可以允许用户将“注释”放到其他元素上,从而在两个元素之间创建引用链接。

    这些自定义还将影响当用户粘贴或拖动并放置元素时将发生的情况。

    有关详细信息,请参阅自定义元素的创建和移动

  • 编写代码来自定义工具,以便它可以创建元素组。 该工具由 ToolboxHelper.cs 中可重写的方法进行初始化。 有关详细信息,请参阅从工具创建元素组

从工具创建元素组

每个元素工具都包含它应创建的元素的原型。 默认情况下,每个元素工具都将创建单个元素,但也可以使用一个工具创建一组相关对象。 为此,请使用包含相关项的 ElementGroupPrototype 初始化工具。

以下示例取自 DSL,其中有晶体管类型。 每个晶体管具有三个命名的“终端”。 用于晶体管的元素工具将存储包含四个模型元素和三个关系链接的原型。 当用户将工具拖动到关系图上时,该原型将进行实例化并链接到模型根。

此代码将重写在 Dsl\GeneratedCode\ToolboxHelper.cs 中定义的方法。

有关使用程序代码自定义模型的详细信息,请参阅在程序代码中导航和更新模型

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Diagrams;

  public partial class CircuitsToolboxHelper
  {
    /// <summary>
    /// Toolbox initialization, called for each element tool on the toolbox.
    /// This version deals with each Component subtype separately.
    /// </summary>
    /// <param name="store"></param>
    /// <param name="domainClassId">Identifies the domain class this tool should instantiate.</param>
    /// <returns>prototype of the object or group of objects to be created by tool</returns>
    protected override ElementGroupPrototype CreateElementToolPrototype(Store store, Guid domainClassId)
    {
        if (domainClassId == Transistor.DomainClassId)
        {
            Transistor transistor = new Transistor(store);

            transistor.Base = new ComponentTerminal(store);
            transistor.Collector = new ComponentTerminal(store);
            transistor.Emitter = new ComponentTerminal(store);

            transistor.Base.Name = "base";
            transistor.Collector.Name = "collector";
            transistor.Emitter.Name = "emitter";

            // Create an ElementGroup for the Toolbox.
            ElementGroup elementGroup = new ElementGroup(store.DefaultPartition);
            elementGroup.AddGraph(transistor, true);
            // AddGraph includes the embedded parts

            return elementGroup.CreatePrototype();
        }
        else
        {
            return base.CreateElementToolPrototype(store, domainClassId);
}  }    }

自定义连接工具

通常,在创建新的连接符类后创建元素工具。 或者,可通过允许两个终端的类型确定关系类型来重载一个工具。 例如,可以定义一个可同时创建 Person-Person 关系和 Person-Town 关系的连接工具。

连接工具将调用连接生成器。 使用连接生成器来指定用户在生成的设计器中链接元素的方式。 连接生成器将指定可链接的元素以及在元素之间创建的链接类型。

在域类之间创建引用关系后,将自动创建连接生成器。 可在映射连接工具时使用此连接生成器。 有关如何创建连接工具的详细信息,请参阅配置工具箱

你可以修改默认连接生成器,以便它能够处理不同范围的源和目标类型,以及创建不同类型的关系。

还可为连接生成器编写自定义代码,以便为连接指定源和目标类、定义要进行的连接类型,以及执行与创建连接相关联的其他操作。

连接生成器的结构

连接生成器包含一个或多个链接连接指令,可指定域关系以及源和目标元素。 例如,在任务流解决方案模板中,可在“DSL 资源管理器”中看到 CommentReferencesSubjectsBuilder。 此连接生成器包含一个名为“CommentReferencesSubjects”的链接连接指令,该指令将映射到域关系 CommentReferencesSubjects。 此链接连接指令包含一个指向 Comment 域类的源角色指令,和一个指向 FlowElement 域类的目标角色指令。

使用连接生成器来限制源和目标角色

可使用连接生成器限制使某些类显示在给定域关系的源角色或目标角色中。 例如,你可能有一个与另一个域类具有域关系的基域类,但是你可能不希望该基类的所有派生类在该关系中具有相同角色。 在任务流解决方案中,有四个从抽象域类 FlowElement 直接继承的具体域类(StartPoint、EndPoint、MergeBranch 和 Synchronization),以及两个从其间接继承的具体域类(Task 和 ObjectInState)。 还有一个在其源角色和目标角色中都采用 FlowElement 域类的 Flow 引用关系。 但是,EndPoint 域类的实例不应是 Flow 关系的实例的源,StartPoint 类的实例也不应是 Flow 关系的实例的目标。 FlowBuilder 连接生成器具有一个名为 Flow 的链接连接指令,用于指定可以扮演源角色的域类(Task、MergeBranch、StartPoint 和 Synchronization)以及可以扮演目标角色的域类(MergeBranch、Endpoint 和 Synchronization)。

可向连接生成器添加多个链接连接指令。 这样可帮助你对用户隐藏域模型的某些复杂性,并防止“工具箱”变得过于杂乱。 可将多种不同的域关系的链接连接指令添加到单个连接生成器。 但是,应在域关系执行大致相同的函数时合并域关系。

在任务流解决方案中,Flow 连接工具用于绘制 Flow 和 ObjectFlow 域关系的实例。 除了前面所述的 Flow 链接连接指令,FlowBuilder 连接生成器还有两个名为 ObjectFlow 的链接连接指令。 这些指令指定 ObjectFlow 关系的实例可能在 ObjectInState 域类之间绘制,或从 ObjectInState 的实例绘制到 Task 的实例,但不在 Task 的两个实例之间绘制,也不会从 Task 的实例绘制到 ObjectInState 的实例。 但是,Flow 关系的实例可能在 Task 的两个实例之间绘制。 如果编译并运行任务流解决方案,则可以看到从 ObjectInState 的实例到 Task 的实例绘制 Flow 将创建 ObjectFlow 的实例,但在 Task 的两个实例之间绘制 Flow 将创建 Flow 的实例。

为连接生成器自定义代码

在用户界面中有四个用于定义不同类型的连接生成器的自定义的复选框:

  • 源或目标角色指令上的“自定义接收”复选框

  • 源或目标角色指令上的“自定义连接”复选框

  • 连接指令上的“使用自定义连接”复选框

  • 连接生成器的“是自定义”属性

    你必须提供一些程序代码,才能进行这些自定义。 若要发现你必须提供的代码,请检查这些框之一、单击“转换所有模板”,然后生成解决方案。 将产生一个错误报告。 双击该错误报告以查看注释,该注释解释了应添加的代码。

注意

若要添加自定义代码,请在与 GeneratedCode 文件夹中的代码文件不同的代码文件中创建分部类定义。 为避免丢失工作,不应编辑生成的代码文件。 有关详细信息,请参阅重写和扩展生成的类

创建自定义连接代码

在每个链接连接指令中,“源角色指令”选项卡将定义可从哪些类型进行拖动。 同样,“目标角色指令”选项卡将定义可拖动到哪些类型。 对于每个类型,可进一步指定是否通过设置“自定义接受”标志,然后提供额外代码来允许连接(针对该链接连接指令)。

还可自定义在进行连接时发生的情况。 例如,可自定义仅在特定类上发生来回拖动的情况、一个链接连接指令控制的所有情况,或整个 FlowBuilder 连接生成器。 对于其中每个选项,你可以在相应的级别上设置自定义标志。 在转换所有模板并尝试生成解决方案时,错误消息将使你转到位于生成代码中的注释。 这些注释将标识你必须提供的内容。

在“组件图”示例中,将自定义用于“连接”域关系的连接生成器以限制可在端口之间进行的连接。 下图显示只能从 OutPort 元素到 InPort 元素进行连接,但可将组件互相嵌套在内。

从嵌套组件传入到 OutPort 的连接

Connection Builder

因此,你可能想要指定可从嵌套组件传送到 OutPort 的连接。 若要指定此类连接,请在“DSL 详细信息”窗口中,将“InPort”类型上的“使用自定义接受”设置为源角色,并将“OutPort”类型设置为目标角色,如下图所示:

DSL 资源管理器中的链接连接指令

Connection builder image

“DSL 详细信息”窗口中的链接连接指令

Link connect directive in DSL Details window

然后,必须在 ConnectionBuilder 类中提供方法:

  public partial class ConnectionBuilder
  {
    /// <summary>
    /// OK if this component has children
    /// </summary>
    private static bool CanAcceptInPortAsSource(InPort candidate)
    {
       return candidate.Component.Children.Count > 0;
    }

    /// <summary>
    /// Only if source is on parent of target.
    /// </summary>
    private static bool CanAcceptInPortAndInPortAsSourceAndTarget                (InPort sourceInPort, InPort targetInPort)
    {
      return sourceInPort.Component == targetInPort.Component.Parent;
    }
// And similar for OutPorts...

有关使用程序代码自定义模型的详细信息,请参阅在程序代码中导航和更新模型

例如,可使用相似的代码以防止用户使用父子链接创建循环。 将这些限制视为“硬”约束,因为用户在任何时候都不能违反这些约束。 还可创建“软”验证检查,用户可通过创建无法保存的无效配置来临时绕过这些检查。

定义连接生成器的最佳做法

你应定义一个连接生成器来创建不同类型的关系(仅当它们在概念上相互相关时)。 在任务流示例中,请使用同一个生成器在任务之间以及在任务和对象之间创建流。 但是,使用同一个生成器在注释和任务之间创建关系可能会造成混淆。

如果定义用于多种类型的关系的连接生成器,你应确保它不能从同一对源和目标对象与多种类型相匹配。 否则,结果将不可预知。

可使用自定义代码来应用“硬”约束,但你应该考虑用户是否应能够临时进行无效连接。 如果他们应能够如此,你可以修改约束以使连接不进行验证,直到用户尝试保存更改。