Snip your dsl into prototypes

Posted on June 25, 2008 by

0


Mary Jo Foley revealed the love story between Microsoft and modeling a few months ago. But the information that must retain our attention is that Oslo will be certainly the city chosen for this honeymoon. Indeed, a few years ago Microsoft did the first move when appointing famous modeling engineers and developing DSL Tools in order to seduce this model on the first date. Since then, Microsoft never stopped declaring its love and even promoted it as an essential pillar of its SOA strategy.

So, for connected system developers, having a look at DSL tools seems to be a good way to understand the perspectives that Oslo can bring into the SOA panorama.

That being said, this post targets DSL model prototyping.

Introduction

Everybody knows patterns promoted by the famous Gang of Four, when defining a model the same problematic applies. Some concepts are based on the same structure or a concept can be an aggregate of existing concepts. It would be very useful to be able to define reusable patterns in DSL as we can do for code.

This post will present a solution to integrate such mechanisms into your DSL tools projects. DSL tools offer many customization points that we will use to accomplish our goal. Unlike technologies such as WCF, WPF or WF, DSL tools offers customization points rather than extensibility mechanisms. The promised feature codenamed Backplane (designer bus) or Oslo will maybe change this situation.

This post will present a solution to integrate such mechanisms into your DSL Tools projects. DSL Tools offers many customization points, we’ll use them to respond to our needs. Unlike technologies such as WF or WCF, with DSL Tools we use more customization mechanisms than extensibility mechanisms. The promised Backplane feature (designer bus) and even Oslo may lead Microsoft to change its orientation towards this and introduce more extensibility rather than customization.

How to

DSL Tools offers basic features to support prototyping that are used essentially to customize dragging and dropping of a dsl toolbox item onto a diagram.
These features are essentially based on a single class: ElementGroupPrototype.
We must not interact directly with this class. A builder design pattern was set up in order to produce a prototype. ElementGroup class must be used instead because it provides a more convenient developer experience. This class has basic methods to build your prototype from selected model elements: Add, AddGraph, AddRange. The AddGraph method adds automatically embedded children and associated relationships of the added model element. Prototyping can be seen as a clone to
whom a specific property is set with an autogenerated value. Indeed, when you merge your elementgroupprototype on your model, the created model element takes into account if one of its properties has an associated ElementNameProvider allowing assigning a unique value to this property.

These features can be used to enable copy and paste operations as well.  It will be our first step, it allows to easily adopt prototype mechanisms.

We won’t dig into the details about this as it’s already described into the “Domain-Specific Development with Visual Studio DSL Tools” book that must be considered as a bible for people using DSL Tools (See page 419 “Implementing Copy and Paste”).

An implementation of these operations is available as well within the library developed by the DslFactory community; this library named “DslFactory Utilities” is available on Codeplex. We will take into account various fore mentioned work. We’ll add the cut operation support and enable all these operations from the visual studio standard edit menu.

The next step will be to customize the ModelExplorer window, especially the contained tree view in order to add our own nodes and mechanisms to list user defined prototypes.
Finally, we will take advantage of DSL tools prototype serialization to persist prototypes on disk thus allowing offering a similar mechanism as code snippets does.

Enable cut, copy and paste operations

Here is the step by step procedure:

  • Define copy, cut and paste operation logic,
  • Register these operations in order to be able to call them via the standard and contextual menus.

We’ll use a DSL Tools solution based on the Domain-Specific Language Designer Class Diagrams template in order to present this solution. We add to this template solution a folder named “CustomCode” in the “DSL” and “DSL package” projects.

Declare your new commands

1. Open Solution Explorer

2. Go to the DslPackage project

3. Open the Commands.vsct file

4. Define a button for each operation

<Button guid="guidCmdSet" id="cmdidCopy" priority="0x0902" type="Button">
  <Parent guid="guidCmdSet" id="grpidContextMain"/>
  <CommandFlag>DefaultDisabled</CommandFlag>
  <CommandFlag>DefaultInvisible</CommandFlag>
  <CommandFlag>DynamicVisibility</CommandFlag>
  <Strings>
    <ButtonText>Copy</ButtonText>
  </Strings>
</Button>

5. Declare its associated symbols.

5.1 Open the GeneratedCode/Constants.cs

5.2 Copy the value of the Constants.xxxCommandSetId constant 5.3 Assign it to the value attribute of the GuidSymbol xml element

<GuidSymbol name="guidCmdSet" value="{xxxx-xxx-xx-xx-xxx}" >
  <IDSymbol name="cmdidCopy" value="0x804"/>
</GuidSymbol>

6. Create a static Commands class to reference the information associated to your new commands.

6.1 Right-click on the CustomCode folder of the DslPackage project, Add Class

6.2 Enter “Commands” as name.

6.3 For each command, declare the constant integer used as value for the value attribute of the IDSymbol xml element associated to the command.

internal static class Commands
{
    private const int CopyCommandId = 0x804;
    private const int CutCommandId = 0x805;
    private const int PasteCommandId = 0x806;

    public static readonly CommandID Copy;
    public static readonly CommandID Paste;
    public static readonly CommandID Cut;

    static Commands()
    {
        Copy = new CommandID(Constants.xxxCommandSetId,
             CopyCommandId);
        Paste = new CommandID(Constants.xxxCommandSetId,
             PasteCommandId);
        Cut = new CommandID(Constants.xxxCommandSetId,
             CutCommandId);
    }
}

Implement your commands

7. Open or create a partial class of your CommandSet in the “CustomCode” folder.

8. Develop for each operation an handler in order to display (or hide) the command depending on the context and an handler that executes the command when it is selected.

private void OnStatusCopyOrCut(object sender, EventArgs e)
{
    MenuCommand command = sender as MenuCommand;
    command.Visible = command.Enabled = 
        ((this.CurrentSelection.Count > 0) &&
         !(this.GetSelectedObject(0) is Diagram));
}

private void OnMenuCopy(object sender, EventArgs e)
{
    Diagram l_diagram = this.CurrentDocView.CurrentDiagram;
    ElementGroup l_elementGroup =
        new ElementGroup(l_diagram.Partition);

    foreach (object selectedObject in this.CurrentSelection)
    {
        // Pick out shapes representing Component model elements.
        ShapeElement l_shapeElement =
            selectedObject as ShapeElement;
        if (l_shapeElement != null &&
            l_shapeElement.ModelElement != null &&
            l_shapeElement.ModelElement is ModelType)
        {
            // add the element and its embedded children
            // to the group
            l_elementGroup.AddGraph(l_shapeElement.ModelElement,
                true);
        }
    }
    if (l_elementGroup.RootElements.Count == 0) return;
    //A DataObject wraps a serialized version.
    IDataObject data = new DataObject();
    data.SetData(l_elementGroup.CreatePrototype());
    Clipboard.SetDataObject
    (data,   // serialized clones of our selected model elements
    false,  // we don't want to export outside this application
    10,     // retry 10 times on failure
    50);    // waiting 50ms between retries
}

For the copy and cut operations, we display commands only if the current selection contains objects other than current Diagram.

For the paste operations, we could check if the clipboard data content can be merged with the current diagram.

The cut operation logic adds to the copy operation logic a step that deletes the copied elements.

As you can see, if you downloaded the complete solution, we added to our partial  CommandSet class 2 additional things:

  • An operation GetSelectedObject to get a selected object from its index in the current selection list.
  • A property CurrentStore to easily access the current IMS.

9. Override the GetMenuCommands() method to add your new commands to the context menu.

9.1 Instantiate a DynamicStatusMenuCommand for each operation.

protected override IList<MenuCommand> GetMenuCommands()
{
    IList<MenuCommand> l_commandList = base.GetMenuCommands();

    DynamicStatusMenuCommand l_cmdCopy =
        new DynamicStatusMenuCommand(
            new EventHandler(OnStatusCopyOrCut),
            new EventHandler(OnMenuCopy),
            Commands.Copy);
    l_commandList.Add(l_cmdCopy);
    ...
    return l_commandList;
}

In the introduction, we wanted to enable the standard Visual Studio cut, copy, paste commands on our shapes. The only remaining difficulty is to identify the commands associated to them. They are available in the well known StandardCommands.

By now, you must only instantiate a new DynamicStatusMenuCommand class by using the relevant StandardCommands field for each command (StandardCommands.Copy etc…).

Deploy your new commands

It is required to increment the Menu Resource Index for DSL Tools to take into account your new commands.

10. Open GeneratedCode\Package.tt

11. Increment the second integer of the ProvideMenuResource attribute.

[VSShell::ProvideMenuResource("1000.ctmenu", 2)]

12. Transform all templates

13. Rebuild.

14. Enjoy

image

Figure 1 – Visual Studio Edit Menu

Conclusion

So, it works fine but the current behavior is very restricted not only because operations are enabled only for ModelType and its derived types. When you copy a class that contains operations, they are not copied. It’s the expected behavior, indeed, by default; the copy propagation is not enabled. So, if you want to change this behavior, you only need to set to true the property PropagatesCopy of ModelClass Domain Role of ClassHasOperations relationship.

image

Figure 2 –  PropagesCopy property

The recommendation is to allow a fine grained customization of the prototype creation from a domain class. To do this, you can define an interface IPrototypableto declare the domain class that support copy operation and to allow the customization of the creation of an ElementGroupPrototypefrom it.

internal interface IPrototypable
{
    ElementGroupPrototype GetPrototype();
}

To implement this interface by a desired domain class, it’s only required to create a dedicated partial class and to implement the default prototype logic:

public ElementGroupPrototype GetPrototype()
{
    ElementGroup l_elementGroup =
        new ElementGroup(this.Store);
    l_elementGroup.AddGraph(this, true);
    return l_elementGroup.CreatePrototype();
}

Customize the model explorer

Add your own nodes

The model explorer user interface is based on a tree view; the idea here is to add custom nodes to the treeview in order to list user-defined prototypes.

Unfortunately, if it’s possible to customize the existing nodes (label, image, visibility), no real mechanism are offered to add our own nodes. However, a workaround exists: the treeview loading mechanism is done by a single method: RefreshBrowserView()

If it was a virtual method, we could override it to add our own nodes (This one is going straight to connect.microsoft.com, so feel free to vote for it). In the mean time, you can have a look at the methods that use it thanks to Reflector: ModelExplorerToolWindow.OnDocumentWindowChanged, and ModelExplorerTreeContainer.ElementEventsEndedEventHandlerImpl.

We can override the first method to add a call to our own refresh browser view logic. The second method is a callback registered for ElementEventsEnded IMS event, we can add or own callback for this event.

To add custom nodes to the model explorer, there is only one constraint, custom node class must inherit from ExplorerTreeNode. It is required to call the

method ProvideNodeText in order to populate the Text property of your node if it is not set it in its constructor.

Below is a basic implementation of a custom node:

public class DefaultTreeNode : ExplorerTreeNode
{
        public DefaultTreeNode(String name, String text)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            if (text == null)
            {
                throw new ArgumentNullException("text");
            }
            this.Name = name;
            this.Text = text;
        }
        protected override string ProvideNodeText()
        {
            return this.Text;
        }
}

1. Go to the DslPackage project

2. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

3. Add it this method :

internal void RefreshBrowserViewExt()
{
   // Add your custom logic here
}

4. Add your custom logic to load your nodes in the model explorer.

5. Create a callback that calls your previously created method.

private void ElementEventsEndedEventHandlerImplExt(object sender,
              ElementEventsEndedEventArgs e)
{
   this.RefreshBrowserViewExt();
}

6. Override SubscribeToImsEvent and add your own callback in order to call your custom refresh browser logic.

protected override void SubscribeToImsEvent(Store newStore)
{
   base.SubscribeToImsEvent(newStore);
   // HACK : Check above if the store is null.
   newStore.EventManagerDirectory.ElementEventsEnded.Add(
      new EventHandler<ElementEventsEndedEventArgs>(
      this.ElementEventsEndedEventHandlerImplExt));
}

7. Override the UnsubscribeToImsEvent to remove your callback.

protected override void UnsubscribeToImsEvent(Store oldStore)
{
   base.UnsubscribeToImsEvent(oldStore);
   // HACK : Don't check above if the store is null.
   if (oldStore != null)
   {
     oldStore.EventManagerDirectory.ElementEventsEnded.Remove(
       new EventHandler<ElementEventsEndedEventArgs>(
          this.ElementEventsEndedEventHandlerImplExt));
   }
}

8. Open or create a partial class of your ModelExplorerToolWindow  in the “CustomCode” folder of the DslPackage.

9. Override OnDocumentWindowChanged method in order to call your custom refresh browser logic view.

protected override void OnDocumentWindowChanged(
    ModelingDocView oldView, ModelingDocView newView)
{
    base.OnDocumentWindowChanged(oldView, newView);
    ((YourModelExplorer)this.TreeContainer).
       RefreshBrowserViewExt();
}

Add your own model explorer commands

You can add your own commands in the context menu associated to the model explorer. The only differences between this and doing it for the context menu associated to the current diagram are:

  • The ids to use: guidCommonModelingMenu and cmdidMenuExplorerCommand.
  • The mandatory definition of a guid symbol associated to the command

1. Go to the DslPackage project.

2. Open the Commands.vsct file.

3. Define a button for your operation using the relevant ids.

<Button guid=" guidMenuExplorerCommand"
        id="cmdidMenuExplorerCommand" priority="0x0902"
        type="Button">
  <Parent guid="guidCommonModelingMenu"
          id="grpidExplorerMenuGroup"/>
  <CommandFlag>DefaultDisabled</CommandFlag>
  <CommandFlag>DefaultInvisible</CommandFlag>
  <CommandFlag>DynamicVisibility</CommandFlag>
  <Strings>
    <ButtonText>My command</ButtonText>
  </Strings>
</Button>

4. Declare the symbol associated to it.

<GuidSymbol name=" guidMenuExplorerCommand "
            value="{xxxx048E-739A-474A-94CC-CDA7347C52E7}" >
  <IDSymbol name=" cmdidMenuExplorerCommand " value="0x807"/>
</GuidSymbol>

5. Create a static Commands class to reference the information associated to your new commands.

5.1 Right-click on the CustomCode folder of the DslPackage project, Add Class

5.2 Enter “Commands” as name.

5.3 Declare the constant guid used as value for the value attribute of the GuidSymbol xml element associated to your command.

5.4 Declare the constant integer used as value for the value attribute of the IDSymbol

xml element associated to your command.

6. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

7. For each command implement a handler in order to display (or hide) the command

depending on the context and a handler that is executed if the command is selected.

8. Override the AddCommandHandlers method to add your new commands to the model explorer context menu.

public override void AddCommandHandlers(
   IMenuCommandService menuCommandService)
{
    base.AddCommandHandlers(menuCommandService);
    DynamicStatusMenuCommand l_cmdMyCmd =
      new DynamicStatusMenuCommand(
                new EventHandler(OnStatusCmd),
                new EventHandler(OnMenuCmd),
                Commands.MyCmd);
    menuCommandService.AddCommand(l_ cmdMyCmd);
}

9. Open the GeneratedCode\Package.tt

10. Increment the second integer of the ProvideMenuResource attribute.

[VSShell::ProvideMenuResource("1000.ctmenu", 3)]

11. Transform all templates

12. Rebuild

13. Enjoy

Define prototype mechanism

Here we’re going to offer to users a mechanism to prototype a model element. To do this, we use the logic presented to enable copy operations and to customize the model explorer.

The customization of the model explorer must allow:

  • Adding custom node when a user defines a prototype.
  • Handling drag and drop on prototype node from the model explorer.
  • Saving user-defined prototypes beyond the scope of visual studio instance (persisted on disk).

Add custom node when a user defines a prototype

1. Create a serializable class Pattern

1.1 Right-click on the CustomCode folder of the DslPackage project, Add Class

1.2 Set its name to “Pattern”.

2. Add to it two properties : Name (String) and Prototype (ElementGroupPrototype)

[Serializable]
public sealed class Pattern
{
    public String Name { get; set; }
    public ElementGroupPrototype Prototype { get; set; }

    public Pattern(String name,
        ElementGroupPrototype prototype)
    {
        this.Name = name;
        this.Prototype = prototype;
    }
}

3. Implement an ExplorerTreeNode dedicated to your class.

public class PatternTreeNode : ExplorerTreeNode
{
    private Pattern m_pattern;

    public PatternTreeNode(Pattern pattern)
    {
        if (pattern == null)
        {
            throw new ArgumentNullException("pattern");
        }
        m_pattern = pattern;
        this.Name = pattern.Name;
    }

    public Pattern Pattern
    {
        get { return m_pattern; }
    }

    protected override string ProvideNodeText()
    {
        return m_pattern.Name;
    }
}

4. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

5. Add private field to store Pattern objects.

private List<Pattern> m_patternList = new List<Pattern>();

6. Code an Add and Remove method to manage the pattern list and to update the model explorer

internal void AddPatternTreeNode(String name,
    ElementGroupPrototype prototype)
{
    string l_name = name;
    if (string.IsNullOrEmpty(name))
    {
        l_name = this.GetNewPatternName();
    }
    Pattern l_pattern = new Pattern(l_name, prototype);
    this.AddPatternTreeNode(l_pattern);
    m_patternList.Add(l_pattern);
}
private void AddPatternTreeNode(Pattern pattern)
{
    TreeNode l_rootTreeNode =
        this.ObjectModelBrowser.TopNode;
    TreeNode l_prototypesTreeNode =
        l_rootTreeNode.Nodes["Prototypes"];
    PatternTreeNode l_patternTreeNode =
        new PatternTreeNode(pattern);
    l_patternTreeNode.UpdateNodeText();
    this.InsertTreeNode(l_prototypesTreeNode.Nodes,
        l_patternTreeNode);
}
private void RemovePatternTreeNode(
    PatternTreeNode patternTreeNode)
{
    patternTreeNode.Remove();
    m_patternList.Remove(patternTreeNode.Pattern);
}

7. Load into the Model Explorer your list of patterns by using a specific RefreshBrowserViewExt method

internal void RefreshBrowserViewExt()
{
    this.ObjectModelBrowser.BeginUpdate();
    if (this.ObjectModelBrowser.TopNode != null)
    {
        TreeNode l_rootTreeNode =
            this.ObjectModelBrowser.TopNode;
        DefaultTreeNode l_defaultTreeNode =
            new DefaultTreeNode("Prototypes");
        this.InsertTreeNode(l_rootTreeNode.Nodes,
            l_defaultTreeNode);
        l_defaultTreeNode.UpdateNodeText();
        for (int i = 0; i < m_patternList.Count; i++)
        {
            this.AddPatternTreeNodeToObjectModelBrowser(
                m_patternList[i]);
        }
        l_defaultTreeNode.Expand();
    }
    this.ObjectModelBrowser.EndUpdate();
}

8. Similar to what has been done for copy operations; add to your diagram’s context menu a method to prototype the user’s selection.  The only difference is that it is not the Clipboard that will receive the created ElemtGroupPrototype but the odelExplorer. To get a pointer to our ModelExplorer, we add a strongly typed property to our ModelExplorerToolWindow that casts its generic TreeContainer property.

private void OnMenuPrototype(object sender, EventArgs e)
{
    Diagram l_diagram = this.CurrentDocView.CurrentDiagram;
    ElementGroup l_elementGroup =
        new ElementGroup(l_diagram.Partition);
    foreach (object selectedObject in this.CurrentSelection)
    {
        // Pick out shapes representing Component model elements.
        ShapeElement l_shapeElement =
            selectedObject as ShapeElement;
        if (l_shapeElement != null &&
            l_shapeElement.ModelElement != null &&
            l_shapeElement.ModelElement is ModelType)
        {
            l_elementGroup.AddGraph(l_shapeElement.ModelElement,
              true);
        }
    }
    this.PrototypeExplorerToolWindow.PrototypeExplorer.
        AddPatternTreeNode(null, l_elementGroup.CreatePrototype());
}

9. Don’t forget to increment the second integer of the ProvideMenuResource attribute.

10. Transform all templates

11. Rebuild

12. Enjoy

image

Figure 3 – Diagram Context Menu

Implement prototype node drag and drop from the model explorer

The tree control contained in the ModelExplorer is simply the well known System.Windows.Forms.TreeView control. Thus , it offers the same features such as drag and drop.

To customize its behavior, you can override your ModelExplorer’s OnCreateControol method (due to ContainerControl base class inheritance).

1. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

2. Override the OnCreateControl () method to customize the tree control.

protected override void OnCreateControl()
{
    base.OnCreateControl();
    this.ObjectModelBrowser.LabelEdit = true;
    this.ObjectModelBrowser.KeyUp += new KeyEventHandler(
        ObjectModelBrowser_KeyUp);
    this.ObjectModelBrowser.ItemDrag += new ItemDragEventHandler(
        ObjectModelBrowser_ItemDrag);
    this.ObjectModelBrowser.AfterLabelEdit += new
        NodeLabelEditEventHandler(
          ObjectModelBrowser_AfterLabelEdit);
}

3. Implement the desired logic for each event handled. We decide to offer:

  • Prototype deletion by selecting it and pressing the delete key.
  • Prototype renaming by clicking on it.
  • Prototype drag and drop (as you can see it, it’s straightforward to activate it, thanksto ElementGroupPrototype).
private void ObjectModelBrowser_KeyUp(object sender,
    KeyEventArgs e)
{
    if (!e.Handled && e.KeyValue == (int)Keys.Delete)
    {
        TreeNode l_treeNode =
            this.ObjectModelBrowser.SelectedNode;
        if (l_treeNode != null &&
            l_treeNode is PatternTreeNode)
        {
            this.RemovePatternTreeNode(
                (PatternTreeNode)l_treeNode);
        }
    }
}
private void ObjectModelBrowser_ItemDrag(object sender,
    ItemDragEventArgs e)
{
    if (e.Item is PatternTreeNode)
    {
        this.ObjectModelBrowser.SelectedNode =
            (TreeNode)e.Item;
        this.ObjectModelBrowser.DoDragDrop(
            ((PatternTreeNode)e.Item).Pattern.Prototype,
            DragDropEffects.Copy);
    }
}
private void ObjectModelBrowser_AfterLabelEdit(object sender,
    NodeLabelEditEventArgs e)
{
    if (e.Node is PatternTreeNode)
    {
        ((PatternTreeNode)e.Node).Pattern.Name =
            e.Label;
    }
}

image

Figure 4 – Customized model explorer

Save user-defined prototypes beyond the scope of visual studio instance

Saving user-defined prototypes is only a matter of serializing it. Indeed, ElementGroupPrototype can transit through the Clipboard. So, the serialization mechanism is already supported. We use it to save them in a basic file.

1. Create a static class to manage serialization (and deserialisation) of user-defined prototype list into a file.

1.1 Right-click on the CustomCode folder of the DslPackage project, Add Class

1.2 Enter “UserDefinedPrototypeSerializationHelper” as its name.

2. Add a method to implement serialization and deserialization mechanism.

internal static void Serialize(List<Pattern> patternList,
    string file)
{
  if (!Directory.Exists(Path.GetDirectoryName(file)))
  {
    Directory.CreateDirectory(Path.GetDirectoryName(file));
  }
  using (Stream stream = File.Open(file, FileMode.Create))
  {
    BinaryFormatter l_binaryFormatter = new BinaryFormatter();
    l_binaryFormatter.Serialize(stream, patternList);
  }
}

internal static List<Pattern> DeSerializeObject(string file)
{
  List<Pattern> l_patternList = null;
  using (Stream stream = File.Open(file, FileMode.Open))
  {
    BinaryFormatter l_binaryFormatter = new BinaryFormatter();
    l_patternList =
      (List<Pattern>)l_binaryFormatter.Deserialize(stream);
  }
  return (l_patternList == null ?
             new List<Pattern>() : l_patternList);
}

3. The deserialization mechanism used in the experimental hive requires that you provide a specific a AssemblyResolve mechanism in order to resolve dsl assemblies.

static UserDefinedPrototypeSerializationHelper()
{
    AppDomain.CurrentDomain.AssemblyResolve +=
        new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}

private static Assembly CurrentDomain_AssemblyResolve(
    object sender,
    ResolveEventArgs args)
{
    if (args.Name.IndexOf("DslPackage") > 0)
    {
        return
          typeof(UserDefinedPrototypeSerializationHelper).Assembly;
    }
    else if (args.Name.IndexOf("Dsl") > 0)
    {
        return typeof(YourDomainModel).Assembly;
    }
    return null;
}

4. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

5. Add specific code to the overridden method OnCreateControl () to deserialize saved user-defined prototypes and to load it into the ModelExplorer.

protected override void OnCreateControl()
{
    String l_fileName = this.GetPrototypeFileName();
    if (File.Exists(l_fileName))
    {
        m_patternList =
            PrototypeSerializationHelper.DeSerializeObject(
              l_fileName);
    }
    base.OnCreateControl();
    this.ObjectModelBrowser.LabelEdit = true;
    …

6. Override the Dispose method to save when the user-defined prototypes into a file when the ModelExplorer is closed.

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        String l_fileName = this.GetPrototypeFileName();
        UserDefinedPrototypeSerializationHelper.Serialize(
            m_patternList, l_fileName);
    }
    base.Dispose(disposing);
}

Going further

This article lists a fair amount of customization mechanisms and promotes ElementGroupPrototype class. It’s possible, of course to go further. You can add export and import feature on prototypes in order to share them in a similar way as visual studio templates or code snippets. If the fact that you can associate an ElementNameProvider to only one property is too limitating, one can use an interface (IPrototypable to broaden the customization of the model element created. For instance, customizing the property grid associated to the prototype allows defining a description to it.

It would be very useful if the Model Explorer could support domain class namespaces (as model element base classes).

Add to toolbox

Before ending this article, we add to the ModelExplorer context menu a specific command to allow adding to the toolbox and user-defined prototype.

1. Implement an “Add to toolbox” command (See paragraph entitled “Add your own model explorer commands”)

2. Use IToolboxService service mechanism in the execution logic of your command and take advantage of ModelingToolboxItem class to create and add to the toolbox an item dedicated to your prototype.

private void OnMenuAddToToolBox(object sender, EventArgs e)
{
    IToolboxService l_toolBoxService =
        (IToolboxService)this.ServiceProvider.GetService(
            typeof(IToolboxService));

    if (this.ObjectModelBrowser.SelectedNode is PatternTreeNode)
    {
        Pattern l_pattern =
            ((PatternTreeNode)this.ObjectModelBrowser.SelectedNode)
                .Pattern;

        ModelingToolboxItem l_modelingToolboxItem =
            new ModelingToolboxItem(l_pattern.Name, 1,
               l_pattern.Name,
              (Bitmap)ImageHelper.GetImage(
                PrototypeDomainModel.SingletonResourceManager.
                  GetObject("ModelClassToolboxBitmap")),
                "Prototypes", "Prototypes", "", "Prototype",
                l_pattern.Prototype,
                new ToolboxItemFilterAttribute[]
                {
                   new ToolboxItemFilterAttribute(
                    PrototypeToolboxHelper.ToolboxFilterString,
                    ToolboxItemFilterType.Require)
                });

        l_toolBoxService.AddToolboxItem(l_modelingToolboxItem,
            "Prototypes");
    }
}

image

Figure 5 – Customized toolbox

Workaround

Unfortunately, the treeview control has unexpected behavior when you want to customize the status of a context menu command.

If you want command displayed only for a specific tree node, you basically use SelectedNode property of ObjectModelBrowser (on the ModelExplorer instance). But this property doesn’t return the relevant selected node if you do not select the node with a left click before right clicking it to display the context menu.

To work around this problem, one can implement a specific callback for the AfterSelect event raised by the treeview that uses the selection service.

1. Open or create a partial class of your ModelExplorerTreeContainer (ModelExplorer) in the “CustomCode” folder.

2. Add specific code to the overrided OnCreateControl () method to add a callback for AfterSelect event raised by the contained treeview (ObjectModelBrowser).

protected override void OnCreateControl()
{
  String l_fileName = this.GetPrototypeFileName();
  if (File.Exists(l_fileName))
  {
    m_patternList =
       UserDefinedPrototypeSerializationHelper.
          DeSerializeObject(l_fileName);
  }
  base.OnCreateControl();
  this.ObjectModelBrowser.LabelEdit = true;
  this.ObjectModelBrowser.KeyUp += new KeyEventHandler(
    ObjectModelBrowser_KeyUp);
  this.ObjectModelBrowser.ItemDrag +=
    new ItemDragEventHandler(ObjectModelBrowser_ItemDrag);
  this.ObjectModelBrowser.AfterLabelEdit +=
    new NodeLabelEditEventHandler(
       ObjectModelBrowser_AfterLabelEdit);
  this.ObjectModelBrowser.AfterSelect +=
    new TreeViewEventHandler(ObjectModelBrowser_AfterSelect);
}

3. Implement your callback by using the selection service to customize the selection mechanism associated to our custom tree node.

private void ObjectModelBrowser_AfterSelect(object sender,
    TreeViewEventArgs e)
{
    if (e.Node is PatternTreeNode)
    {
        this.SelectionService.SetSelectedComponents(
            new object[] { ((PatternTreeNode)e.Node).Pattern });
    }
}

Support prototype auto-layout on drag and drop

When you drag and drop a prototype or when you copy and paste a model element, the associated shapes created by this action are displayed by default on the same position.

To avoid this, one can call AutoLayoutShapeElements method of the diagram on which modelelements are dropped.

Here is below a code extract to accomplish this task.

1. Open Solution Explorer.

2. Go to the Dsl project.

3. Open or create a partial class of your Diagram in the “CustomCode” folder.

4. Override the OnDragDrop method to layout automatically dropped elements.

Take into account model element namespace

There are multiple ways to customize the model explorer; one of them is to define a new tool window.

You can act on the default model explorer representation and take into account the namespace associated to the domain class of a model element.

A smart Class designer

The sample solution coming along with this paper is, as described in the introduction, based on the class diagram DSL template. If one of Rosario’s promises is to offer a set of new diagrams built on DSL tools, it would be very useful if these diagrams provide prototyping capability. For instance, a class diagram could assist developers by offering by default a set of code design patterns (gang of four…) and offering a simple sharing capability between users following code snippet footsteps.

Special thanks

Alain METGE (ASF), Nicolas FARCET (Thales), Jean Marc PRIEUR (Microsoft), François MERAND(Microsoft)


Download the complete solution here.

A pdf version of the article is available here.


Creative Commons License
Snip your dsl into prototypes by MEXEDGE is licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License.
Based on a work at www.netfxfactory.org.

Tagged: ,
Posted in: Modeling