如何:响应 UML 模型中的更改

更新:2011 年 3 月

您可以编写当 Visual Studio 中的 UML 模型发生更改时即执行的代码。 它将以等同方式响应由用户直接进行的更改和由其他 Visual Studio 扩展进行的更改。

警告

UML 扩展 API 不直接支持此功能。 因此,您必须使用略微有别于传统的方法。 本主题中提供了必要的代码。 但是,在某些情况下,可能会产生意外的效果。 另外,将来对 UML 的 Visual Studio 实现进行的更改也可能使本主题中介绍的方法无效。

若要使用本主题中介绍的方法,有必要熟悉可视化和建模 SDK (VMSDK),因为,UML 工具需要通过它来实现。 有关更多信息,请参见 可视化和建模 SDK - 域特定语言

本主题内容:

  • 创建 UML 扩展项目

  • 事件和规则

  • 定义事件

  • 示例:使用事件根据构造型对类进行着色

  • 定义规则

  • 示例:使用规则根据构造型对类进行着色

  • 示例:默认情况下为双向的关联

  • 核心模型和视图模型

创建 UML 扩展项目

在许多情况下,您将向已实现命令或笔势处理程序的扩展添加事件处理程序。 在这种情况下,您可以将此处介绍的代码添加到相同的 Visual Studio 项目。 有关更多信息,请参见如何:在建模图上定义菜单命令如何:在建模图上定义放置和双击处理程序

如果要在单独的 Visual Studio 扩展中创建您自己的事件处理程序,则先创建新的 UML 验证项目。 在**“新建项目”对话框中,单击“建模项目”,然后选择“模型验证扩展”**。 另外,您也可以遵循如何:为 UML 模型定义验证约束中的定义验证扩展这一过程。

您必须将以下引用添加到您的项目:

  • \Program Files\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\Microsoft.VisualStudio.Uml.dll

  • Microsoft.VisualStudio.ArchitectureTools.Extensibility.dll

  • Microsoft.VisualStudio.Modeling.Sdk.10.0dll

  • Microsoft.VisualStudio.Modeling.Sdk.Diagrams.10.0.dll

  • Microsoft.VisualStudio.Uml.Interfaces.dll

事件和规则

VMSDK 提供两种主要方法来检测存储中的更改:

  • 事件处理程序响应发生更改的事务结束后所进行的更改。 事件处理程序通常用于在模型外将更改传播到用户界面、文件或数据库。 您还可以编写对新事务中的模型进行其他更改的事件处理程序。

    规则响应发生更改的事务内所进行的更改。 通常,规则用于在模型内传播更改,以便保持模型两个部分之间的一致性。 您还可以编写防止通过取消事务进行无效更改的规则。

有关事务的更多信息,请参见如何:使用事务链接模型更新

定义事件处理程序

若要在发生更改时调用您的事件处理程序,必须进行注册。 必须针对您要监控的每类元素(如 UseCase 或 Activity)注册该处理程序。 不必针对每个实例进行注册。

下面的示例依据用户应用的构造型设置 UML 类的颜色。 事件处理程序被注册为只要创建或删除构造型实例就触发。 因为本示例使用的是事件处理程序而非规则,所以将在构造型发生更改的事务完成后调用这些处理程序。 因为颜色也是 VMSDK 存储中的一种更改,所以必须在第二个事务中执行。

您还可以使用事件处理程序在存储外执行更改。

您可以采用下面示例的代码来响应您自己选择的事件。 以下是有关此代码的注意事项:

  • 验证 API 用于注册事件处理程序。 验证框架提供了在打开模型时执行代码的便利方式。 但是,该代码实际并不执行验证,并且用户不必调用验证即可执行更新。

  • 事件处理程序是添加到 Store.EventManagerDirectory 的方法。 这是基础 VMSDK (DSL) 实现的 Store,而非 UML ModelStore。 EventManagerDirectory 有一个固定的字典集,适用于不同类型的事件,如 ElementAdded 和 ElementDeleted。

  • 若要注册事件,必须知道您要监控的实现类或关系的名称。 这些类在 Microsoft.VisualStudio.Modeling.Uml.dll 中定义,并且,当在调试器中查看属性时可以看到类名称。 您可以将这些类成员强制转换为适当的接口类型,如 IClass、IStereotype。 有关接口类型的列表,请参见模型元素类型

    实现类名称在将来的版本中可能有所不同。

  • 用户调用**“Undo”“Redo”**命令时会调用事件处理程序。 例如,在 Add 事件之后,Undo 将引发 Delete 事件。 如果您的事件处理程序正在存储外传播更改,则应响应这些事件。 但是,不得为响应 Undo 或 Redo 而在存储内进行更改,也不得在从文件读取模型时进行更改。 您可以使用 if (!store.InUndoRedoOrRollback && !store.InSerializationTransaction)...。

  • 该示例演示在模型中添加和删除对象的事件处理程序。 您还可以针对属性值的更改创建事件处理程序。 有关更多信息,请参见事件处理程序在模型外部传播更改

  • 有关事件的更多详细信息,请参见事件处理程序在模型外部传播更改

示例:使用事件根据构造型对类进行着色

对于本示例,您还必须添加对 System.Drawing.dll 的项目引用

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Drawing;
using System.Linq;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Presentation;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Diagrams;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Uml.AuxiliaryConstructs;
using Microsoft.VisualStudio.Uml.Classes;
using Microsoft.VisualStudio.Uml.Profiles;
using Microsoft.VisualStudio.Uml.UseCases;

using Microsoft.VisualStudio.Uml.ModelStore; // in private assembly. Used for Get|IsElementDefinition()

namespace UmlEvents  // <<<< EDIT
{
/// <summary>
/// Wraps a UML model to add stereotype coloring.
/// </summary>
public partial class ColoringModelAdapter
{
  // This is the underlying DSL store, not the wrapping UML ModelStore:
  private Store store;

  /// <summary>
  /// This isn't actually validation. It's to couple this adapter to the model before we start.
  /// The validation package creates an instance of this class and then calls this method.
  /// See "Validation": https://msdn.microsoft.com/library/bb126413.aspx
  /// </summary>
  /// <param name="vcontext"></param>
  /// <param name="model"></param>
  [Export(typeof(System.Action<ValidationContext, object>))]
  [ValidationMethod(ValidationCategories.Open)]
  public void ConnectAdapterToModel(ValidationContext vcontext, IModel model)
  {
    // This is the underlying DSL store, not the wrapping UML ModelStore:
    store = (model as ModelElement).Store;

    // Add an event that triggers on creating a stereotype instance.
    // See "Event handlers": https://msdn.microsoft.com/library/bb126250.aspx
    DomainClassInfo siClass = store.DomainDataDirectory.FindDomainClass
      ("Microsoft.VisualStudio.Uml.Classes.StereotypeInstance");
    store.EventManagerDirectory.ElementAdded.Add(siClass,
      new EventHandler<ElementAddedEventArgs>(StereotypeInstanceAdded));

    // For the deletion, we need to trigger from the deleted link
    // between the stereotype instance and the model element - 
    // because after deletion we can't find the element from the stereotype instance.
    DomainRelationshipInfo linkToStereotypeClass = store.DomainDataDirectory.FindDomainRelationship
      ("Microsoft.VisualStudio.Uml.Classes.ElementHasAppliedStereotypeInstances");
    store.EventManagerDirectory.ElementDeleted.Add(linkToStereotypeClass,
      new EventHandler<ElementDeletedEventArgs>(StereotypeInstanceDeleted));

    // Add here handlers for other events.
  }

  /// <summary>
  /// Event handler called whenever a stereotype instance is linked to a uml model element.
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void StereotypeInstanceAdded(object sender, ElementAddedEventArgs e)
  {
    // Don't handle changes in undo or load from file:
    if (store.InUndoRedoOrRollback || store.InSerializationTransaction) return;

    IStereotypeInstance si = e.ModelElement as IStereotypeInstance;
    IElement umlElement = si.Element;

     // Work only with the core model, not the views:
     if (!umlElement.IsElementDefinition()) return;

    // I'm only interested in coloring classes and interfaces:
    if (!(umlElement is IType)) return;

    Color? color = ColorForStereotype(si.Name);
    if (color.HasValue)
    {
      SetColorOfShapes(si.Element, color.Value);
    }
  }

  /// <summary>
  /// Called whenever a stereotype instance is deleted - well, actually, 
  /// when the link between the stereotype instance and the uml model element is deleted.
  /// Triggering on the link deletion allows us to get both ends.
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void StereotypeInstanceDeleted(object sender, ElementDeletedEventArgs e)
  {
    // Don't handle changes in undo or load from file:
    if (store.InUndoRedoOrRollback || store.InSerializationTransaction) return;

    // Use the generic link type to avoid unearthing the UML implementation DLL:
    ElementLink elementToStereotypeLink = e.ModelElement as ElementLink;
    IElement umlElement = elementToStereotypeLink.LinkedElements[0] as IElement;
    IStereotypeInstance si = elementToStereotypeLink.LinkedElements[1] as IStereotypeInstance;

     // Work only with the core model, not the views:
     if (!umlElement.IsElementDefinition()) return;

    // We're here either because a stereotype is being un-applied,
    // or because the uml element is being deleted.
    // Don't bother if the element is being deleted:
    if ((umlElement as ModelElement).IsDeleting) return;

    // We're only interested in classes and interfaces:
    if (!(umlElement is IType)) return;

    // Because more than one stereotype can be applied to an element,
    // we should check to see if there are any remaining:
    Color newColor = Color.WhiteSmoke; // Default if there aren't.
    foreach (IStereotypeInstance remainingSi in umlElement.AppliedStereotypes)
    {
      Color? color = ColorForStereotype(remainingSi.Name);
      if (color.HasValue)
      {
        newColor = color.Value;
        break;
      }
    }
    SetColorOfShapes(umlElement, newColor);
  }

  private void SetColorOfShapes(IElement element, Color color)
  {
    foreach (IShape shape in element.Shapes())
    {
      shape.Color = color;
    }
  }

  /// <summary>
  /// This example deals only with a subset of the standard stereotypes.
  /// </summary>
  private Color? ColorForStereotype(string name)
  {
    switch (name)
    {
      case "focus": return Color.AliceBlue;
      case "auxiliary": return Color.Bisque;
      case "specification": return Color.OliveDrab;
      case "realization": return Color.LightSeaGreen;
      case "implementationClass": return Color.PaleGoldenrod;
    }
    return null;
  } 
}}

定义规则

您可以通过定义规则来在 VMSDK 存储中传播更改。 触发更改和规则将在同一事务中执行。 用户调用 Undo 时,将同时撤销这两个更改。

上面示例的缺点是它使用事件处理程序来更改形状的颜色。 颜色也附加于 VMSDK 存储中的字段,因此,其必须在事务中执行。 因此,如果用户在应用构造型后调用**“Undo”命令,则将撤销颜色更改,但构造型仍被应用。 若要逆转构造型的应用,需要执行另一个“Undo”**命令。 在某些情况下,这可能是您想要获得的效果,但如果不是,则可以通过定义规则完全在一个事务内传播更改。

规则对于在存储外传播更改的用处不大,因为当用户执行**“Undo”“Redo”**命令时,不会调用规则。

规则是向规则管理器注册的类。 通常,当您编写 VMSDK 代码时,只要将某一特性附加到该类并将该类纳入到加载扩展代码时会读取的列表中,即注册了规则。 但是,由于 UML 实现已经编译,因此您必须将规则动态添加到规则管理器。 示例中给出的代码很大程度上依赖于当前的实现规则管理,该实现规则管理在将来的版本中可能发生更改。

若要添加规则,您必须知道实现类的名称。 这些名称在将来的版本中可能发生更改。 您应尽可能将元素强制转换为 UML API 类型,如 IClass、IProfile。

示例演示了用于处理 UML 模型中的对象添加和删除事务的规则。 您还可以创建响应对象属性更改的规则。 有关更多信息,请参见规则在模型内部传播更改

示例:使用规则根据构造型对类进行着色

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Drawing;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Presentation;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Diagrams;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Uml.AuxiliaryConstructs;
using Microsoft.VisualStudio.Uml.Classes;
using Microsoft.VisualStudio.Uml.UseCases; 

using Microsoft.VisualStudio.Uml.ModelStore; // in private assembly. Used for Get|IsElementDefinition()


namespace UmlRules
{
  class ColorByStereotype
  {
    /// <summary>
    /// Singleton wrappers: one per model.
    /// </summary>
    private static Dictionary<IPackage, ColorByStereotype > modelAdapters = 
        new Dictionary<IPackage, ColorByStereotype >();

    private class Wrapper
    {
      /// <summary>
      /// This isn't actually validation. 
      /// It sets up some store rules.
      /// The validation package creates an instance of this class and then calls this method.
      /// See "Validation": https://msdn.microsoft.com/library/bb126413.aspx
      /// </summary>
      /// <param name="vcontext"></param>
      /// <param name="model"></param>
      [Export(typeof(System.Action<ValidationContext, object>))]
      [ValidationMethod(ValidationCategories.Open)]
      private void ConnectAdapterToModel(ValidationContext vcontext, IModel model)
      {
        modelAdapters.Add(model, new ColorByStereotype (model));
      }
    }

    private IModel model;
    private Store store;
    private ColorByStereotype (IModel model)
    {
      this.model = model;
      // This is the underlying DSL store, not the wrapping UML ModelStore:
      store = (model as ModelElement).Store;

      SetRule<StereotypeInstanceAddedRule>(
        store.DomainDataDirectory.FindDomainClass(
          "Microsoft.VisualStudio.Uml.Classes.StereotypeInstance"));
      
      // For the deletion, we need to trigger from the deleted link
      // between the stereotype instance and the model element - 
      // because after deletion we can't find the element from the stereotype instance.
      
      SetRule<StereotypeInstanceDeletedRule>(
        store.DomainDataDirectory.FindDomainRelationship(
        "Microsoft.VisualStudio.Uml.Classes.ElementHasAppliedStereotypeInstances"));
    }

    /// <summary>
    /// Register a rule. 
    /// Normally, you set a rule by prefixing the rule class with 
    /// [RuleOn(typeof(TargetClass))]
    /// but we are setting up the rule at runtime, so must add
    /// the rules to the relevant dictionaries.
    /// </summary>
    /// <typeparam name="T">Rule class</typeparam>
    /// <param name="classInfo">Class or relationship to which to attach the rule.</param>
    private void SetRule<T>(DomainClassInfo classInfo) where T : Rule, new()
    {
      T rule = new T();
      rule.FireTime = TimeToFire.TopLevelCommit;

      System.Type tt = typeof(T);
      string ruleSet = (typeof(AddRule).IsAssignableFrom(tt)) ? "AddRules" :
        (typeof(ChangeRule).IsAssignableFrom(tt)) ? "ChangeRules" :
        (typeof(DeleteRule).IsAssignableFrom(tt)) ? "DeleteRules" :
        (typeof(DeletingRule).IsAssignableFrom(tt)) ? "DeletingRules" : "";

      // The rest of this method uses reflection to achieve the following:
      // store.RuleManager.RegisterRule(rule);
      // classInfo.AddRules.Add(rule);

      System.Reflection.BindingFlags privateBinding = 
          System.Reflection.BindingFlags.Instance 
        | System.Reflection.BindingFlags.NonPublic;
      System.Reflection.MethodInfo mi = 
        typeof(RuleManager).GetMethod("RegisterRule", privateBinding);
      mi.Invoke(store.RuleManager, new object[] { rule });

      store.RuleManager.EnableRule(typeof(T));

      System.Reflection.PropertyInfo pi = 
        typeof(DomainClassInfo).GetProperty(ruleSet, privateBinding);
      dynamic rules = pi.GetValue(classInfo, null);
      System.Type ruleListType = rules.GetType();
      System.Reflection.FieldInfo listpi = 
        ruleListType.GetField("list", privateBinding);
      dynamic list = listpi.GetValue(rules);
      System.Type listType = list.GetType();
      System.Reflection.MethodInfo addmi = listType.GetMethod("Add");
      addmi.Invoke(list, new object[] { rule });


      System.Reflection.MethodInfo resetRulesCache = 
        typeof(DomainClassInfo).GetMethod("ResetRulesCache", privateBinding);
      resetRulesCache.Invoke(classInfo, null);

    }

    #region Rules.
    private class StereotypeInstanceAddedRule : AddRule
    {
      public override void ElementAdded(ElementAddedEventArgs e)
      {
        base.ElementAdded(e);
        Store store = e.ModelElement.Store;
        // Don't handle load from file:
        if (store.InSerializationTransaction)
          return;

        IStereotypeInstance si = e.ModelElement as IStereotypeInstance;
        IElement umlElement = si.Element;
        
         // Work only with the core model, not the views:
         if (!umlElement.IsElementDefinition()) return;

        // I'm only interested in coloring classes and interfaces:
        if (!(umlElement is IType)) return;

        Color? color = ColorForStereotype(si.Name);
        if (color.HasValue)
        {
          SetColorOfShapes(si.Element, color.Value);
        }
      }
    }
    private class StereotypeInstanceDeletedRule : DeleteRule
    {
      public override void ElementDeleted(ElementDeletedEventArgs e)
      {
        base.ElementDeleted(e);
        Store store = e.ModelElement.Store;


        // Use the generic link type to avoid using the UML implementation DLL:
        ElementLink elementToStereotypeLink = e.ModelElement as ElementLink;
        IElement umlElement = elementToStereotypeLink.LinkedElements[0] as IElement;

         // Work only with the core model, not the views:
         if (!umlElement.IsElementDefinition()) return;

        // We're here either because a stereotype is being un-applied,
        // or because the uml element is being deleted.
        // Don't bother if the element is being deleted:
        if ((umlElement as ModelElement).IsDeleting) return;

        // We're only interested in classes and interfaces:
        if (!(umlElement is IType)) return;

        // Because more than one stereotype can be applied to an element,
        // we should check to see if there are any remaining:
        Color newColor = Color.WhiteSmoke; // Default if there aren't.
        foreach (IStereotypeInstance remainingSi in umlElement.AppliedStereotypes)
        {
          Color? color = ColorForStereotype(remainingSi.Name);
          if (color.HasValue)
          {
            newColor = color.Value;
            break;
          }
        }
        SetColorOfShapes(umlElement, newColor);
      }
    }

    /// <summary>
    /// Set the color of the shapes that display an element.
    /// </summary>
    /// <param name="element"></param>
    /// <param name="color"></param>
    private static void SetColorOfShapes(IElement element, Color color)
    {
      foreach (IShape shape in element.Shapes())
      {
        shape.Color = color;
      }
    }

    /// <summary>
    /// For this sample, we just deal with some of the standard stereotypes.
    /// </summary>
    /// <param name="name">Stereotype name</param>
    /// <returns></returns>
    private static Color? ColorForStereotype(string name)
    {
      switch (name)
      {
        case "focus": return Color.AliceBlue;
        case "auxiliary": return Color.Bisque;
        case "specification": return Color.OliveDrab;
        case "realization": return Color.LightSeaGreen;
        case "implementationClass": return Color.PaleGoldenrod;
      }
      return null;
    }
    #endregion
  }
}

示例:默认情况下为双向的关联

默认情况下,当您在类图中绘制关联时,新关联只可沿一个方向导航。 其一端为箭头。 就某些方面而言,绘制不带箭头的双向关联更为方便。 您可以通过添加下面的规则使双向关联成为默认设置。

/// <summary>
/// Rule invoked when an Association is created.
/// This rule sets both ends navigable, which is convenient for representing requirements.
/// </summary>
private class AssociationAddRule : AddRule
{
  public override void ElementAdded(ElementAddedEventArgs e)
  {
    Store store = e.ModelElement.Store;
    IAssociation association = e.ModelElement as IAssociation;

    // Do not apply the rule if we are reading from file or undoing a deletion:
    if (association.MemberEnds.Count() == 0 
       || store.InSerializationTransaction || store.InUndoRedoOrRollback) return;

    // Do not apply the rule unless a containing package or model has imported 
    // a profile that defines the stereotype "Default Binary Associations" for associations:
    // if (!association.ApplicableStereotypes.Any
    //      (s => s.DisplayName == "Default Binary Associations")) return;

    // Don’t apply the rule to use cases:
    if (!(association.SourceElement is IUseCase && association.TargetElement is IUseCase))
    {
      association.OwnedEnds.First().SetNavigable(true);
      association.OwnedEnds.Last().SetNavigable(true);
    }
  }
}

若要注册规则,您必须使用定义规则中介绍的 SetRule 方法。

SetRule<AssociationAddRule>(store.DomainDataDirectory.
      FindDomainRelationship("Microsoft.VisualStudio.Uml.Classes.Association"));

如果您希望能够启用或禁用此规则,其中一种方法就是定义可在其中定义特定构造型的配置文件。 您可以通过将代码添加到您的规则来验证是否已在包含程序包或模型中启用了配置文件。 有关更多信息,请参见如何:定义用于扩展 UML 的配置文件

核心模型和视图模型

UML 模型由多个 VMSDK (DSL) 模型组成:

  • 核心模型包含 UML 模型中所有元素的表现形式。 用户可以在“UML 模型资源管理器”窗口中看到这些元素,而您则可以通过 ModelStore API 访问这些元素。 核心模型占据一个 VMSDK 存储分区。

  • UML 项目中的每个 UML 关系图都有一个视图模型。 各视图模型中的对象都是核心模型中各对象的代理。 UML 关系图上显示的每个元素都有一个视图模型对象。 每个视图模型占据一个 VMSDK 存储分区。

  • 关系图上显示的每个元素都有一个 VMSDK 形状对象。 视图模型元素与形状之间存在 1:1 的关系。

您定义了规则或事件处理程序后,在核心对象和视图对象都发生更改时会调用它们。 您应仅处理对核心对象进行的更改。 示例中的处理程序使用 element.IsElementDefinition() 来确定是否处理核心对象。

若要使用此方法,您必须添加对以下项的项目引用:

%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies\Microsoft.VisualStudio.Uml.dll

警告

在私有程序集中定义的 IsElementDefinition 和其他方法可能会在将来版本中发生更改。

using Microsoft.VisualStudio.Uml.ModelStore; 
  // in private assembly. Used for GetElementDefinition()
...
  // Determine whether an element is view or core:
  if (anElement.IsElementDefinition()) 
  { /* core */ }
  else
  { /* view */ }
...
  // If shapeElement is a shape on a diagram -
  // The VMSDK element connected to the shape is in the view:
  IElement viewModelElement = shapeElement.ModelElement as IElement;
  // Get the core element for which the view is a proxy:
  IElement coreModelElement = viewModelElement.GetElementDefinition();
...

请参见

任务

事件处理程序在模型外部传播更改

概念

如何:导航 UML 模型

其他资源

Sample: Color by Stereotype(示例:根据构造型设置颜色)

修订记录

日期

修订记录

原因

2011 年 3 月

创建了主题。

客户反馈