Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DSL Tools

0.00/5 (No votes)
19 Jan 2007 1  
DSL Tools enables the construction of custom graphical designers and the generation of source code using domain-specific diagrammatic notations.

Introduction

Domain-Specific Language Tools, as you might know, enable the construction of custom graphical designers and the generation of source code using domain-specific diagrammatic notations. In other words, it will allow you to create a UML-like graphic designer in Visual Studio 2005, which will generate a .yourLanguageName file.

Using DSL Tools is quite straightforward, and you can create your own UML-like language relatively quickly, but when you want to do some more complex tasks (like adding a custom attribute to a domain property, or make your own custom tool rather than using the text templates), it can get darker and especially with the current lack of documentation.

This is what this article is all about: creating your own domain specific language and some more user friendly controls, as well as your own code generator. In the source code attached to the article, you will find a DSL tools example project that was started from the minimal language template, and to which I added the two features I'm going to expose to you. Finally, the project was created with the Visual Studio SDK 1.0 released in September.

Background

Now before starting, I'll quickly introduce the DSL Tools. This summer, DSL Tools was merged with the Visual Studio SDK (they used to be two distinct parts), but do not mix them up because DSL Tools is actually one layer above as it uses the Visual Studio SDK to allow you to create your plug-in. We could schematize the relationships between the Visual Studio SDK, DSL Tools, and Visual Studio 2005 as three domains containing each other like this:

Thanks to the DSL Tools, you will be able to produce a package the user can install on his machine and which will add new functionalities to his Visual Studio, the new functionalities being your language's related features (its toolbox, its new graphical designer, its new file extension, and so on).

Using the code

Before talking about the sources you need to have Visual Studio 2005 Professional installed and then to install the Visual Studio SDK 1.0. The project attached to this article was created with the Visual Studio SDK 1.0 September 2006 version.

The example solution is composed of two projects: one named Dsl, the other one named DslPackage. The Dsl project contains all that is relative to your domain specific language, its diagram, domain classes, relations, our custom attribute, and a tool too. The DslPackage, as it name points, contains all the package relative data, everything to make your domain specific languages incorporated and useable by Visual Studio. Those two projects get created automatically by Visual Studio when you create a Domain Specific Language solution.

In the Dsl project, you will find a DslDefinition.dsl file. This is the file in which you conceive your domain specific language. It's where you set your domain classes, their properties, and relationships, as well as their graphic shapes, and finally map the domain classes to their corresponding shapes and decorators. In this project, you will also find a PropertyTypeNameEditor.cs. This class corresponds to the custom attribute. By pressing F5, you will build the project, and Visual Studio will launch a debugging project which is another Visual Studio called the experimental hive which has separate registries. To use the custom attribute, add an element, and in its properties, click on the type property. It will trigger the custom attribute, showing a listbox with several pre-defined types. If you add another element on the diagram and go in its type property, you'll notice that the other element can be of the first element name, as shown on the picture.

The custom generator is called "SmplGenerator". To use it, press F5 again, close the debugging project, create a bogus new one, and add a new file to this project. Then, select a file type called "Smpl". A new file called Smpl1.smpl will be created, and if you expand it, there are two files under it: Smpl1.smpl.diagram and Smpl1.txt. Smpl1.txt was generated by our SmplGenerator, and will be again whenever you save it, or if you right click on Smpl1.smpl and then click on "Run Custom Tool".

Points of interest

We will not go through the whole process of creating your own language (Microsoft provides some good tutorials, see the Family Tree walkthrough), but I will highlight two points of interest: the first one being the creation of a custom attribute to a property; and the second one being the creation of your own custom run tool. A custom run tool is any code that is bound to a file, and triggered on specific events. For example, when you save your form in the form designer, it will automatically generate your corresponding C# or VB class. This is the behaviour that I will show you how to reproduce: our custom run tool will generate a file from the diagram designed by the user, and which will automatically appear in the tree view, under the diagram, in Visual Studio's Solution Explorer.

If you have never created a Dsl Tools project, I would recommend you to do the walkthroughs first, and then get back on this article as you need to know the basics to understand it. However, I'll explain quickly the structure of a Dsl Language first. A Dsl Language is designed in the DslDefinition.dsl file. You design your language by dragging and dropping elements on a drawing plan and setting relationships between them. For instance, every Dsl Language needs to have a root element that has an embedding relationship of all the other elements. On the user end, this will correspond to the drawing surface which will contain all the elements the user will add. Elements can have properties, which the user can set using Visual Studio's property grid, or thanks to decorators.

In the sample project furnished with this article, the language's root element is the ExampleModel element, which can contain elements of type ExampleElement. ExampleElement contains properties, and has a decorator which displays the element name in a rectangle, the rectangle representing the element. Once you've designed your language by setting all possible elements and the relationships between them, you're good to go with this article as it shows how to add custom code in order to make a more user friendly interface.

Adding a custom attribute

Let's look at what we have. In the DslDefinition.dsl file, you'll see all the domain classes of my SMPL language. Those classes are, in this case, named ExampleModel for the root, and ExampleElement for our elements. The DSL Tools automatically generated us the corresponding C# classes from the DslDefinition file. In the ExampleElement domain class, there are two properties: Name and Type. Our custom attribute will be placed on our "Type" property, and will list all element names contained by the model as well as common types in a listbox.

Now that we know where we are and what we want to do, let's start coding. First of all, you have to create your own class which derives from System.Drawing.Desgin.UITypeEditor. UITypeEditor is a base class which can be used to design value editors that can provide a user interface, which is exactly what we want to do. As you can see in PropertyTypeNameEditor.cs, you simply have to override the GetEditStyle and EditValue methods.

Here is what we want to do: when the user clicks on the Type property of an element in the property grid, we want to propose to him a list of common types (bool, string, char, int, etc.), types of elements that he already created in his model, or let him enter his own type. To do this, we will create a dropdown list containing all our type values, and then retrieve the elements names that the user defined too, then add them to the dropdown list as well.

The EditValue method takes as a parameter a System.ComponentModel.ITypeDescriptorContext. From this interface, we will be able to retrieve an ElementPropertyDescriptor object from which we'll get the element we are on, thanks to its property ModelElement. Then, we will get our root object which is, in our case, of type ExampleModel, and we will be able to iterate on each ExampleElement contained in our ExampleModel.

Once we can iterate on them, we just have to add each one of them to our list. Here is a part of the code of the EditValue method:

    ListBox listBox = new ListBox();
    listBox.Sorted = true;
    listBox.Click += new EventHandler(List_Click);

    listBox.Items.Add("Blob");
    listBox.Items.Add("Boolean");
    listBox.Items.Add("Byte");
    listBox.Items.Add("Char");
    listBox.Items.Add("Currency");
    listBox.Items.Add("DateTime");
    listBox.Items.Add("Decimal");
    listBox.Items.Add("Document");
    listBox.Items.Add("Double");
    listBox.Items.Add("Email");
    listBox.Items.Add("Guid");
    listBox.Items.Add("HyperLink");
    listBox.Items.Add("Integer");
    listBox.Items.Add("Object");
    listBox.Items.Add("Password");
    listBox.Items.Add("Picture");
    listBox.Items.Add("RichString");
    listBox.Items.Add("Single");
    listBox.Items.Add("String");
    listBox.Items.Add("TimeSpan");
    
    ElementPropertyDescriptor desc = 
       context.PropertyDescriptor as ElementPropertyDescriptor;
    ExampleElement currentElement = desc.ModelElement as ExampleElement;
    ExampleModel currentModel = currentElement.ExampleModel;
    IList elements = currentModel.Elements;
    foreach (ExampleElement element in elements)
    {
      listBox.Items.Add(element.Name);
    }

    listBox.SelectedItem = value;
}

Once your class is ready, open the DslDefinition file, click on the concerned property and its custom attribute field. A dialog will pop up, and we are going to add our attribute, which is a System.ComponentModel.EditorAttribute, taking as a parameter our class and a UITypeDescriptor. This should look like this:

Adding a custom run tool

A custom run tool is a file generator, similar to the form designer in Visual Studio. To make your own file generator, you will first have to register it in the registry, so that Visual Studio knows that a certain type of file must be handled by your generator; then, you will have to implement the IVsSingleFileGenerator interface, and finally you can configure Visual Studio to notify your generator on certain events (such as when the user saves), so that your generator would be launched automatically.

There are different ways to register your custom run tool (or generator). The most simple ones being to either make your own .reg file or to do it programmatically. In our case, we'd rather do it programmatically, because when we installed the Visual Studio SDK 1.0, it copied all the Visual Studio registries to create an "Experimental Visual Studio Hive", thanks to which you are able to test your new language. What we will do, is a small class which will register our generator in the right registries depending on the context.

In the DslPackage project, you will notice a Generated Code directory. In this directory, you will find text template files (*.tt), and if you expand the node in the Solution Explorer, you will see C# files under them. One of those files is named Package.tt. In this text template, you'll notice registering information, made through attributes. We will copy this principle, and make our own attribute that we are going to add to this text template.

Quick note: you can notice DSL Tools has its own custom run tool, which generates from a Text Template (*.tt) a C# file (*.cs), and this is the exact same behaviour we want to copy: we will generate a text file (*.txt) from our domain specific language file (*.smpl).

Here is part of the code contained in SmplGeneratorRegistrationAttribute:

public class SmplGeneratorRegistrationAttribute : RegistrationAttribute
{
    private string _packageGuid;
    private string _generatorClsid;
    private string _editorFactoryGuid;
    private Type _generatorType;

    private const string CSharpGeneratorsGuid = 
      "{fae04ec1-301f-11d3-bf4b-00c04f79efbc}";
    private const string CSharpProjectGuid = 
      "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";

    public ExampleGeneratorRegistrationAttribute(string packageGuid, 
           string generatorClsid, 
           string editorFactoryGuid, Type generatorType)
    {
        if (packageGuid == null)
            throw new ArgumentNullException("packageGuid");
        if (generatorClsid == null)
            throw new ArgumentNullException("generatorClsid");
        if (editorFactoryGuid == null)
            throw new ArgumentNullException("editorFactoryGuid");
        if (generatorType == null)
            throw new ArgumentNullException("generatorType");

        _packageGuid = packageGuid;
        _generatorClsid = generatorClsid;
        _generatorType = generatorType;
        _editorFactoryGuid = editorFactoryGuid;
    }

    public override void 
       Register(RegistrationAttribute.RegistrationContext context)
    {
        try
        {
            context.Log.Write("Registering Example generator ... ");

            // First we are registering our class

            Key key = context.CreateKey(@"CLSID");
            Key subKey = key.CreateSubkey(_generatorClsid);
            subKey.SetValue("ThreadingModel", "Both");
            subKey.SetValue("InprocServer32", 
              Path.Combine(Environment.SystemDirectory, "mscoree.dll"));
            subKey.SetValue("Class", _generatorType.FullName);
            subKey.SetValue("Assembly", _generatorType.Assembly.FullName);
            subKey.Close();
            key.Close();

            // Then we are registering our custom generator

            key = context.CreateKey(@"Generators\" + CSharpGeneratorsGuid);
            subKey = key.CreateSubkey("SmplGenerator");
            subKey.SetValue(string.Empty, "Example Generator");
            subKey.SetValue("CLSID", _generatorClsid);
            subKey.SetValue("GeneratesDesignTimeSource", 1);
            subKey.Close();
            key.Close();

            // register .sl editor notification


            key = context.CreateKey(@"Projects\" + 
                  CSharpProjectGuid + @"\FileExtensions");
            subKey = key.CreateSubkey(".smpl");
            subKey.SetValue("EditorFactoryNotify", _editorFactoryGuid);
            subKey.Close();
            key.Close();
            context.Log.WriteLine("Success.");
        }
        catch (Exception e)
        {
            context.Log.WriteLine("Failure: " + e);
        }
    }

    public override void Unregister(
           RegistrationAttribute.RegistrationContext context)
    {
        try
        {
            context.Log.Write("Unregistering Example generator... ");
            context.RemoveKey(@"CLSID\" + _generatorClsid);
            context.RemoveKey(@"Generators\" + 
               CSharpGeneratorsGuid + @"\SmplGenerator");
            context.RemoveKey(@"Projects\" + 
               CSharpProjectGuid + @"\FileExtensions\.smpl");
            context.Log.WriteLine("Success.");
        }
        catch (Exception e)
        {
            context.Log.WriteLine("Failure: " + e);
        }
    }
}

Now that our attribute was created, we are going to add it to the Package.tt file:

[SmplGeneratorRegistrationAttribute(Constants.<#= dslName #>PackageId, 
   "{D8760704-A993-40ee-89B9-FB77764D99AF}", "{" + 
   Constants.<#= dslName #>EditorFactoryId + "}", 
   typeof(<#= this.Dsl.Namespace #>.ExampleGenerator))]

Be very careful not to forget the braces surrounding the GUIDs, or else it won't work. You can also notice that we are using a Constants class. The Constants class is one of the classes automatically generated by the DslTools, thanks to the text template files of the Dsl Tools projects. I also invite you to check the text templating syntax documentation if you want to know more about it.

From now on, our generator will be registered and attached to this file format; however, we still have to set Visual Studio to notify our generator in case of an event that we are interested in. This is done in the editor factory. You will notice a file in the DslPackage project called [NameOfYourProject]EditorFactory.tt. For my part, I find the text templating syntax not quite convenient, so instead of editing the text templates, I preferred to use another way. If you look at the [NameOfYourProject]EditorFactory class, you will notice that it is a partial class; so all we have to do is add our own [NameOfYourProject]EditorFactory partial class in which we implement the IVsEditorFactoryNotify interface, so that our generator is notified of a save or some other event you may be interested in. This is also here so that you will set your new language's file custom run tool property to your generator.

private int SafeNotifyItemAdded(uint grfEFN, 
    IVsHierarchy pHier, uint itemid, string pszMkDocument)
{
    object itemObject;
    int hr = pHier.GetProperty(itemid, 
             (int)__VSHPROPID.VSHPROPID_ExtObject, out itemObject);
    if (hr < 0)
    {
        return hr;
    }

    ProjectItem item = itemObject as ProjectItem;
    if (item == null)
    {
        return -1;
    }

    // Place here the name of the custom tool you want to run

    item.Properties.Item("CustomTool").Value = "SmplGenerator";

    return 0;
}

Now that everything is set (registration + event notification), we can add our generator. To do so, we will have to implement the IVsSingleFileGenerator in case of a single file generator like the one generating C# code from text templates. I would also recommend to implement the IVsGeneratorProgress interface too, so that you will be able to throw exceptions which will appear in the Visual Studio Error List window. So this would look like this:

public void Generate(
    string inputFilePath,
    string inputFileContents,
    string defaultNamespace,
    out System.IntPtr outputFileContents,
    out int outputLength,
    IVsGeneratorProgress generateProgress)
{
    ProjectItem item = SiteServiceProvider.GetService(
                typeof(EnvDTE.ProjectItem)) as ProjectItem;

    outputLength = 0;
    byte[] bytes = null;
    try
    {
        bytes = GenerateCode(inputFilePath, 
          inputFileContents, item, out outputLength);
    }
    catch (SmplConverterException cce)
    {
        generateProgress.GeneratorError(false, 0, cce.Message, 0, 0);
    }
    catch (Exception e)
    {
        generateProgress.GeneratorError(false, 0, e.ToString(), 0, 0);
    }
    if (bytes == null)
    {
        outputFileContents = IntPtr.Zero;
        outputLength = 0;
    }
    else
    {
        outputFileContents = Marshal.AllocCoTaskMem(outputLength);
        Marshal.Copy(bytes, 0, outputFileContents, outputLength);
    }
}

And now everything is set to work. Check the "Using the code" section to know how to test your custom run tool (it is not as straightforward as usual, and is a great productivity loss).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here