Dans la précédente partie de cet article, nous avons vu comment générer plusieurs fichier à partir du même DSL. Nous allons maintenant voir comment faire de la génération de code incrémentale. C'est à dire que l'on va modifier du code existant écrit par l'utilisateur en injectant du code provenant de notre générateur. Pour y parvenir, nous allons utiliser une API COM exposée par Visual Studio baptisée CodeModel.

CodeModel permet de parcourir en lecture-écriture notre code source sous forme d'arbre d'objets, pour peu qu'il soit écrit dans un langage supportant CodeModel (C#, VB.Net et C++ sont actuellement supportés par Microsoft). Cette API est assez rudimentaire et peu pratique à utiliser "out of the box". Pour comprendre l'API et son fonctionnement, je vous conseille la lecture de cet article de la MSDN : http://msdn2.microsoft.com/en-us/library/92aexfx5(VS.80).aspx.

Ce qu'il faut en retenir, c'est que ca ressemble énormément à du DOM XML, mais dont la grammaire est différentes. L'interface "de base" (oui, on est en COM, on ne manipule pas les objets directement en utilisant leur classe, mais via les interfaces qu'ils implémentent... le "de base" voulant dire que c'est l'interface commune à tous les objets d'un arbre CodeModel) CodeElement a des propriétés Parent, Children et Kind, tout comme XmlNode. On peut ensuite le caster sous forme de CodeClass, CodeNamespace etc., tout come XmlNode peut-être casté en XmlElement, XmlAttribute, etc. Le soucis étant qu'il n'éxiste pas d'API de requétage correct pour CodeModel.

Afin de palier à ce manque et rendre l'API plus "developer friendly", mon idée a été de développer des méthodes d'extensions pour les classes CodeClass2, CodeFunction2, CodeProperty etc.

Voici un exemple simple:

public static IEnumerable<CodeFunction2> FindMethodsByName(this CodeClass2 codeClass, string name)
        {
            return codeClass.Children.OfType<CodeFunction2>().Where(cf => cf.Name == name);
        }

Cette méthode d'extension me permet de faire des choses du genre :

if (codeClass.FindMethodsByName("Initialize").FirstOrDefault() == null)
                CreateInitializeMethod();

Ce qui est vous l'avouerez est quand même plutôt simple comparé à ce que l'on aurait du faire en C# 2 :).

Pour illustrer l'utilisation de cette API, de mes méthodes d'extension ainsi que de ce que l'on a vu dans la précédente partie, nous allons écrire un générateur de Behavior ASP.Net Ajax qui produit les behaviors côté client en Script#, et des Control Extender côté serveur pour faciliter leur utilisation avec ASP.Net.

 

  • Création du modèle

Le modèle est très simple, car il ne fait intervenir que 2 notions : L'Extender et l'ExtenderProperty

image

Je ne m'étendrai pas trop là dessus, car c'est vraiment simple. La seule chose à dire c'est que j'ai créé des TypeEditor pour le BehaviorFile et ExtenderFile, ainsi qu'une enum PropertyType (qui n'a que 3 valeurs possibles pour l'instant : String, ExternalControl, ExternalBehavior, qui permettront donc de référencer d'autres contrôles / extenders facilement).

  • Création des "CodeGenerators"

Comme nous l'avons vu dans l'article précédant, nous n'allons pas créer de CustomTool, mais nous allons directement mettre le code de génération dans le projet de Package Visual Studio. Dans notre cas, nous avons 2 classes à générer dans 2 projets différents (un projet Script# côté client, un projet C# côté serveur). Les 2 classes étant écrites en C#, avec les même contraintes de capacité de "customisation", nous allons toutes les 2 les créer / modifier avec CodeModel. Pour faciliter tout celà, j'ai créer une classe de base dont les 2 générateurs hériteront :

public abstract class CodeModelBasedGenerator
    {
        private DTE2 _dte;
        public DTE2 DTE
        {
            get { return _dte; }
        }
        private string _fileName;

        protected string FileName
        {
            get { return _fileName; }
        }

        private FileCodeModel2 _fileCodeModel;

        protected FileCodeModel2 FileCodeModel
        {
            get { return _fileCodeModel; }
        }
        private CodeModel _codeModel;

        protected CodeModel CodeModel
        {
            get { return _codeModel; }
        }
        private ProjectItem _projectItem;

        protected ProjectItem ProjectItem
        {
            get { return _projectItem; }
        }
        protected CodeModelBasedGenerator(string fileName, IServiceProvider serviceProvider)
        {
            _fileName = fileName;
            _dte = (DTE2)serviceProvider.GetService(typeof(DTE));
            if (_dte == null)
                throw new InvalidOperationException("The service provider cannot provide Visual Studio DTE Service");
            _projectItem = _dte.Solution.FindProjectItem(_fileName);
            if (_projectItem == null)
                throw new InvalidOperationException("The specified file is not part of the current solution projects");
            _fileCodeModel = (FileCodeModel2)_projectItem.FileCodeModel;
            if (_fileCodeModel == null)
                throw new InvalidOperationException("The specified file does not support Code Model");
            _codeModel = _projectItem.ContainingProject.CodeModel;
           
        }

        public abstract void Generate();
    }

En héritant de cette classe, mes générateurs auront ainsi accès au FileCodeModel du fichier qu'ils doivent manipuler, ainsi qu'à d'autres points d'extensibilité.

Voici un extrait de code du générateur de behavior Script#, illustrant l'utilisation de la classe de base, et de mon API de requétage CodeModel :

public class BehaviorCodeGenerator : CodeModelBasedGenerator
    {
        private ExtenderElement _element;
        public BehaviorCodeGenerator(string fileName,ExtenderElement element, IServiceProvider provider)
            : base(fileName, provider)
        { _element = element; }

        public override void Generate()
        {
            FileCodeModel.AddImportsIfNotExist("System", "System.DHTML", "Sys", "Sys.UI");
            var behaviorNS = FileCodeModel.FindOrCreateNamespace(_element.BehaviorNamespace);
            var behaviorClass = behaviorNS.FindOrCreateClass(_element.Name + "Behavior");
            if (behaviorClass.Bases.OfType<CodeElement>().Where(ctr => ctr.FullName == "Sys.UI.Behavior")
.FirstOrDefault() == null) behaviorClass.AddBase("Behavior", null); // Constructor var constructor = behaviorClass.FindConstructors().Where(fc => fc.MatchParameterTypes("System.DHTML.DOMElement"))
.FirstOrDefault(); if (constructor == null) { constructor = (CodeFunction2) behaviorClass.AddFunction(_element.Name + "Behavior",
vsCMFunction.vsCMFunctionConstructor,
null, -1,
vsCMAccess.vsCMAccessPublic, null); var param = constructor.AddParameter("element", "DOMElement", -1); var editPoint = param.EndPoint.CreateEditPoint(); editPoint.EndOfLine(); editPoint.InsertFormated(" : base(element)"); } // Properties foreach (var prop in _element.ExtenderPropertyElements) { CodeVariable2 field; CodeProperty codeProp;
                {...}
            }
        }
    }

(en violet, ce sont les méthodes d'extensions que j'ai rajouté)

Comme on peut le voir, le code existant est manipulé, mais pas écraser. On vérifie la présence du constructeur des propriétés, des "using" etc. avant de les ajouter au code existant.

  • Branchement des générateurs

Comme dans mon article précédant, je déclenche la génération au moment ou le document est sauvé. Ce code a donc lieu dans la classe DocData (que l'on étend par partialité, pour ne pas se faire écraser par le générateur de code du designer de DSL) :

partial class ExtenderFactoryDocData
   {
       protected override void OnDocumentSaved(EventArgs e)
       {
           base.OnDocumentSaved(e);
           ExtenderBagModel root = RootElement as ExtenderBagModel;
           foreach (var elem in root.ExtenderElements)
           {
               var behaviorGen = new CodeGenerators.BehaviorCodeGenerator(elem.BehaviorFile, elem, ServiceProvider);
               behaviorGen.Generate();
               var extenderGen = new CodeGenerators.ExtenderCodeGenerator(elem.ExtenderFile, elem, ServiceProvider);
               extenderGen.Generate();
           }
       }
   }

Et voilà, rien de très compliqué en somme... juste une librairies d'extensions (que vous pouvez télécharger avec les sources), qui facilite grandement la vie.

 

Pour regarder ce que ca donne en vrai, manipuler le code et l'améliorer (par exemple, utiliser des chemins relatifs plutôt qu'absolus pour référencer les fichiers, gérer d'autres types de données...), voici les sources, contenant un petit projet de test tout simple (qui nécessite par ailleurs l'installation de Script# pour fonctionner) :

Les sources

Mots clés Technorati : ,,,

Commentaires :

# re: [DSL Tools] Génération incrémentale, génération multi-artefacts : partie 2
Ecrit par CyrilJ le 5/18/2008 2:45 PM
Bonjour Simon,

Article très intéressant. CodeModel est en effet assez utile, même si un peu trop couplée à mon goût aux "vieilles" APIs DTE/VSLang de VS. En outre, il faut savoir que CodeModel a ses limites et ne s'abstrait pas suffisamment du langage cible : ainsi, certaines constructions du langage (C#, VB.NET, C++.NET, etc) ne sont pas accessibles. Mais bon, c'est mieux que rien.

Maintenant, pour une autre approche concernant la génération de code incrémentale (autant que faire se peut) à partir d'une instance de modèle d'un DSL et, surtout, multi-artefacts, j'ai pensé que vous seriez intéressé par la technique que j'ai implémentée dans le proto de mon outil. Une technique qui s'appuie principalement sur le pattern "IoC" (Inversion Of Control) entre l'information portée par le modèle, d'une part, et l'interface exposée au designer VS du DSL par le "processeur" (typiquement, un générateur de code) qui va visiter le modèle, lui donner une interprétation, et générer les artefacts, d'autre part.

Voici la chose :

http://www.cjandia.com/experiments/using-Microsoft-DSL-Tools/AppBuilder-SF/wink/

La même, en 1024 x 800 :

http://www.cjandia.com/experiments/using-Microsoft-DSL-Tools/AppBuilder-SF/1024x800.html

(durée : +40 mins. démo flash, met un peu de temps à se charger dans le browser)

Bien à vous,

Cyril
# re: [DSL Tools] Génération incrémentale, génération multi-artefacts : partie 2
Ecrit par CyrilJ le 5/18/2008 2:49 PM
Oops!

Correction : la seconde URL est bien sûr :

http://www.cjandia.com/experiments/using-Microsoft-DSL-Tools/AppBuilder-SF/wink/1024x800.html

Well, fixed. ;)

'HTH,

Cyril

Ecrire un commentaire :

Titre :*
Nom *
Email
Url
Commentaire : *  


Please add 4 and 7 and type the answer here: