DSL Tools #14 - Double-click in the Model Explorer

Every DSL Tools project generates this handy model explorer where you can browse the domain classes that are on your domain model.

Something like this:

Model Explorer Preview

This is particularly helpful for example to change the properties of a particular domain class. You browse for in the explorer, click on it, and simply edit the properties.

Now there is a fundamental feature that the guys at Microsoft missed in the explorer. The ability to double click a domain class there and have the domain model view show that domain class centered. A feature very useful if you have a model with a few dozens of domains classes (which is very typical).

So I put my mind to implement such behavior on my various DSL explorers. It turns out that it isn't very difficult.

1. Determine the explorer class name

The model explorer class is generated as part of your DSLPackage project. The name of the class depends on the name of your DSL. To find out the correct name, browse the DSLPackage project, under GeneratedCode, and you'll find a file named ModelExplorer.cs. The name of the explorer is the name of the first class on that file.

In my example that is UserInterfaceModelExplorer:

   1: /// <summary>
   2: /// Double-derived class to allow easier code customization.
   3: /// </summary>
   4: internal partial class UserInterfaceModelExplorer : UserInterfaceModelExplorerBase
   5: {
   6:     /// <summary>
   7:     /// Constructs a new UserInterfaceModelExplorer.
   8:     /// </summary>
   9:     public UserInterfaceModelExplorer(global::System.IServiceProvider serviceProvider)
  10:         : base(serviceProvider)
  11:     {
  12:     }
  13: }

2. Create a custom class with that same name

Under the DSLPackage, you will need to create a new custom partial class with the name of your model explorer. All the magic will happen here.

3. Override the CreateElementVisitor method

Here you should subscribe to the ObjectModelBrowser.DoubleClick event:

   1: /// <summary>
   2: /// Executed when the model explorer creates the tree element visitor.
   3: /// </summary>
   4: /// <returns>The tree element visitor.</returns>
   5: protected override IElementVisitor CreateElementVisitor()
   6: {
   7:     // Subscribe double click in the tree view
   8:  
   9:     this.ObjectModelBrowser.DoubleClick += new EventHandler(this.ObjectModelBrowser_DoubleClick);
  10:  
  11:     // Default behavior
  12:  
  13:     return base.CreateElementVisitor();
  14: }

4. Implement the corresponding event handler

The magic consists in:

  • First, finding the element that is selected in the explorer,
  • Second, getting the corresponding model element,
  • And, finally, selecting that shape in the model viewer.

Check the full source code bellow for the implementation of these steps.

5. Transform All templates

Before you test the new behavior of the model explorer, you'll need to transform all the templates to let the DSL know that you have customized your model explorer.

And that's it. Double-click any domain class in the explorer and the model viewer will be updated accordingly.

Full Source Code

   1: using System;
   2: using System.Collections.ObjectModel;
   3: using Microsoft.VisualStudio.Modeling;
   4: using Microsoft.VisualStudio.Modeling.Diagrams;
   5: using Microsoft.VisualStudio.Modeling.Shell;
   6:  
   7: namespace MyDSL.UserInterface
   8: {
   9:     /// <summary>
  10:     /// Custom behavior for UserInterfaceModelExplorer.
  11:     /// </summary>
  12:     internal partial class UserInterfaceModelExplorer
  13:     {
  14:         #region Public Methods
  15:  
  16:         /// <summary>
  17:         /// Executed when the model explorer creates the tree element visitor.
  18:         /// </summary>
  19:         /// <returns>The tree element visitor.</returns>
  20:         protected override IElementVisitor CreateElementVisitor()
  21:         {
  22:             // Subscribe double click in the tree view
  23:  
  24:             this.ObjectModelBrowser.DoubleClick += new EventHandler(this.ObjectModelBrowser_DoubleClick);
  25:  
  26:             // Default behavior
  27:  
  28:             return base.CreateElementVisitor();
  29:         }
  30:  
  31:         #endregion
  32:  
  33:         #region Private Methods
  34:  
  35:         /// <summary>
  36:         /// Selects the specified shape using the given modeling document data.
  37:         /// </summary>
  38:         /// <param name="shapeElement">The shape element that should be selected.</param>
  39:         /// <param name="docData">The modeling document data.</param>
  40:         private static void SelectShape(ShapeElement shapeElement, DocData docData)
  41:         {
  42:             // Validation
  43:  
  44:             if (shapeElement == null)
  45:             {
  46:                 throw new ArgumentNullException("shapeElement");
  47:             }
  48:  
  49:             if (docData == null)
  50:             {
  51:                 throw new ArgumentNullException("docData");
  52:             }
  53:  
  54:             // Select the shape
  55:  
  56:             ModelingDocView docView = docData.DocViews[0];
  57:             if (docView != null)
  58:             {
  59:                 docView.SelectObjects(1, new object[] { shapeElement }, 0);
  60:             }
  61:         }
  62:         
  63:         /// <summary>
  64:         /// Gets the first shape the represents the specified model element.
  65:         /// </summary>
  66:         /// <param name="modelElement">The model element whose shape will be returned.</param>
  67:         /// <returns>The first shape the represents the specified model element.</returns>
  68:         private static ShapeElement GetModelElementFirstShape(ModelElement modelElement)
  69:         {
  70:             // Presentation elements
  71:  
  72:             LinkedElementCollection<PresentationElement> presentations = PresentationViewsSubject.GetPresentation(modelElement);
  73:             foreach (ModelElement element in presentations)
  74:             {
  75:                 ShapeElement shapeElement = (element as ShapeElement);
  76:                 if (shapeElement != null)
  77:                 {
  78:                     return shapeElement;
  79:                 }
  80:             }
  81:  
  82:             // Default result
  83:  
  84:             return null;
  85:         }
  86:  
  87:         /// <summary>
  88:         /// Return the model element which is the parent of the specified model element considering that
  89:         /// this element is placed in a compartment.
  90:         /// </summary>
  91:         /// <param name="modelElement">The model element whose parent will be returned.</param>
  92:         /// <returns>
  93:         /// The model element which is the parent of the specified model element considering that
  94:         /// this element is placed in a compartment.
  95:         /// </returns>
  96:         private static ModelElement GetCompartmentElementFirstParent(ModelElement modelElement)
  97:         {
  98:             // Get the domain class associated with model element.
  99:  
 100:             DomainClassInfo domainClass = modelElement.GetDomainClass();
 101:             if (domainClass != null)
 102:             {
 103:                 // A element is only considered to be in a compartment if it participates in only 1 embedding relationship
 104:                 // This might be wrong for some models
 105:  
 106:                 if (domainClass.AllEmbeddedByDomainRoles.Count == 1)
 107:                 {
 108:                     // Get a collection of all the links to this model element
 109:                     // Since this is in a compartment there will only be one
 110:  
 111:                     ReadOnlyCollection<ElementLink> links = DomainRoleInfo.GetAllElementLinks(modelElement);
 112:                     if (links.Count == 1)
 113:                     {
 114:                         // Get the model element participating in the link that isn't the current one
 115:                         // That will be the parent
 116:                         // Probably there is a better way to achieve the same result
 117:  
 118:                         foreach (ModelElement linkedElement in links[0].LinkedElements)
 119:                         {
 120:                             if (!modelElement.Equals(linkedElement))
 121:                             {
 122:                                 return linkedElement;
 123:                             }
 124:                         }
 125:                     }
 126:                 }
 127:             }
 128:  
 129:             // Default result
 130:  
 131:             return null;
 132:         }
 133:  
 134:         #endregion
 135:  
 136:         #region Event Handlers
 137:  
 138:         private void ObjectModelBrowser_DoubleClick(object sender, EventArgs e)
 139:         {
 140:             // Get the node selected in the model explorer tree
 141:  
 142:             ModelElementTreeNode node = (this.ObjectModelBrowser.SelectedNode as ModelElementTreeNode);
 143:             if (node != null)
 144:             {
 145:                 // Get the corresponding model element
 146:  
 147:                 ModelElement element = node.ModelElement;
 148:                 if (element != null)
 149:                 {
 150:                     // Get the corresponding shape
 151:                     // If the model element is in a compartment the result will be null
 152:  
 153:                     ShapeElement shape = GetModelElementFirstShape(element);
 154:                     if (shape == null)
 155:                     {
 156:                         // If the element is in a compartment, try to get the parent model element to select that
 157:  
 158:                         ModelElement parentElement = GetCompartmentElementFirstParent(element);
 159:                         if (parentElement != null)
 160:                         {
 161:                             // Get the corresponding shape
 162:  
 163:                             shape = GetModelElementFirstShape(parentElement);
 164:                         }
 165:                     }
 166:  
 167:                     // Select the shape
 168:  
 169:                     if (shape != null)
 170:                     {
 171:                         SelectShape(shape, this.ModelingDocData);
 172:                     }
 173:                 }
 174:             }
 175:         }
 176:  
 177:         #endregion
 178:     }
 179: }

Special thanks are in order to Oleg Sych for his fundamental tips to get this working.

Published 16 December 08 04:22 by hgr
Filed under: ,

Comments

# Hugo Ribeiro said on December 16, 2008 4:23 PM:

Since I've been doing a number of posts on the DSL Tools, I thought it would be helpful to have a list

Anonymous comments are disabled