Génération de code C# à la volée

Vous vous êtes déjà demandé comment générer du code C# à la volée simplement ? Si oui, alors cet article est fait pour vous !

Durant mes développements divers à mon travail, j'ai eu le besoin à un moment donné, de créer une application dans laquelle je voulais afficher la requête SQL généré par NHibernate. Pour ce faire, je devais laisser la possibilité à l'utilisateur d'écrire dans une boite de saisie sa requête NHibernate.

Par la suite l'application devait donc compiler et exécuter cette requête NHibernate pour que je puisse récupérer la requête SQL générée.

Mais prenons un cas plus simple avec le classique HelloWord. En code C# cela donne :

MessageBox.Show("Hello world !");

Le but de notre application est donc de pouvoir saisir ce code dans une TextBox puis de cliquer sur un bouton "exécuter" pour que le code soit compilé et exécuté.

Pour notre exemple je vais développer une application en WPF avec une implémentation très rapide du MVVM. Voilà le code XAML :

<Window x:Class="GenerationCodeTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Générateur de code" Height="614" Width="308" ResizeMode="CanMinimize">
    <Window.Background>
        <LinearGradientBrush StartPoint="0,0.5" EndPoint="0,1">
            <GradientStop Color="SteelBlue" Offset="-0.5"/>
            <GradientStop Color="White" Offset="1"/>
        </LinearGradientBrush>
    </Window.Background>
    <Grid>
        <Label Content="Exécuteur de code basique" Height="35" HorizontalAlignment="Center" Margin="24,8,12,0" Name="label1" VerticalAlignment="Top" Width="250"
               FontWeight="Bold" FontFamily="Calibri" FontSize="20" />

        <TextBox Name="txtCode" Margin="37,57,0,193" AcceptsReturn="True" TextWrapping="Wrap" BorderBrush="#FF4AA7D6" HorizontalAlignment="Left" Width="214">
        </TextBox>

        <Button Content="Générer code !" HorizontalAlignment="Left" Margin="84,524,0,12" Name="btnGenereCode" Width="133" Click="BtnGenereCode_Click" />
    </Grid>
</Window>

 

Le code behind de la fenêtre se présente ainsi :

        /// <summary>
        /// VueModel de la fenetre
        /// </summary>
        private readonly FenetrePrincipaleViewModel _fpvm;

        /// <summary>
        /// Méthode d'initialisation de la fenêtre
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
            this._fpvm = new FenetrePrincipaleViewModel();
            this.DataContext = this._fpvm;
        }

        /// <summary>
        /// Evenement sur le clique du bouton de génération du code
        /// </summary>
        /// <param name="sender">Objet de l'évènement</param>
        /// <param name="e">Arguments de l'évènement</param>
        private void BtnGenereCode_Click(object sender, RoutedEventArgs e)
        {
            this._fpvm.GenereCodeBasique(txtCode.Text);
        }

Notre ViewModel lui, à le code suivant :


    /// <summary>
    /// ViewModel de la vue : FenetrePrincipale
    /// </summary>
    public class FenetrePrincipaleViewModel : INotifyPropertyChanged
    {

        #region PropertyChanged Block

        /// <summary>
        /// Evenement du propertychanged
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Méthode qui va gérer les mise à jour graphique lorsqu'une propriété bindé change
        /// </summary>
        /// <param name="property">propriété qui a changé</param>
        private void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

        #endregion

        /// <summary>
        /// Constructeur de la classe
        /// </summary>
        public FenetrePrincipaleViewModel()
        {
        }

        /// <summary>
        /// Méthode qui va appeler la coucher model pour générer le code passé en paramètre
        /// </summary>
        /// <param name="code">Code à générer</param>
        public void GenereCodeBasique(string code)
        {
            GenerateurCode gc = new GenerateurCode();

            gc.GenerationDuCodeBasique(code);
        }
    }

Deux façons de faire se présente alors à nous du côté de la génération de code. La façon compliquée et la simple. Je vais vous présenter les deux pour que puissiez voir l'intérêt d'utiliser la DLL que je vais vous présenter.

En premier lieu, la version compliquée. Il faut écrire le code suivant :

 

         /// <summary>
         /// Méthode qui va générer le code sans DLL
         /// </summary>
         /// <param name="code">Code à générer</param>
         public void GenerationDuCodeBasique(string code)
         {
             CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
             CompilerParameters cp = new CompilerParameters();

             cp.ReferencedAssemblies.Add("System.dll");
             cp.ReferencedAssemblies.Add(@"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Windows.Forms.dll");

             string script = @"namespace GenerationScript {
                                        using System;
                                        using System.Windows.Forms;
                                        public class Script
                                        {
                                            public void ExecuteCode()
                                            {" +
                                                code +
                                            "}" +
                                       "}" +
                              "}";

             cp.GenerateInMemory = false;

             CompilerResults result = provider.CompileAssemblyFromSource(cp, script);

             Assembly loAssembly = result.CompiledAssembly;

             object loObject = loAssembly.CreateInstance("GenerationScript.Script");

             object loResult = loObject.GetType().InvokeMember("ExecuteCode", BindingFlags.InvokeMethod, null, loObject, null);
         }

Inutile de dire que... C'est horrible à lire ! Par ailleurs il faut ajouter à la main les références d'assembly, le calvaire.

Il existe heureusement une DLL nommée CS-Script qui permet de simplifier très largement le code. Ajouter donc la DLL en référence dans votre projet puis le code si compliqué plus haut deviens plus simplement :

 

        /// <summary>
        /// Méthode qui va générer le code à partir de la DLL CS-Script
        /// </summary>
        /// <param name="code">Code à générer</param>
        public void GenerationDuCodeCsScript(string code)
        {
             dynamic script = CSScript.LoadCode(@"namespace GenerationScript {
                                                        using System;
                                                        using System.Windows.Forms;
                                                        public class Script
                                                        {
                                                            public void ExecuteCode()
                                                            {" +
                                                                        code +
                                                           "}" +
                                                       "}" +
                                                 "}")
                                       .CreateObject("*");

             // Exécution du code généré
             script.ExecuteCode();
         }

Il est, je pense, nécessaire d'être en Framework 4.0 pour utiliser le type dynamic.

 

C'est quand même plus clair non ? Pensez en revanche que le temps d'exécution d'un code généré à la volée n'est pas le même qu'un appel classique. En effet le code doit être compilé avant et cela prend du temps...


comments powered by Disqus
Publié le Mercredi 14 Novembre 2012 à 11:21