Dans la plupart des présentations sur les DSL Tools, on nous montre des scénarios très simples de génération de code "one-shot", et qui ne tiennent pas compte du code éxistant.
Récemment, j'ai eu à travailler sur un cas plus complexe : Notre client souhaitait faire du développement d'interface utilisateur Ajax piloté par les specs. C'est à dire, que la personne en charge des spécifications de l'UI doit pouvoir définir le contenu de l'écran (les champs de formulaires), des règles d'assistance à la saisie (remplir une combobox en fonction de tels ou tels autres champs, de données provenant d'un SGBD etc.) et des déclencheurs pour exécuter ces règles (événement sur un champs, timer, ...), et qu'à partir de cela, un squelette de code et des artefacts de documentation soient générés. Pour chaque champs et chaque règle, le "spécificateur" donnera une description détaillée de ce que le "dévelopeur" devra faire. L'outil utilisé par le "spécificateur" (et plus tard par le "dévelopeur" aussi, rassurez vous ;-)) est un DSL qui va donc générer un certain nombre d'artefacts:
- Un extrait de doc au format xhtml utilisé pour les documents de spécification détaillant le contenu des écrans, les actions déclenchées sur tel ou tel évènement etc.
- Un UserControl ASP.Net contenant les champs définis dans le DSL
- Un Behavior ASP.Net Ajax permettant de contrôler l'état de l'écran côté client et générer les "triggers" sensés déclencher les "règles", et contenir des points d'entrée customisables par le dévelopeur (notamment pour donner la possibilité d'implémenter certaines règles côté client)
- Un Service WCF exposé à ASP.Net Ajax, contenant des méthodes correspondant aux règles à exécuter côté serveur (celles qui nécessitent un accès à la base de donnée par exemple). La description de chaque règle sera recopiée dans la Documentation Xml de ces méthodes afin d'assister le dévelopeur
- Bien sûr, le développeur n'aura à sa charge que l'implémentation des règles et éventuellement la retouche du UserControl pour modifier le layout (la communication entre le behavior et les règles côté client et serveur étant entièrement générée)
Une première problématique se pose directement : notre DSL génère plusieurs artefacts, donc un CustomTool implémentant IVsSingleFileGenerator ne fonctionnera pas. Pour cela il existe un moyen simple, il suffit dans la classe DocData générée par le designer DSL (dans le projet DslPackage) et de surcharger sa méthode OnDocumentSaved (créez une partial class dans un fichier à part pour ne pas vous faire écraser vos modifications) :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Company.DemoIncrementalDSL
{
partial class DemoIncrementalDSLDocData
{
protected override void OnDocumentSaved(EventArgs e)
{
base.OnDocumentSaved(e);
// Do your own logic
var rootElement = this.RootElement as ModelWithMultipleFiles;
string docFilePath = rootElement.DocumentationFilePath;
string classFilePath = rootElement.ClassFilePath;
}
}
}
Dans cet exemple, ModelWithMultipleFiles est le nom de l'élément racine de mon model, et il contient 2 DomainProperties "DocumentationFilePath" et "ClassFilePath". Nous sommes donc prêt ici à générer nos fichiers à partir du model (car ici nous savons que le model vient d'être persisté, nous avons l'instance en cours dans la variable rootElement, et nous avons donc les chemins des fichier à générer).
Pour parfaire cet exemple, il serait bon de fournir un éditeur personnalisé pour les chemins de fichier. En effet, l'expérience utilisateur de notre DSL reste pour le moment à désirer, car nos propriétés de types string, ne sont éditables que via une TextBox.
Pour y remédier, nous devons implémenter un UITypeEditor et placer un attribut sur notre propriété spécifiant l'utilisation de notre editeur. Voici un exemple de code pour implémenter UITypeEditor à base de SaveFileDialog :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing.Design;
using System.Windows.Forms;
namespace Company.DemoIncrementalDSL
{
// UITypeEditor est situé dans le namespace System.Drawing.Design
class FilePathEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(System.ComponentModel.ITypeDescriptorContext context, IServiceProvider provider, object value)
{
using (var sfd = new SaveFileDialog())
{
sfd.FileName = value.ToString();
if (sfd.ShowDialog() == DialogResult.OK)
{
return sfd.FileName;
}
else
return value;
}
}
}
}
Dans l'éditeur de DSL, on posera alors un custom attribute sur les propriété qui devront utiliser cet éditeur :

Un deuxième problème se pose à nous : Générer du code de manière incrémentale. En effet, je n'ai jamais vu de projet où les spécifications étaient fixées une fois pour toute. Il y a toujours des allers-retours à faire, adapter les specs au feedback utilisateur etc. Il n'est donc pas possible de regénérer notre code sans tenir compte des personnalisations opérées par le dévelopeur depuis la précédente génération (remplissage des méthodes de règle). Pour des fichiers basés sur du X(ht)ml, il n'y a pas de soucis, les APIs DOM, ou Linq to Xml sont là pour ca, mais en ce qui concerne le code C# (ou VB), cela semble assez compliqué.
Nous verrons dans la prochaine partie de cet article comment utiliser une API fournie par Visual Studio pour explorer / écrire / modifier du code programatiquement (et qui nous aidera donc à régler notre problème) : CodeModel.
UPDATE : La 2e partie de l'article est publiée