Integrate models by using Visual Studio ModelBus

Visual Studio ModelBus provides a method for creating links between models and from other tools into models. For example, you could link domain-specific language (DSL) models and UML models. You can create an integrated set of DSLs.

Warning

The Visual Studio ModelBus Extension, described in this article, is no longer available for download. However, these instructions still work for those users who already have this extension installed.

ModelBus lets you create a unique reference to a model or to a specific element inside a model. This reference can be stored outside the model, for example, in an element in another model. When, on a later occasion, a tool wants to obtain access to the element, the ModelBus infrastructure loads the appropriate model and return the element. If you want, you can display the model to the user. If the file can't be accessed in its previous location, ModelBus asks the user to find it. If the user finds the file, ModelBus updates all the references to that file.

Note

In the current Visual Studio implementation of ModelBus, the linked models must be items in the same Visual Studio solution.

For more information about the ModelBus Extension, see the following resources:

Note

The Text Template Transformation component is automatically installed as part of the Visual Studio extension development workload. You can also install it from the Individual components tab of Visual Studio Installer, under the SDKs, libraries, and frameworks category. Install the Modeling SDK component from the Individual components tab.

Provide access to a DSL

Before you can create ModelBus references to a model or its elements, define a ModelBusAdapter for the DSL. The easiest way to define one is to use the Visual Studio ModelBus Extension, which adds commands to the DSL Designer.

To expose a DSL definition to ModelBus

  1. Open the DSL definition file. Right-click the design surface and then select Enable ModelBus.

  2. In the dialog, choose I want to expose this DSL to the ModelBus. You can choose both options if you want this DSL both to expose its models and to consume references to other DSLs.

  3. Select OK. A new project ModelBusAdapter is added to the DSL solution.

  4. If you want to access the DSL from a text template, you must modify AdapterManager.tt in the new project. Omit this step if you want to access the DSL from other code such as commands and event handlers. For more information, see Using Visual Studio ModelBus in a Text Template.

    1. Change the base class of AdapterManagerBase to VsTextTemplatingModelingAdapterManager.

    2. Near the end of the file, insert this attribute in front of class AdapterManager:

      [Microsoft.VisualStudio.Modeling.Integration.HostSpecific(HostName)]

    3. In the NuGet Package Manager of ModelBusAdapter project, add a NuGet PackageReference to Microsoft.VisualStudio.TextTemplating.Modeling.

      If you want to access the DSL both from text templates and from other code, you need two adapters, one modified and one unmodified.

  5. Select Transform All Templates.

  6. Rebuild the solution.

    ModelBus can now open instances of this DSL.

    The folder ModelBusAdapters\bin* contains the assemblies built by the Dsl project and the ModelBusAdapters project. To reference this DSL from another DSL, import these assemblies.

Ensure that elements can be referenced

By default, Visual Studio ModelBus adapters use the GUID of an element to identify it. These identifiers must be persisted in the model file.

To ensure that element IDs are persisted:

  1. Open DslDefinition.dsl.

  2. In DSL Explorer, expand Xml Serialization Behavior, then Class Data.

  3. For each class to which you want to create ModelBus references:

    Select the class node, and in the Properties window, make sure that Serialize ID is set to true.

Alternatively, if you want to use element names to identify elements instead of GUIDs, you can override parts of the generated adapters. Override the following methods in the adapter class:

  • Override GetElementId to return the identifier you want to use. This method is called when creating references.
  • Override ResolveElementReference to locate the correct element from a ModelBus reference.

Access a DSL from another DSL

You can store ModelBus references in a domain property in a DSL and write custom code that uses them. You can also let the user create a ModelBus reference by picking a model file and an element within it.

To enable a DSL to use references to another DSL, you should first make it a consumer of model bus references.

To enable a DSL to consume references to an exposed DSL

  1. In the DSL Definition diagram, right-click the main part of the diagram and then select Enable ModelBus.

  2. In the dialog, select I want to enable this model to consume model bus references.

  3. In the Dsl project of the consuming DSL, add the following assemblies to the project references. These assemblies (.dll files) are in the ModelBusAdapter\bin\* directory of the exposed DSL.

    • The exposed DSL assembly, for example Fabrikam.FamilyTree.Dsl.dll

    • The exposed model bus adapter assembly, for example Fabrikam.FamilyTree.ModelBusAdapter.dll

  4. Add the following .NET assemblies to the project references of the consuming DSL project.

    • Microsoft.VisualStudio.Modeling.Sdk.Integration.dll
    • Microsoft.VisualStudio.Modeling.Sdk.Integration.Shell.dll

To store a ModelBusReference in a domain property

  1. In the DSL Definition of the consuming DSL, add a domain property to a domain class and set its name.

  2. In the Properties window, with the domain property selected, set Type to ModelBusReference.

    At this stage, program code can set the property value. The value is read-only in the Properties window.

    You can allow users to set the property with a specialized ModelBusReference editor. There are two versions of this editor or picker. One allows users to choose a model file and the other allows users to choose a model file and an element within the model.

To allow the user to set a ModelBusReference in a domain property

  1. Right-click the domain property and then select Edit ModelBusReference specific properties. The Model Bus Picker dialog opens.

  2. Set the appropriate Kind of ModelBusReference to a model or to an element inside a model.

  3. In file dialog filter string, enter a string such as Family Tree files |*.ftree. Substitute the file extension of your exposed DSL.

  4. If you chose to reference an element in a model, you can add a list of types that the user can select, for example Company.FamilyTree.Person.

  5. Select OK, and then select Transform All Templates in the Solution Explorer toolbar.

    Warning

    If you have not selected a valid model or entity, the OK button has have no effect, even though it might appear enabled.

  6. If you specified a list of target types, such as Company.FamilyTree.Person, then you must add an assembly reference to your DSL project, referencing the DLL of the target DSL, for example, Company.FamilyTree.Dsl.dll.

To test a ModelBusReference

  1. Build both the exposed and consuming DSLs.

  2. Run one of the DSLs in experimental mode by pressing F5 or CTRL+F5.

  3. In the Debugging project in the experimental instance of Visual Studio, add files that are instances of each DSL.

    Note

    Visual Studio ModelBus can only resolve references to models that are items in the same Visual Studio solution. For example, you cannot create a reference to a model file in another part of your file system.

  4. Create some elements and links in the instance of the exposed DSL, and save it.

  5. Open an instance of the consuming DSL, and select a model element that has a model bus reference property.

  6. In Properties window, double-click the model bus reference property. The picker dialog opens.

  7. Select Browse and select the instance of the exposed DSL.

    If you specified the element-specific model bus reference, the picker also lets you choose an item in the model.

Create references in program code

When you want to store a reference to a model or an element inside a model, create a ModelBusReference. There are two kinds of ModelBusReference: model references and element references.

To create a model reference, you need the AdapterManager of the DSL of which the model is an instance, and the file name or Visual Studio project item of the model.

To create an element reference, you need an adapter for the model file, and the element you want to refer to.

Note

With the Visual Studio ModelBus, you can create references only to items in the same Visual Studio solution.

Import the exposed DSL assemblies

In the consuming project, add project references to the DSL and ModelBusAdapter assemblies of the exposed DSL.

For example, suppose that you want to store ModelBus references in elements of a MusicLibrary DSL. The ModelBus references refer to elements of the FamilyTree DSL. In the References node, in the Dsl project of the MusicLibrary solution, add references to the following assemblies:

  • Fabrikam.FamilyTree.Dsl.dll. The exposed DSL.

  • Fabrikam.FamilyTree.ModelBusAdapters.dll The ModelBus adapter of the exposed DSL.

  • Microsoft.VisualStudio.Modeling.Sdk.Integration

  • Microsoft.VisualStudio.Modeling.Sdk.Integration.Shell

    These assemblies can be found in the ModelBusAdapters project of the exposed DSL, under *bin\**.

    In the code file where you create references, you typically have to import these namespaces:

// The namespace of the DSL you want to reference:
using Fabrikam.FamilyTree;  // Exposed DSL
using Fabrikam.FamilyTree.ModelBusAdapters;
using Microsoft.VisualStudio.Modeling.Integration;
using System.Linq;
...

To create a reference to a model

To create a model reference, you access the AdapterManager for the exposed DSL, and use it to create a reference to the model. You can specify either a file path, or a EnvDTE.ProjectItem.

From the AdapterManager, you can obtain an Adapter, which provides access to individual elements in the model.

Note

You must dispose an Adapter when you have finished with it. The most convenient way to achieve this is with a using statement. The following example illustrates this.

// The file path of a model instance of the FamilyTree DSL:
string targetModelFile = "TudorFamilyTree.ftree";
// Get the ModelBus service:
IModelBus modelBus =
    this.Store.GetService(typeof(SModelBus)) as IModelBus;
// Get an adapterManager for the target DSL:
FamilyTreeAdapterManager manager =
    (modelbus.GetAdapterManager(FamilyTreeAdapter.AdapterId)
     as FamilyTreeAdapterManager;
// or: (modelBus.FindAdapterManagers(targetModelFile).First())
// or could provide an EnvDTE.ProjectItem

// Create a reference to the target model:
// NOTE: the target model must be a file in this project.
ModelBusReference modelReference =
     manager.CreateReference(targetModelFile);
// or can use an EnvDTE.ProjectItem instead of the filename

// Get the root element of this model:
using (FamilyTreeAdapter adapter =
     modelBus.CreateAdapter(modelReference) as FamilyTreeAdapter)
{
  FamilyTree modelRoot = adapter.ModelRoot;
  // Access elements under the root in the usual way:
  foreach (Person p in modelRoot.Persons) {...}
  // You can create adapters for individual elements:
  ModelBusReference elementReference =
     adapter.GetElementReference(person);
  ...
} // Dispose adapter

If you want to be able to use modelReference later, you can store it in a domain property that has the External Type ModelBusReference:

using Transaction t = this.Store.TransactionManager
    .BeginTransaction("keep reference"))
{
  artist.FamilyTreeReference = modelReference;
  t.Commit();
}

To allow users to edit this domain property, use ModelReferenceEditor as the parameter in the Editor attribute. For more information, see Allow the User to Edit a Reference.

To create a reference to an element

The adapter that you created for the model can be used to create and resolve references.

// person is an element in the FamilyTree model:
ModelBusReference personReference =
  adapter.GetElementReference(person);

If you want to be able to use elementReference later, you can store it in a domain property that has the External Type ModelBusReference. To allow users to edit it, use ModelElementReferenceEditor as the parameter in the Editor attribute. For more information, see Allow the User to Edit a Reference.

Resolving references

If you have a ModelBusReference (MBR) you can obtain the model or the model element to which it refers. If the element is presented on a diagram or other view, you can open the view and select the element.

You can create an adapter from an MBR. From the adapter, you can obtain the root of the model. You can also resolve MBRs that refer to specific elements within the model.

using Microsoft.VisualStudio.Modeling.Integration; ...
ModelBusReference elementReference = ...;

// Get the ModelBus service:
IModelBus modelBus =
    this.Store.GetService(typeof(SModelBus)) as IModelBus;
// Use a model reference or an element reference
// to obtain an adapter for the target model:
using (FamilyTreeAdapter adapter =
   modelBus.CreateAdapter(elementReference) as FamilyTreeAdapter)
   // or CreateAdapter(modelReference)
{
  // Get the root of the model:
  FamilyTree tree = adapter.ModelRoot;

  // Get a model element:
  MyDomainClass mel =
    adapter.ResolveElementReference<MyDomainClass>(elementReference);
  if (mel != null) {...}

  // Get the diagram or other view, if there is one:
  ModelBusView view = adapter.GetDefaultView();
  if (view != null)
  {
   view.Open();
   // Display the diagram:
   view.Show();
   // Attempt to select the shape that presents the element:
   view.SetSelection(elementReference);
  }
} // Dispose the adapter.

To resolve ModelBusReferences in a text template

The DSL you want to access must have a ModelBus Adapter that has been configured for access by text templates. For more information, see Providing Access to a DSL.

Typically, you access a target DSL using a Model Bus Reference (MBR) stored in a source DSL. Your template therefore includes the directive of the source DSL, plus code to resolve the MBR. For more information about text templates, see Generating Code from a Domain-Specific Language.

<#@ template debug="true" hostspecific="true"
inherits="Microsoft.VisualStudio.TextTemplating.Modeling.ModelBusEnabledTextTransformation" #>
<#@ SourceDsl processor="SourceDslDirectiveProcessor" requires="fileName='Sample.source'" #>
<#@ output extension=".txt" #>
<#@ assembly name = "Microsoft.VisualStudio.Modeling.Sdk.Integration.11.0" #>
<#@ assembly name = "System.Core" #>
<#@ assembly name = "Company.CompartmentDragDrop.Dsl.dll" #>
<#@ assembly name = "Company.CompartmentDragDrop.ModelBusAdapter.dll" #>
<#@ import namespace="Microsoft.VisualStudio.Modeling.Integration" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="Company.CompartmentDragDrop" #>
<#@ import namespace="Company.CompartmentDragDrop.ModelBusAdapters" #>
<# // Get source root from directive processor:
  ExampleModel source = this.ExampleModel;
  // This DSL has a MBR in its root:
using (ModelBusAdapter adapter = this.ModelBus.CreateAdapter(source.ModelReference) as ModelBusAdapter)
  {
  ModelBusAdapterManager manager = this.ModelBus.FindAdapterManagers(this.Host.ResolvePath("Sample.compDD1")).FirstOrDefault();
  ModelBusReference modelReference =
    manager.CreateReference(this.Host.ResolvePath("Sample.compDD1"));

  // Get the root element of this model:
  using (CompartmentDragDropAdapter adapter =
     this.ModelBus.CreateAdapter(modelReference) as CompartmentDragDropAdapter)
  {
    ModelRoot root = adapter.ModelRoot;
#>
[[<#= root.Name #>]]
<#
  }
#>

For more information and a walkthrough, see Using Visual Studio ModelBus in a Text Template

Serialize a ModelBusReference

If you want to store a ModelBusReference (MBR) in the form of a string, you can serialize it:

string serialized = modelBus.SerializeReference(elementReference);
// Store it anywhere, then get it back again:
ModelBusReference elementReferenceRestored =
    modelBus.DeserializeReference(serialized, null);

An MBR that is serialized in this manner is independent of context. If you're using the simple file-based Model Bus Adapter, the MBR contains an absolute file path. If the instance model files never move, this serialization is sufficient. However, the model files are typically items in a Visual Studio project. Your users expect to be able to move the whole project to different parts of the file system. They also expect to be able to keep the project under source control and open it on different computers. Path names should therefore be serialized relative to the location of the project that contains the files.

Serializing relative to a specified file path

A ModelBusReference contains a ReferenceContext, which is a dictionary in which you can store information such as the file path relative to which it should be serialized.

To serialize relative to a path:

elementReference.ReferenceContext.Add(
   ModelBusReferencePropertySerializer.FilePathSaveContextKey,
   currentProjectFilePath);
string serialized = modelBus.SerializeReference(elementReference);

To retrieve the reference from the string:

ReferenceContext context = new ReferenceContext();
context.Add(ModelBusReferencePropertySerializer.FilePathLoadContextKey,
    currentProjectFilePath);
ModelBusReference elementReferenceRestored =
    modelBus.DeserializeReference(serialized, context);

ModelBus references created by other adapters

The following information is useful if you want to create your own adapter.

A ModelBusReference (MBR) consists of two parts: the MBR header, which is deserialized by the model bus, and an adapter-specific that is handled by the specific adapter manager. This approach lets you provide your own adapter serialization format. For example, you could reference a database rather than a file, or you could store additional information in the adapter reference. Your own adapter can place additional information in the ReferenceContext.

When you deserialize an MBR, you must provide a ReferenceContext, which is then stored in the MBR object. When you serialize an MBR, the stored ReferenceContext is used by the adapter to help generate the string. The deserialized string doesn't contain all the information in the ReferenceContext. For example, in the simple file-based adapter, the ReferenceContext contains a root file path. The path isn't stored in the serialized MBR string.

The MBR is deserialized in two stages:

  • ModelBusReferencePropertySerializer is the standard serializer that deals with the MBR header. It uses the standard DSL SerializationContext property bag, which is stored in the ReferenceContext using the key ModelBusReferencePropertySerializer.ModelBusLoadContextKey. In particular, the SerializationContext should contain an instance of ModelBus.

  • Your ModelBus Adapter deals with the adapter-specific part of the MBR. It can use additional information stored in the ReferenceContext of the MBR. The simple file-based adapter keeps root file paths using the keys FilePathLoadContextKey and FilePathSaveContextKey.

    An adapter reference in a model file is deserialized only when it's used.

Create a model

Creating, opening, and editing a model

The following fragment is taken from the State Machine sample on the VMSDK website. It illustrates the use of ModelBusReferences to create and open a model, and to obtain the diagram associated with the model.

In this sample, the name of the target DSL is StateMachine. Several names are derived from it, such as the name of the model class and the name of the ModelBusAdapter.

using Fabrikam.StateMachine.ModelBusAdapters;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Diagrams;
using Microsoft.VisualStudio.Modeling.Integration;
using Microsoft.VisualStudio.Modeling.Integration.Shell;
using Microsoft.VisualStudio.Modeling.Shell;
...
// Create a new model.
ModelBusReference modelReference =
   StateMachineAdapterManager    .CreateStateMachineModel(modelName, fileName);
//Keep reference of new model in this model.
using (Transaction t = ...)
{
  myModelElement.ReferenceProperty = modelReference;
  t.Commit();
}
// Get the ModelBus service from Visual Studio.
IModelBus modelBus = Microsoft.VisualStudio.Shell.Package.
    GetGlobalService(typeof(SModelBus)) as IModelBus;
// Get a modelbus adapter on the new model.
ModelBusAdapter modelBusAdapter;
modelBus.TryCreateAdapter(modelReference,
    this.ServiceProvider, out modelBusAdapter);
using (StateMachineAdapter adapter =
      modelBusAdapter as StateMachineAdapter)
{
    if (adapter != null)
    {
        // Obtain a Diagram from the adapter.
        Diagram targetDiagram =
           ((StandardVsModelingDiagramView)
                 adapter.GetDefaultView()
            ).Diagram;

        using (Transaction t =
             targetDiagram.Store.TransactionManager
                .BeginTransaction("Update diagram"))
        {
            DoUpdates(targetDiagram);
            t.Commit();
        }

        // Display the new diagram.
        adapter.GetDefaultView().Show();
    }
}

Validate references

The BrokenReferenceDetector tests all the domain properties in a Store that can hold ModelBusReferences. It calls the action you that provide where any action is found. This test is useful for validation methods. The following validation method tests the store on an attempt to save the model, and reports broken references in the errors window:

[ValidationMethod(ValidationCategories.Save)]
public void ValidateModelBusReferences(ValidationContext context)
{
  BrokenReferenceDetector.DetectBrokenReferences(this.Store,
    delegate(ModelElement element, // parent of property
             DomainPropertyInfo property, // identifies property
             ModelBusReference reference) // invalid reference
    {
      context.LogError(string.Format(INVALID_REF_FORMAT,
             property.Name,
             referenceState.Name,
             new ModelBusReferenceTypeConverter().
                 ConvertToInvariantString(reference)),
         "Reference",
         element);
    });
}
private const string INVALID_REF_FORMAT =
    "The '{0}' domain property of this ReferenceState instance "
  + "named '{1}' contains reference value '{2}' which is invalid";

Actions performed by the ModelBus Extension

The following information might be useful if you make extensive use of ModelBus.

The ModelBus Extension makes the following changes in your DSL solution.

When you right-click the DSL Definition diagram, select Enable Modelbus, and then select Enable this DSL to Consume the ModelBus:

  • In the DSL project, a reference should be added to Microsoft.VisualStudio.Modeling.Sdk.Integration.dll.

  • In the DSL Definition, an External Type reference is added: Microsoft.VisualStudio.Modeling.Integration.ModelBusReference.

    You can see the reference in DSL Explorer, under Domain Types. To add external type references manually, right-click the root node.

  • A new template file is added, Dsl\GeneratedCode\ModelBusReferencesSerialization.tt.

When you set the type of a domain property to ModelBusReference, and then right-click the property and select Enable ModelBusReference specific properties:

  • Several CLR attributes are added to the domain property. You can see them in the Custom Attributes field in the Properties window. In Dsl\GeneratedCode\DomainClasses.cs, you can see the attributes on the property declaration:

    [System.ComponentModel.TypeConverter(typeof(
    Microsoft.VisualStudio.Modeling.Integration.ModelBusReferenceTypeConverter))]
    [System.ComponentModel.Editor(typeof(
      Microsoft.VisualStudio.Modeling.Integration.Picker
      .ModelReferenceEditor // or ModelElementReferenceEditor
      ), typeof(System.Drawing.Design.UITypeEditor))]
    [Microsoft.VisualStudio.Modeling.Integration.Picker
      .SupplyFileBasedBrowserConfiguration
      ("Choose a model file", "Target model|*.target")]
    

When you right-click the DSL Definition Diagram, select Enable ModelBus, and select Expose this DSL to the ModelBus:

  • A new project ModelBusAdapter is added to the solution.

  • A reference to ModelBusAdapter is added to the DslPackage project. ModelBusAdapter has a reference to the Dsl project.

  • In DslPackage\source.extention.tt, |ModelBusAdapter| is added as a MEF Component.