Sdílet prostřednictvím


How to Respond to Changes in a UML Model

You can write code that is executed whenever a change occurs in a UML model in Visual Studio. It will respond equally to changes that are made directly by the user, and by other Visual Studio extensions.

Upozornění

This feature is not directly supported by the UML Extension API. Therefore, you have to use slightly unconventional techniques. The necessary code is provided in this topic. However, it is possible that unexpected effects might occur in some cases. It is also possible that future changes in the Visual Studio implementation of UML will invalidate the techniques described in this topic.

To use the techniques described in this topic, it is useful to be familiar with the Visualization and Modeling SDK (VMSDK), with which the UML tools are implemented. For more information, see Visualization and Modeling SDK - Domain-Specific Languages.

In this topic:

  • Creating a UML Extension Project

  • Events and Rules

  • Defining Events

  • Example: Coloring Classes from Stereotypes by using Events

  • Defining Rules

  • Example: Coloring Classes from Stereotypes by using Rules

  • Example: Associations that are Bidirectional by Default

  • Core and View Models

Creating a UML Extension Project

In many cases, you will add an event handler to an extension that already implements a command or gesture handler. In that case, you can add the code described here to the same Visual Studio project. For more information, see How to: Define a Menu Command on a Modeling Diagram and How to: Define a Drop and Double-Click Handler on a Modeling Diagram.

If you want to create your event handler in a separate Visual Studio Extension, start by creating a new UML validation project. In the New Project dialog, click Modeling Projects, and then select Model Validation Extension. Alternatively, you can follow the procedure Defining a Validation Extension in How to: Define Validation Constraints for UML Models.

You must add the following references to your project:

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

Events and Rules

VMSDK provides two principal methods of detecting changes in the store:

  • An event handler responds to a change after the end of the transaction in which the change occurred. Event handlers are typically used to propagate changes outside the model, to user interfaces, files, or databases. You can also write an event handler that makes an additional change to the model in a new transaction.

    A rule responds to a change within the transaction in which the change occurred. Typically, rules are used to propagate changes inside the model, so as to maintain consistency between two parts of the model. You can also write a rule that prevents an invalid change by cancelling the transaction.

For more information about transactions, see How to: Link Model Updates using Transactions.

Defining Event Handlers

To have your event handler invoked when a change occurs, you must register it. You must register the handler for each class of element that you want to monitor, such as UseCase or Activity. You do not have to register it for each instance.

The example that follows sets the color of a UML class according to the stereotype that the user applies. The event handlers are registered to be triggered whenever a stereotype instance is created or deleted. Because this example uses event handlers, and not rules, the handlers are called after the completion of the transaction in which the stereotype is changed. Because the color is also a change in the VMSDK Store, it must be performed in a second transaction.

You could also use event handlers to perform changes outside the Store.

You can adapt the code of the following example to respond to events of your own choice. The important points to notice about this code are as follows:

  • The Validation API is used to register the event handlers. The validation framework provides a convenient way to execute code when the model is opened. But the code does not actually perform validation, and the user does not have to invoke validation in order to perform updates.

  • Event handlers are methods that are added to Store.EventManagerDirectory. This is the Store of the underlying VMSDK (DSL) implementation, not the UML ModelStore. EventManagerDirectory has a fixed set of dictionaries for different types of events, such as ElementAdded and ElementDeleted.

  • To register an event, you must know the name of the implementation class or relationship that you want to monitor. These classes are defined in Microsoft.VisualStudio.Modeling.Uml.dll, and you can see the class names when you watch properties in the debugger. You can cast these class members to the appropriate interface types such as IClass, IStereotype. For a list of interface types, see Model Element Types.

    The implementation class names might be different in future releases.

  • Event handlers are called when the user invokes the Undo and Redo commands. For example, after an Add event, Undo will raise a Delete event. Your event handler should respond to these events if it is propagating changes outside the Store. But it should not make changes inside the Store in response to Undo or Redo, and should not make changes when the model is being read from file. You can use if (!store.InUndoRedoOrRollback && !store.InSerializationTransaction)....

  • The example shows event handlers for adding and deleting objects in the model. You can also create event handlers for changes in property values. For more information, see Event Handlers Propagate Changes Outside the Model.

  • For more detailed information about events, see Event Handlers Propagate Changes Outside the Model.

Example: Coloring Classes by Stereotype by using Events

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  

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

Defining Rules

You can define a rule to propagate a change within the VMSDK Store. Both the triggering change and the rule are performed within the same transaction. When the user invokes Undo, both changes are undone together.

A drawback of the previous example is that it uses event handlers to change the color of the shapes. The color is also attached to a field in the VMSDK Store, and it must therefore be performed in a transaction. Consequently, if the user invokes the Undo command after applying a stereotype, the color change is undone, but the stereotype remains applied. Another Undo is required to reverse the application of the stereotype. In some cases, this might be the effect that you intend, but if not, you can propagate the changes all inside one transaction by defining rules.

Rules are less useful for propagating changes outside the store, because they are not invoked when the user performs an Undo or Redo command.

A rule is a class that is registered with the rule manager. Ordinarily, when you write VMSDK code, you register a rule by attaching an attribute to the class and including the class in a list that is read when the extension code is loaded. But because the UML implementation is already compiled, you must add the rule to the rule manager dynamically. The code given in the example is strongly dependent on the current implementation rule management, which might change in future releases.

To add a rule, you have to know the names of the implementation classes. These might change in future releases. As far as possible, you should cast elements to the UML API types such as IClass, IProfile.

The example shows rules that handle addition and deletion of objects in the UML model. You can also create rules that respond to changes in the properties of objects. For more information, see Rules Propagate Changes Within the Model.

Example: Coloring Classes by Stereotype by using Rules

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

Example: Associations that are Bidirectional by Default

By default, when you draw an association in a class diagram, the new association is navigable only in one direction. It has an arrowhead at one end. For some purposes, it is more convenient to draw bidirectional associations, with no arrowheads. You can make bidirectional associations the default by adding the following rule.

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

To register the rule, you must use the SetRule method described in Defining Rules:

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

If you want to be able to enable or disable this rule, one method of doing so is to define a profile in which a particular stereotype is defined. You could add code to your rule to verify that the profile has been enabled in a containing package or model. For more information, see How to: Define a Profile to Extend UML.

Core and View Models

The UML model consists of more than one VMSDK (DSL) model:

  • The core model contains representations of all the elements in the UML model. The user can see these elements in the UML Model Explorer window, and you can access them through the UML ModelStore API. The core model occupies one VMSDK Store Partition.

  • There is one view model for every UML diagram in the UML project. The objects in each view model are proxies for objects in the core model. There is a view model object for each element that is displayed on the UML diagram. Each view model occupies a VMSDK store partition.

  • There is a VMSDK shape object for each element displayed on a diagram. There is a 1:1 relationship between view model elements and shapes.

When you define rules or event handlers, they will be called both when the core and view objects change. You should handle only changes to the core objects. The handlers in the examples use element.IsElementDefinition() to determine whether they are dealing with the core object.

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

See Also

Concepts

How to: Navigate the UML Model

Event Handlers Propagate Changes Outside the Model

Change History

Date

History

Reason

March 2011

Created topic.

Customer feedback.