Multiply Dsl points of view

Posted on January 13, 2009 by

1


DSL Tools are becoming more popular everyday and naturally questions are ncreasing in volume on MSDN forums dedicated to Visual Studio Extensibility. However, this act is not the only reason to advocate that DSL Tools gained in maturity. It is better for now to focus on a recurring request in the form of multiple views of a single diagram. This is mainly derived  from the fact that models implemented using DSL Tools are getting bigger and bigger by the day and it becomes quickly important to be able to occult some aspects of the model while putting the emphasis on others. The goal of this article is to present a solution to implement such functionality.

image

Figure 1 – Multiply Dsl points of view

Alan Cameron Wills told us

As soon as 2005, Alan Cameron Wills suggested a way to put in place this mechanism as an answer to a question on the above mentioned forum. However, while the answer clearly paved the way of the solution presented here, he omitted a few steps when described the procedure. In conjunction with the sparse level of documentation available around this area, this probably explains why no one came up with a solution yet.

With this starting point, a few alternative ways have been pursued. One based on the creation of a DocData instance for each diagram that needs to be displayed has been successful… until one of Alan’s missing steps reminds you that this might be more complex than originally anticipated. The revelator has been the discovery of the ProvideEditorLogicalView attribute that can decorates the Package class. This new attribute tends to think that Alan’s suggestion might be incomplete and only
regret the low level of documentation around this area. Using this attribute activates 2 mechanisms:  OpenView (method defined in DocData base class), MapLogicalView (method owned by EditorFactory).

As we will explain in this article, the efforts required to enable multiple diagrams are not limited to only overriding these two methods. Some actions will necessary on both DSL Tools and Visual Studio sides. This implies therefore working on two projects: Dsl and DslPackage.

A lot of choices will be made during the implementation of multiple diagrams and will be explained during the course of this article. The readers are invited to submit their though and opinions on these choices.

DslPackage

Developments made in this project are focused mostly around 3 classes. Not surprisingly, it is about customizing classes related to document management within Visual Studio: EditorFactory, DocView and DocData. The mechanisms involved by DSL Tools make a large use of these and even generate an implementation of them.

EditorFactory

The work required on this class is only overriding 2 methods:

  • The first one was mentioned earlier and is about taking advantage of the MapLogicalView method in order to support multiple diagrams on a single model.
  • The second one is about using the previous override (MapLogicalView) in order to indicate to the class representing a document view which diagram is associated with it. By default, none is defined.

MapLogicalView

A first question arises: « Should a logical view be associated to each diagram type that will be defined on top of the default diagram generated by the DSL Tools?”.

The short answer is no. This is not a unique identifier that will associate a logical view with the diagram to display, this is the context associated to the display request. A single identifier thus a single logical view is used in our case.

Therefore the mechanism relies on the second parameter of the MapLogicalView method. This parameter is essential in order to find which diagram to display. Indeed, the purpose of this method is to return a string that identifies a logical view specified via an id and a context. In our sample, we return the context as a string.

Even if the solution proposed relies on a single view identifier, each diagram instance will have a dedicated document view instance as described in the next paragraph.

if (logicalView == Constants.LogicalView1)
{
   return (string)viewContext;
}

CreateDocView

The purpose of this method is to create a document view based on several parameters, one of them being provided by the call to the MapLogicalView method.

It also allows specifying the text that will be displayed in the tab title that will host the view and therefore the diagram.

The goal is to specialize a view based on the diagram to display. This method now returns an instance of the DocView class taking into account the result of the MapLogicalView method. The DocView class must therefore expose a new constructor handling this new parameter.

editorCaption = string.Format(" [{0}]", physicalView);
return new ExtendedClassDiagramDocView(docData,
               this.ServiceProvider, physicalView);

DocView

The work required in this area is limited to 2 actions:

  • Develop a new constructor in order to handle the parameter returned by the  MapLogicalView method,
  • Override the LoadView method in order to use this new parameter

The first action does not require any further explanations and it is almost the case for the second. Indeed, in the context of the example provided, this is the name of the diagram that is returned by the MapLogicalView method that is being used as the identifier of the physical view. This being said, overriding the LoadView method is only about not using the first item in the list of diagrams loaded by DSL Tools but the one with the same name as the identifier.

l_diagram = string.IsNullOrEmpty(m_physicalView) ?
    l_diagramList[0] : l_diagramList.Find(
                delegate(Diagram diagram)
                { return diagram.Name == m_physicalView; });
if (l_diagram != null)
{
    this.Diagram = l_diagram;
    return true;
}

This being said, a new question arises : Can a user create diagrams if the model has already been loaded?

The sample provided allow such a functionality. In order to allow this, when a new diagram is created it is synchronized with the model. Therefore if this new diagram displays elements of the model created by its own creation, the graphics associated to the elements will be created when the diagram  is created or loaded. This synchronization mechanism relies on the definition of a rule with the Dsl project actived when creating a diagram element.

([RuleOn(typeof(Diagram), FireTime = TimeToFire.TopLevelCommit)])

The details of this rule will be presented later on when dealing specifically with the Dsl project.

DocData

Today, DSL Tools allows saving model and its associated diagram in dedicated files. Supporting multiple diagrams supports the same level separation for each diagram. Therefore, a file is created for each new diagram. However, it is good to maintain the same user experience within Visual Studio. A visual link should help identifying the file in which the model is saved and the files where the diagrams are saved.  Offering the same user experience can lead to navigation issues if the number of diagrams thus the number of associated files reach high numbers. Furthermore, opening a “diagram” file, as opposed to a “model” file, does not trigger a dedicated visual designer. Because the diagrams are saved using XML, Visual Studio uses its default XML editor. It is therefore not relevant to allow opening this kind of files via a double click in the solution explorer. It is also important to maintain features that are now mainstream such as “Save as” or “Include”.

To answer each of these requirements, it has been decided to leverage the OpenXML format and specially its dedicated library that is now offered in the .Net Framework through the System.IO.Packaging namespace. All the files related to the diagrams will therefore be saved inside a single file. The later on top of saving the diagrams on disk will also compress these. As demonstrated in the sample accompanying this article, the users will only face, even though we introduce multiple diagrams, only two files one with a “diagramx” extension similar to the new file extension convention
used within Microsoft Office products.

This being said DocData class does not have to handle the serialization mechanism involved in the DSL Tools. It delegates this work to the SerializationHelper class that will be details later on.

The modifications required on the DocData class on top of overloading the OpenView class are twofold:

  • Adapt the mechanism managing the .diagram files in order to handle .diagramx. It is nothing more than copying one mechanism and apply it to the second,
  • Call within the load and save methods the serialization mechanisms supporting multiple diagrams as described earlier.

The actions impact very sparingly the default behavior of the DocData class therefore readers are invited to refer to the source code provided in the sample should they require further details.

OpenView

As mentioned earlier, diagrams can be instantiated on the fly even if the model has been modified. Therefore, this is when the OpenView method is called that a check is done in order to identify which type of diagram is to be displayed. If the diagram in question does not exist yet, an instance will be created within this method. To do so, only the parameter specifying the logical view context is taken into account. This parameter is used to determine which type of diagram is to be instantiated.

Of course, a next release of the solution exposed here could include a more elaborated mechanism to link a type of diagram to string (dedicated attribute decorating the class associated to a diagram etc…) and not only rely on the name property of the diagram. This would allow adding new parameters to the diagram instantiation.

It is important to note that this should happen within a DSL Tools transaction because this new diagram instance has to be inserted into the DSL Tools store.

Following this instantiation, the OpenView method exposed by the base class is called therefore leveraging the Visual Studio mechanisms orchestrating the call to the methods mentioned earlier (MapLogicalView etc…).

The most curious readers will notice that the DocData class provided in the DSL Tools API: ModelingDocData specifies its own implementation of the OpenView method. Because the DocData class generated by the DSL Tools inherits from it, it leverages this implementation by default.  Using this one would constraint us to use the diagram instance to be displayed as the visualization context. This is an option that be used later on. We have every reason to think that the DSL Tools development team wanted to offer multiple diagrams support. Digging into one of the first betas available confirms this though.

public override void OpenView(Guid logicalView, object viewContext)
{
    string l_viewContext = (string)viewContext;
    Diagram l_diagram =
        this.Store.ElementDirectory.FindElements().
            SingleOrDefault(
                diagram => diagram.Name == l_viewContext);
    if (l_diagram == null)
    {
        using (Transaction transaction =
                this.Store.TransactionManager.BeginTransaction(
                    "DocData.OpenView", true))
        {
            if (l_viewContext == "Flow")
            {
                l_diagram = new FlowDiagram(this.Store,
                    new PropertyAssignment(
                      Diagram.NameDomainPropertyId,
                      l_viewContext));

            }
            else if (l_viewContext == "Package")
            {
                l_diagram = new PackageDiagram(this.Store,
                    new PropertyAssignment(
                      Diagram.NameDomainPropertyId,
                      l_viewContext));
            }
            else
            {
                throw new NotImplementedException();
            }
            l_diagram.ModelElement = this.RootElement;
            transaction.Commit();
        }
    }
    base.OpenView(logicalView, viewContext);
}

ProjectItem Templates

The previous paragraph introduced it, a new file with diagramx extension is to replace the default .diagram file in order to enable multiple diagram. This involves modifying the item project models produced by the Dsl solution in order to replace .diagram in diagramx.

The files to modify are xxx.diagram, CSharp.tt and VisualBasic.tt.

Package

The work done on the DslPackage project will be complete once the two actions on the Package class will be done:

  • Decorate the Package class with the LogicalView attribute as described in the introduction, this enables multiple views of the same model.
  • Decorate the Package class with the RelatedFiles attribute in order to support the new .diagramx extension.
[ProvideEditorLogicalViewAttribute(typeof(
    ExtendedClassDiagramEditorFactory),
    Constants.LogicalView1Value, IsTrusted = true)]
[ProvideRelatedFile("." + Constants.DesignerFileExtension,
    Constants.DiagramxExtension,
    ProjectSystem = ProvideRelatedFileAttribute.CSharpProjectGuid,
    FileOptions = RelatedFileType.FileName)]
[ProvideRelatedFile("." + Constants.DesignerFileExtension,
    Constants.DiagramxExtension,
    ProjectSystem = ProvideRelatedFileAttribute.VisualBasicProjectGuid,
    FileOptions = RelatedFileType.FileName)]
internal sealed partial class ExtendedClassDiagramPackage
{
}

Dsl

The actions undertaken on the DslPackage paved the way to supporting multiple views of a single model within Visual Studio. As mentioned, these actions are not enough and require modifying the Dsl project as well. The first one is to extend the serialization mechanisms in order to take into account multiple diagrams and OpenXml to save them on disk. These mechanisms are leveraged within the DocData class and exposed by the serialization helper class.

SerialisationHelper

The modifications to this class won’t involve overrides but introduce new methods:
LoadModelAndDiagrams, SaveModelAndDiagrams and SaveDiagrams.

Readers would certainly notice that numerous developments mentioned since the beginning of this article could have been generated. This will be one of the remarks mentioned in the conclusion of this article.

LoadModelAndDiagrams

This new method simply uses the default logic generated by the DSL Tools and only clearly separates what is related to loading the model and what is related to loading a diagram (which has its own dedicated method). The first advantage of this is to easily call it for each diagram.

The last issue to resolve is that all the files associated with the multiple diagrams are compressed into a single file. This is a no brainer when leveraging the System.IO.Packaging namespace offered in WindowsBase.dll as described below.

// Load the model
ModelElement l_rootElement = this.LoadModel(serializationResult,
    store, modelFileName, schemaResolver, validationController);

// Use OpenXml Api
using (Package pkgOutputDoc = Package.Open(diagramxFileName,
    FileMode.Open, FileAccess.Read))
{
    foreach (var packagePart in pkgOutputDoc.GetParts())
    {
        this.LoadDiagram(serializationResult, store,
            l_rootElement,
            packagePart.GetStream(FileMode.Open,
                FileAccess.Read),
            schemaResolver, validationController);
    }
}

As you probably have noticed, it is required to create our own SerializationvalidationObserver class in order to follow the way the method generated by DSL Tools works. Indeed, the generated implementation for this class is set to private thus preventing us from reusing it.

SaveModelAndDiagrams

This paragraph starting with a similar remark than the previous one: it is a shame that the InternalSaveModel and InternalSaveDiagram are set to private. This forced us to duplicate them in order to be able use the functionalities they provide in the SaveModelAndDiagram.

There is no fancy work done in this method, the only highlight would be the use once again of the System.IO.Packaging namespace in order to produce only file for all the diagrams.

using (Package pkgOutputDoc = Package.Open(diagramxFileName,
    FileMode.Create, FileAccess.ReadWrite))
{
    foreach (var memoryStream in l_memoryStreamDictionary.Keys)
    {
        byte[] l_bytes = memoryStream.ToArray();
        Uri uri = new Uri(string.Format("/diagrams/{0}",
            l_memoryStreamDictionary[memoryStream]),
            UriKind.Relative);
        PackagePart part = pkgOutputDoc.CreatePart(uri,
            System.Net.Mime.MediaTypeNames.Text.Xml,
            CompressionOption.Maximum);
        using (var partStream = part.GetStream(FileMode.Create,
            FileAccess.Write))
        {
            partStream.Write(l_bytes, 0, l_bytes.Length);
        }
    }
}

Serializer

So far, we did not mentioned how a new diagram is defined. Far from wanting to maintain an unbearable suspense, this paragraph partially lifts the veil. A new diagram will inherit from the default diagram generated by the DSL Tools. This choice implies a few pros:

  • It allows using the DSL Tools visual designer to define the graphical elements associated with the model, the user experience is therefore just slightly changed.
  • It allows using the default underlying mechanisms involved with the diagram
    management and specially its serialization.

However this decision involves modifying the serializer associated to the default diagram (ClassDiagramSerializer in the sample code). Indeed, when reloading a diagram the correct diagram type must be reloaded.

To do so, it has been decided to add to the Xml representation a new attribute named type. It indicates which type of diagram is to be instantiated. The management of this new attribute requires to overload the WritePropertiesAsAttributes method.

Of course, other solutions could have been chosen such as associating a dedicated element name for each type of diagram, similarly to what is done usually in XAML.

WritePropertiesAsAttributes

To activate the call to the overloaded method, it is required to add at least one DomainProperty to the default diagram.

Then, we only need to use to WriteAttributeString method exposed by the XmlWriter class (an instance of the writer is provided as a parameter of this method)

protected override void WritePropertiesAsAttributes(
    SerializationContext serializationContext,
    ModelElement element, XmlWriter writer)
{
    writer.WriteAttributeString("type",
        element.GetType().FullName);
    base.WritePropertiesAsAttributes(serializationContext,
        element, writer);
}

CreateInstance

If a diagram’s Xml representation now indicates its type, we need to use this information when instantiating a diagram using its representation.

To do so, it is required to override the CreateInstance method of the Serializer associated to the default diagram.

protected override ModelElement CreateInstance(
    SerializationContext serializationContext,
    XmlReader reader, Partition partition)
{
    String l_typeAttribute = reader.GetAttribute("type");
    Assembly l_assembly = this.GetType().Assembly;
    Type l_type = l_assembly.GetType(l_typeAttribute);
    ModelElement l_diagram =
        (ModelElement)Activator.CreateInstance(
            l_type, partition);
    return l_diagram;
}

FixUpDiagramOnElementAddedRule

All the mechanisms allowing saving and loading a diagram have been created. As described in the previous chapter, it is also required to modify the synchronization mechanisms that provide an up to date view of the diagram event if the underlying model has been modified.

By default, DSL Tools relies on a mechanism based on a generate rules: FixUpDiagram.

As we have done in some other areas during this article, we have to modify this mechanism in order to support multiple diagrams. Unfortunately, this modification is not trivial, it is easier to deactivate the default rule and create new one to handle our needs.

The modification mostly revolves around on a single method : GetParentForRelationship. For those who are fans of Reflector, the generated version of the GetParentRelationship method selects the first associated diagram. This cannot be sufficient when several diagrams need to be updated. This is why a new method has been developed that takes a diagram instance as an input parameter and searches its associated parent…

GetParentForRelationship

This method does not rely on any specificity related to the defined Dsl langage. It could even be provided within the DSL Tools SDK. As mentioned, it must take into account, when searching for a parent element, the instance of the diagram provided as a parameter.

if (shape != null && shape.Diagram == diagram)
{
    sourceShape = shape;
    break;
}

It could also not return any parent, indeed a diagram can have no element or relation to display.

if (sourceShape == null || targetShape == null)
{
   return null;
}

ElementAdded

The modification to operate here is to provide a diagram instance to the GetParentForRelationship method. This will be done for every diagrams in the In Memory Store (IMS).

if (childElement is ElementLink)
{
    ReadOnlyCollection diagrams =
        childElement.Store.ElementDirectory.
           FindElements();
    for (int i = 0; i < diagrams.Count; i++)
    {
        parentElement = GetParentForRelationship(
            diagrams[i], (ElementLink)childElement);
        if (parentElement != null)
        {
            DslDiagrams::Diagram.FixUpDiagram(
                parentElement, childElement);
        }
    }
}

Fixup

This method uses the same logic as the ElementAdded method but only on the diagram instance provided in parameter. This static method will be used by the rule triggered when a new diagram is added. This rule aims to synchronizing the diagram with the model.

The other methods exposed by this class are exact copies of those provided by the FixupDiagram class generated by the DSL Tools. This requirement highlights the fact that a set of T4 templates could greatly ease the process of supporting multiple diagrams .

DiagramAddRule

The sample provided with this article allows creating a new diagram after loading the model. This functionality requires the activation of several mechanisms among which the synchronization of the diagram with the model.

This synchronization is based on the FixUp rule presented earlier. However, it is not about browsing all the model elements and apply them the FixUp rule with the newly created diagram instance. It has to be done based on the nature of the elements and on their graphical representation within the diagram. In the sample provided, the elements representing an association DomainRelationship are dealt with only after all the others.

[RuleOn(typeof(Diagram), FireTime = TimeToFire.TopLevelCommit)]
internal sealed class DiagramAddRule : AddRule
{
    public override void ElementAdded(ElementAddedEventArgs e)
    {
        for (int i = 0; i < l_modelElementList.Count; i++)
        {
            ModelElement item = l_modelElementList[i];
            if (item.GetDomainClass().DomainModel.Id ==
                l_domainModelInfo.Id &&
                item.Id != l_diagram.ModelElement.Id &&
                !(item is ModelRootHasTypes))
            {
                FixUpDiagramOnElementAddedRule.Fixup(
                    l_diagram, item);
            }
        }
        for (int i = 0; i < l_elementLinkList.Count; i++)
        {
            ModelElement item = l_elementLinkList[i];
            if (item.GetDomainClass().DomainModel.Id ==
                l_domainModelInfo.Id &&
                item.Id != l_diagram.ModelElement.Id &&
                !(item is ModelRootHasTypes))
            {
                FixUpDiagramOnElementAddedRule.Fixup(
                    l_diagram, item);
            }
        }
    }
}

Define a new diagram

All the plumbing required to support multiple diagrams are now completed.

It is time to present the steps required to define a new diagram on top of the default one. The steps listed below are based on the sample provided.

1. Open Solution Explorer.

2. Right-click on the CustomCode folder of the Dsl project, Add Class.

3. Enter the name of your diagram.

4. Modify the namespace if required.

5. Inherits from the ClassDiagram class.

6. To declare your new class to DSL Tools.

6.1 Decorate your class with DomainObjectId attribute by specifying only
a new guid.

[DomainObjectId("xx-xx-xx-xx-xx")]

6.2 Add your class to the custom type collection returned by
GetCustomDomainModelTypes method of your DomainModel class.

public partial class ExtendedClassDiagramDomainModel
{
    protected override Type[] GetCustomDomainModelTypes()
    {
        return new Type[]
        {
            typeof(xxx),
        };

7. Override the ShouldAddShapeForElement method to filter the model elements displayed by this diagram.

8. Customize your diagram by following DSL Tools guidelines. Use the DslDefinition editor to define shapes that will be used in your new diagram.

Conclusion

The sample provided here is like a wink to Visual Studio 2010. Similarly to VS10, it comes to the rescue of the lonesome class designer which was one of Visual Studio 2005’s highlights.

That being said, the sample does not generate any code. This is not the only discriminator, if Visual Studio 2010 has also used the DSL Tools, they are not based on a single diagram but leverages the Bus Designer known as Codename Backplane that will ship with the next version of Visual Studio. Even if it can be an answer to the issue being addressed in this article, a lot of questions arises regarding the rules definition, the usage of overriding mechanisms as well as the developer user experience. It seems that most of the underlying mechanisms are already provided in the DSL Tools to support multiple diagrams on a single model. Will users have both, complementary, solutions provided out of the box by Microsoft at some point?

What’s next

The solution presented here is part of the Panoptes project. Because this project does not require collaborative tools it has not been ported to Codeplex. All the code developed within this project is included in this article. In addition to what has been detailed in the previous chapters, a filtering mechanism on the toolbox has also been implemented.

The next step is the development of a set of T4 templates in order to improve the user experience. As indicated before, all the code produced here could be generated. Once these templates created, adding a new Domain Specific language Designer project template leveraging the revised T4 templates.

By this time, maybe such a feature will be provided out of the box by the DSL Tools. Chances are that such a feature would lead to an extended Dsl Definition Designer supporting the definition of multiple diagrams (by providing dedicated swimlanes?).

In parallel to these «industrialization » improvements, new functionalities could also be developed such as filtering the property grid based on the diagram displayed or activating drag and drop between diagrams.

If this article ignites some vocations in contributing to this project, Codeplex can be a place of choice to extend on this.

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
Multiply Dsl points of view (Panoptes) 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