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 straight forward, 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 2 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 were merged with the Visual Studio SDK (they used to be two distincts parts), but do not mix them up because DSL Tools are actually one layer above as they are using 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 functionnalities being your language's related features (its toolbox, its new graphical designer, its new file extension, and so on).
Using the code
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 tool too. The DslPackage, as it names 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 seperate 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 was 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".
Variable or class names should be wrapped in < code > tags like this.
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 of one of your domain classes; and the second one being the creation of your own generator, generating 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.
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 those 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 my 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, we want to propose to him a list of common types (Bool, string, char, int, etc.), types of element 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 to 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 it's Custom Attribute field. A dialog will pop up, and we are going to add our attribute which is a System.ComponentModel.EditorAttribute taking as 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, 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 have their own custom generator, 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. 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 will rather 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 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 straight forward as usual and is a great productivity loss).
Download sample source code (861 Ko)
Carl Anderson |