Introduction
Les Domain-Specific Language Tools permettent la construction d'éditeurs graphiques personnalisés et la génération de code source à partir d'une modélisation des concepts métiers. En d'autres mots, les DSL Tools vous permettent de créer un éditeur graphique de "type UML" dans Visual Studio 2005 Professionnel, qui lui-même permettra de générer des fichiers de types .nomDeVotreLangage à partir des concepts définis via l'éditeur graphique.
L'utilisation des DSL Tools est simple de prise en main, et la création de votre propre langage inspiré de l'UML relativement rapide, mais lorsque l'on veut ajouter des fonctionnalités un peu plus complexes à notre éditeur (comme l'ajout d'un attribut personnalisé à une propriété de notre concept ou faire son propre outil personnalisé générant les sources plutôt que d'utiliser les "text templates"), cela peut sembler encore obscur du fait d'une documentation encore peu diserte.
C'est pourquoi cet article se propose d'illustrer la création de votre propre langage métier avec un outil plus convivial que ceux fourni de base, ainsi que votre propre générateur de code. Dans le code source attaché à l'article, vous trouverez un projet d'exemple créé à partir du modèle de langage minimal, et auquel j'ai ajouté les deux fonctionnalités que je vais expliquer dans cet article. Enfin, le projet a été créé avec la version de septembre 2006 du Visual Studio SDK 1.0.
Pré-requis
Avant de commencer, présentons succinctement les DSL Tools; pour en savoir plus je vous invite à consulter ce site : http://www.microsoft.com/france/msdn/architects/industrialisation/dsltools.mspx. Les DSL Tools ont été incorporés dans le Visual Studio SDK, mais il faut bien faire la distinction entre les deux car les DSL Tools sont en réalité une couche au-dessus qui utilise le Visual Studio SDK pour vous permettre de créer votre plug-in. Nous pourrions schématiser la relation entre le Visual Studio SDK, les DSL Tools et Visual Studio 2005 comme 3 domaines distinct se contenant les uns dans les autres comme ceci :
Grâce aux DSL Tools, vous êtes capable de produire un complément à Visual Studio que l'utilisateur final pourra installer, et qui ajoutera de nouvelles fonctionnalités à son Visual Studio 2005. Ces nouvelles fonctionnalités seront alors celles apportées par votre langage (sa boite à outil, son éditeur graphique, son extension de fichier, etc.).
Utiliser le code
Avant d'utiliser le projet d'exemple, vous devez avoir Visual Studio 2005 Professionnel d'installé ainsi que le Visual Studio SDK 1.0. Note : Le projet joint avec cet article a été créé la version de septembre 2006 du Visual Studio SDK 1.0.
La solution d'exemple est composée de deux projets: un nommé Dsl, et l'autre DslPackage. Le projet Dsl contient tout ce qui est relatif au langage créé, son architecture, ses classes métiers, ses relations, son attribut et son outil personnalisés. Le projet DslPackage, comme son nom l'indique, contient toutes les informations relatif au "package" pour permettre à votre langage d'être incorporé et utilisable par Visual Studio. Ces deux projets sont créés automatiquement par Visual Studio quand vous créez une solution "Domain Specific Language".
Vous trouverez le fichier DslDefinition.dsl dans le projet Dsl. Ce fichier est celui dans lequel vous concevez votre langage. C'est dans celui-ci que vous définissez les concepts qui composent le langage, les propriétés des concepts, leur relations, ainsi que leur aspect visuel, et que vous effectuez la correspondance entre les concepts et le visuel. Vous trouverez aussi dans ce projet un fichier nommé PropertyTypeNameEditor.cs. Ce fichier contient la classe de notre attribut personnalisé. En appuyant sur F5, vous construirez le projet, et Visual Studio lance alors un projet de débogage dans un autre Visual Studio appelé "experimental hive". C'est un Visual Studio qui utilise des registres séparés du vôtre de manière à ce que vous puissiez tester votre extension de Visual Studio sans altérer votre environnement de développement. Pour utiliser l'attribut personnalisé, veuillez ajouter à partir de la boîte à outil un nouvel élément sur l'espace graphique, allez dans ses propriétés et éditez la propriété Type. Cela va déclencher notre attribut personnalisé, montrant ainsi une listbox pré-remplie des types de bases. Si vous ajoutez un autre élément sur l'espace d'édition et que vous allez sur la propriété Type, vous remarquerez que celui-ci peut prendre comme type le type d'un autre élément, comme illustré sur cette image :
Le générateur personnalisé est nommé "SmplGenerator". Pour l'utiliser, appuyez sur F5, fermez le projet de débogage, créez un nouveau projet quelconque, puis faites "Add a new item...". Ensuite sélectionnez le type de fichier nommé "Smpl". Un nouveau fichier nommé Smpl1.smpl va être créé, et si vous déployez son arborescente sous-jacente, vous noterez deux fichiers : Smpl1.smpl.diagram et Smpl1.txt. Smpl1.txt a été généré par notre SmplGenerator, et le sera à chaque fois que vous sauvez le fichier Smpl.spml, ou si vous faites un clic droit sur le fichier puis sur Run Custom Tool.
Points particuliers d'intérêt
A défaut d'aborder dans cet article le processus complet de la création de votre propre langage (Microsoft met à disposition plusieurs tutoriels, tel que le Family Tree), cet article détail deux points qui méritent attention : la création d'un attribut personnalisé pour une propriété ; ainsi que la création de son propre outil personnalisé d'exécution. Un outil personnalisé d'exécution est n'importe quel code relié à un fichier, et déclenché sur des actions précises. Un bon exemple est l'outil personnalisé d'exécution qui est déclenché à la sauvegarde de votre WinForm dans l'éditeur de Visual Studio. C'est exactement de ce comportement dont nous allons nous inspirer dans cet article : notre outil personnalisé d'exécution va générer un fichier à partir des éléments que l'utilisateur aura déposé sur le plan de travail, et les fichiers générés vont automatiquement apparaître dans l'explorateur de solution, sous le dessin source, dans Visual Studio. Si vous n'avez jamais créé de projet avec les DSL Tools, je vous recommanderai de passer cet article, pour tout d'abord lire les tutoriels de bases disponibles. Cependant je vais tout de même faire un bref rappel de la structure d'un langage "DSL". Un langage "DSL" est conçu dans le fichier DslDefinition.dsl. La conception de votre langage se fait en glissant/déposant vos éléments sur la plan de travail puis en définissant leurs relations intrinsèques. Pour être plus concret, n'importe quel langage DSL doit avoir un élément racine qui va avoir une relation de conteneur avec les autres éléments. Du point de vue de l'utilisateur cela va correspondre au plan de travail qui va contenir tous les éléments que l'utilisateur ajoutera. Chaque élément peut avoir des propriétés que l'utilisateur pourra spécifier via la grille de propriétés et/ou des décorateurs. Dans le projet fournit en exemple, la racine du langage est l'élément ExampleModel, qui lui-même contient des éléments de type ExampleElement. ExampleElement contient des propriétés (Name et Type), et a un décorateur qui affiche la propriété Name dans un rectangle représentant l'élément. Une fois le langage schématisé en spécifiant tous les éléments possibles et leurs relations, nous sommes fin prêt à passer aux étapes supérieures, tel l'ajout de fonctionnalités personnalisées, de manière à rendre l'interface utilisateur plus "user-friendly".
Ajout d'un attribut personnalisé
Avant de commencer, nous allons faire un point sur ce que nous avons. Dans le fichier DslDefinition.dsl, du projet fourni en exemple, vous y trouverez toutes les concepts de mon langage d'exemple SMPL. Dans cet exemple, ses classes sont nommées ExampleModel pour la racine, et ExampleElement pour les éléments qui composent le langage. A partir des données définies dans ce fichier, les DSL Tools nous génèrent les classes C# correspondantes. Dans la classe ExampleElement, il y a deux propriétés : Name et Type. Notre attribut personnalisé sera placé sur la propriété "Type", et permettra de lister tous les éléments contenus dans le modèle défini par l'utilisateur ainsi que tous les types usuels, le tout rassemblé dans une listbox.
Maintenant que les bases sont posées et que nous savons ce que nous voulons faire, nous pouvons commencer à coder. Tout d'abord nous allons coder notre propre classe qui dérive de System.Drawing.Desgin.UITypeEditor. UITypeEditor est une classe qui est utilisée pour concevoir des éditeurs de valeur, qui peuvent fournir une interface utilisateur, ce qui correspond parfaitement à ce que nous voulons faire. Comme vous pouvez le voir dans le fichier PropertyTypeNameEditor.cs du projet d'exemple, vous devez simplement surcharger les méthodes GetEditStyle et EditValue pour implémenter la classe.
Voici le résultat que nous voulons obtenir : dans la grille de propriétés d'un élément, quand un utilisateur clique sur la valeur de la propriété Type, nous allons lui proposer une liste composée des types communs (bool, string, char, int, etc.), puis les types des éléments qu'il a déjà définis dans son modèle qu'il est en train de concevoir. Pour cela, nous devons créer une "dropdown list" contenant tous les types de base, puis récupérer dynamiquement tous les noms des éléments déjà créés, pour enfin les ajouter à la liste.
La méthode EditValue prend en paramètre un System.ComponentModel.ITypeDescriptorContext. De cette interface, il est possible de récupérer un objet de type ElementPropertyDescriptor du quel nous pourrons récupérer l'élément où nous nous trouvons au moment du clic grâce à sa propriété ModelElement. Ensuite nous allons récupérer notre objet racine, qui dans le cas de notre exemple, est du type ExampleModel, et de là nous serons capable d'itérer sur chaque ExampleElement contenu dans notre ExampleModel 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;
}
Une fois la classe prête il faut maintenant la relier à la propriété de notre élément. Pour cela ouvrez le fichier DslDefinition, puis cliquez sur le champ "custom attribute" dans la grille de propriétés de la propriété concerné (qui est Type dans notre cas d'exemple). Une fenêtre de dialogue va s'ouvrir, et nous allons pouvoir ajouter un attribut personnalisé qui est de type System.ComponentModel.EditorAttribute, prenant en paramètre notre classe UITypeDescriptor. Cette étape devrait ressembler à ceci :
Ajout d'un outil d'exécution personnalisé
Un outil d'exécution personnalisé peut être un générateur de fichier, tel que l'éditeur de formulaire Windows dans Visual Studio. La première étape pour créer son propre générateur de fichier, est de l'enregistrer dans les registres de manière à renseigner Visual Studio, que les fichiers d'un certain type (".smpl" dans notre cas) doivent être gérés par notre générateur. Ensuite il faudra implémenter l'interface IVsSingleFileGenerator, et enfin configurer Visual Studio pour qu'il notifie notre générateur sur certains événements (comme lors de la sauvegarde de notre modèle .smpl par exemple), pour que Visual Studio lance notre générateur automatiquement.
Il y a plusieurs moyens d'enregistrer notre outil d'exécution dans les registres. Les méthodes les plus simples sont de se faire son propre fichier .reg ou de le faire programmatiquement. Dans notre cas, il est préférable de le faire programmatiquement, principalement parce que lors de l'installation du Visual Studio SDK, l'ensemble de la base de registres de Visual Studio a été copiée pour créer une "Experimental Visual Studio Hive", qui nous permet de tester en mode débogage nos projets DSL Tools. Nous allons donc nous faire notre propre classe qui va enregistrer notre générateur dans les bons registres en fonction du contexte d'exécution de Visual Studio (Experimental Hive ou non).
Dans le projet DslPackage, vous remarquerez un répertoire Generated Code. Dans ce répertoire, des fichiers appelés "text templates" (*.tt). Dans l'arborescence d'un "text template", vous trouvez un fichier C# pour chaque "text template". L'un d'eux est nommé Package.tt. Dans ce "text template", vous pouvez voir que par défaut le Package.cs généré à partir de ce "text template" contient des attributs s'occupant d'enregistrer des informations dans les registres. Nous allons copier ce motif (pattern), et créer notre propre attribut que nous allons ajouter à ce "text template".
NB : Comme vous avez pu le deviner, les DSL Tools utilisent eux-mêmes un outil d'exécution personnalisé pour générer des fichiers C# (*.cs) à partir de "text templates" (*.tt). C'est exactement ce comportement que nous voulons copier dans notre exemple : nous allons générer un fichier texte (*.txt) à partir de notre langage (*.smpl).
Voici une partie du code contenu dans l'attribut 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);
}
}
}
Une fois la classe créée, il nous faut maintenant rajouter notre attribut dans le Text Template Package.tt : [SmplGeneratorRegistrationAttribute(Constants.<#= dslName #>PackageId,
"{D8760704-A993-40ee-89B9-FB77764D99AF}", "{" +
Constants.<#= dslName #>EditorFactoryId + "}",
typeof(<#= this.Dsl.Namespace #>.ExampleGenerator))]
NB :Faites attention à ne pas oublier les accolades, sinon cela ne fonctionnera pas. Vous remarquerez que nous utilisons une classe nommée Constants. La classe Constants est une des classes générées automatiquement par les DSL Tools aux fichiers Text Templates. Je vous invite également à regarder la documentation concernant la syntaxe des Text Templates pour en savoir plus, car elle n'est pas abordée dans cet article.
A partir de maintenant, notre générateur va être enregistré et lié à notre format de fichier. Cependant, nous devons toujours configurer Visual Studio pour qu'il notifie notre générateur de l'évènement de sauvegarde d'un fichier .smpl. Ceci est fait dans l'editor factory. Vous noterez un fichier nommé [NameOfYourProject]EditorFactory.tt dans le projet DslPackage. Etant plus à l'aise en C# qu'en Text Templates, j'ai préféré chercher un autre moyen que d'avoir à faire ça en Text Templates. Si vous ouvrez la classe [NameOfYourProject]EditorFactory, vous remarquerez que c'est une classe partielle ! Par conséquent il nous suffit d'ajouter notre propre classe partielle [NameOfYourProject]EditorFactory qui implémentera l'interface IVsEditorFactoryNotify. De cette manière notre générateur sera donc notifié des évènements voulus. C'est également ici, que vous affecterez votre générateur comme étant l'outil d'exécution personnalisé de votre langage. 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;
}
Maintenant que toute la structure est mise en place (valeurs dans les registres + remontée des évènements), nous pouvons donc ajouter notre générateur proprement dit. Pour faire ceci, il vous faudra implémenter l'interface IVsSingleFileGenerator pour un générateur d'un fichier comme celui qui génère des fichiers C# à partir des Text Templates. Je vous recommande d'implémenter l'interface IVsGeneratorProgress également, de façon à afficher automatiquement les exceptions remontées par votre générateur dans la fenêtre de liste d'erreurs de Visual Studio. Cette étape prend donc cette forme : 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);
}
}
Tout est maintenant en place pour fonctionner. Veuillez vous reporter au paragraphe "Utiliser le code" de cet article pour savoir comment tester votre générateur.
Téléchargement du code exemple (861 Ko)
Carl Anderson |