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.

Posted 16 December 08 04:22 by hgr | 1 Comments   
Filed under ,
Agile Prototyping

One of these days I went for a periodic review meeting of the project I'm working on. We were going to discuss the implementation of a critical piece for our next major milestone.

We invited our User Experience Specialist and he was going to present us the first UI prototypes.

I was expecting some great WPF "almost-ready" screens with great user experience stuff and a wonderful look-and-feel like all the other prototypes they did before...

This was what I got to see:

Now talk about agile prototyping. :)

Posted 11 December 08 11:28 by hgr | 1 Comments   
Filed under
DSL Tools #13 - Changing the Appearance of a Shape at Runtime

Finally I found some time to document this simple customization to the DSL Tools designers.

The requirement is as follows:

  • I have a given domain class - called Module Reference - that has a property called Main.
  • This domain property indicates if that particular class is the "most important" of that kind in the model.
  • This domain class as an association with another domain class - called Entity. That association allows me to connect multiple entities to any given module reference.
  • All the entities connected to the main module reference need to presented with a different color (different from the default shape color defined in the DSL definition model).

The domain model is the following:

Achieving this is quite simple:

1. Create a change rule to respond to the change in the Main property of the ModuleReference domain class:

   1: using System;
   2: using Microsoft.VisualStudio.Modeling;
   3: using Microsoft.VisualStudio.Modeling.Diagrams;
   4:  
   5: namespace MyModel
   6: {
   7:     /// <summary>
   8:     /// Defines a change rule for the ModuleReference domain class.
   9:     /// </summary>
  10:     [RuleOn(typeof(Primavera.Athena.Models.Reporting.ModuleReference), FireTime = TimeToFire.TopLevelCommit)]
  11:     public class ModuleReferenceChangeRule : ChangeRule
  12:     {
  13:         #region Public Methods
  14:  
  15:         /// <summary>
  16:         /// Alerts listeners that a property for an element has changed.
  17:         /// </summary>
  18:         /// <param name="e">Provides data for the ElementPropertyChanged event.</param>
  19:         public override void ElementPropertyChanged(ElementPropertyChangedEventArgs e)
  20:         {
  21:             // Validations
  22:  
  23:             if (e == null)
  24:             {
  25:                 throw new ArgumentNullException("e");
  26:             }
  27:  
  28:             if (e.DomainProperty.Id.Equals(ModuleReference.MainDomainPropertyId))
  29:             {
  30:                 HandleMainChanged(e);
  31:             }
  32:  
  33:             // Default behavior
  34:  
  35:             base.ElementPropertyChanged(e);
  36:         }
  37:         
  38:         #endregion
  39:  
  40:         #region Private Methods
  41:  
  42:         /// <summary>