www.rolandk.de
- Aktuelle Themen zu .Net -
Achtung: Hier handelt es sich um meine alte Seite.
Die aktuelle ist unter folgendem Link erreichbar: www.rolandk.de/wp/
Home




















































C#-Scripting 1 - C#-Editor mit Syntax Highlighting und Compiler
Mittwoch, den 03. Oktober 2012 um 17:25 Uhr

 

Allgemeines

In diesem ersten Teil dieses Tutorials zeige ich, wie sich ein einfacher Editor für eine C#-Scripting-Funktion im eigenen Programm umsetzen lässt. Der Artikel zeigt einen schnellen weg, einen einfachen Editor mit Syntax Highlighting bereitzustellen und zusätzlich kann der Code direkt per Knopfdruck kompiliert und ausgeführt werden. Nachfolgend ein Screenshot des Editors, auf Basis dessen dieser Artikel geschrieben wurde.

 

 

Erstellen des Projekts

Mithilfe von Visual Studio 2010 oder 2012 zusammen mit Nuget lässt sich das Beispiel relativ schnell nachbauen. Zuerst habe ich ein normales WPF Projekt erstellt und danach nachfolgende beiden Libraries per Nuget nachgeladen.

 

 

AvalonDock verwende ich für die dockbaren Fenster, damit der Editor eine ähnlich flexible Anordnung der Fenster wie Visual Studio erlaubt - schließlich wird das von einem Entwickler quasi schon von einer IDE erwartet. Die nächste Komponente AvalonEdit stammt aus dem SharpDevelop Projekt und kapselt den dort verwendeten Code-Editor. Sie kümmert sich schon von Haus aus um das Syntax Highlighting - es für die Sprache C# zu aktivieren beschränkt sich nur auf einen Parameter im Xaml-Code.

 

Erstellen des Hauptfensters

Beim Hauptfenster zunächst eine kurze Erläuterung zu den Oberflächen. Nachfolgender Screenshot zeigt ein Mockup von den umzusetzenden Oberflächen.

 

 

Im Prinzip geht es nur um drei Bestandteile. Der Code-Editor selbst, der dann auch die C#-Script-Datei enthält. Zusätzlich dazu gibt es eine Liste von referenzierten Assemblies und abschließend noch einen "Compile and execute!" Button. Soweit, so simpel. Zur Umsetzung des Code-Editors genügen dank AvalonEdit nur einige Zeilen Code.

  1. <avalonedit:TextEditor
  2. Name="TextEditor"
  3. SyntaxHighlighting="C#"
  4. FontFamily="Courier New"
  5. FontSize="10pt">
  6. <avalonedit:TextEditor.Options>
  7. <avalonedit:TextEditorOptions
  8. ConvertTabsToSpaces="True"
  9. EnableTextDragDrop="True" />
  10. </avalonedit:TextEditor.Options>
  11. </avalonedit:TextEditor>

Von der Seite her ist praktisch schon so gut wie alles geschenkt. Die Assembly-Liste auf der linken Seite wird schlicht als ListBox umgesetzt und bindet gegen eine Liste von Strings. Der Button "Compile and execute!" ist als normales MenuItem mit entsprechenden Ereignisbehandler umgesetzt. Die weiteren genaueren Implementierungs-Details finden sich im angehängten Beispielprojekt.

 

Erstellen des Compilers

Zum Kompilieren des C#-Codes verwende ich den auch schon in früheren Versionen des .Net Frameworks vorhandenden CSharpCodeProvider . Mithilfe dieser Klasse kann der C#-Compiler direkt angesprochen werden um etwa C#-Code in eine Dll zu kompilieren. Jetzt könnte man damit mehrere Sachen machen, das offensichtlichste wäre, den C#-Code aus dem Script in eine Dll auf die Festplatte zu kompilieren und anschließend per Reflection zu laden. Glücklicherweise kann man den Compiler aber so einstellen, dass die neu generierte Assembly nicht ins Dateisystem geschrieben werden soll, sondern dass sie direkt in den Speicher geschrieben und von dort geladen wird (Eigenschaft GenerateInMemory in der Klasse CompilerParameters). Nachfolgend eine Klasse von mir, welche intern den eben beschriebenen CSharpCodeProvider verwendet, um den Script zu kompilieren und zu laden.

  1. public class CSharpScriptCompiler
  2. {
  3. private CodeDomProvider m_codeCompiler;
  4.  
  5. /// <summary>
  6. /// Initializes a new instance of the <see cref="CSharpScriptCompiler" /> class.
  7. /// </summary>
  8. public CSharpScriptCompiler()
  9. {
  10. Dictionary<string, string> compilerParameters = new Dictionary<string,string>();
  11. compilerParameters["CompilerVersion"] = "v4.0";
  12. m_codeCompiler = CSharpCodeProvider.CreateProvider(
  13. "cs",
  14. compilerParameters);
  15. }
  16.  
  17. /// <summary>
  18. /// Compiles given source file.
  19. /// </summary>
  20. /// <param name="m_currentFile">The source file to be compiled.</param>
  21. public Action Compile(CSharpScriptFile m_currentFile)
  22. {
  23. //Set compiler parameters
  24. CompilerParameters compilerParameters = new CompilerParameters();
  25. compilerParameters.GenerateExecutable = false;
  26. compilerParameters.GenerateInMemory = true;
  27. compilerParameters.OutputAssembly = "ScriptedAssembly";
  28. compilerParameters.ReferencedAssemblies.AddRange(
  29. m_currentFile.ReferencedAssemblies.ToArray());
  30.  
  31. //Compile the code and return the result or throw an exception in case of an error
  32. CompilerResults results = m_codeCompiler.CompileAssemblyFromSource(
  33. compilerParameters,
  34. m_currentFile.ScriptContent);
  35. if (results.Errors.Count > 0)
  36. {
  37. StringBuilder compilerErrors = new StringBuilder();
  38. foreach (var actError in results.Errors)
  39. {
  40. compilerErrors.Append("" + actError + Environment.NewLine);
  41. }
  42. throw new InvalidOperationException(
  43. "Unable to compile scripts: " +
  44. compilerErrors.ToString());
  45. }
  46. else if (results.CompiledAssembly == null)
  47. {
  48. throw new InvalidOperationException(
  49. "Unable to compile scripts: " +
  50. "No result assembly from compiler!");
  51. }
  52.  
  53. //Try to get main class
  54. Assembly compiledAssembly = results.CompiledAssembly;
  55. Type scriptClass = compiledAssembly.GetType("ScriptedClass");
  56. if (scriptClass == null)
  57. {
  58. throw new InvalidOperationException(
  59. "Unable to compile scripts: " +
  60. "class ScriptedClass not found!");
  61. }
  62.  
  63. //Try to get main method
  64. MethodInfo scriptMethod = scriptClass.GetMethod(
  65. "Execute",
  66. BindingFlags.Static | BindingFlags.Public);
  67. if (scriptMethod == null)
  68. {
  69. throw new InvalidOperationException(
  70. "Unable to compile scripts: " +
  71. "method Execute not found within class ScriptedClass!");
  72. }
  73.  
  74. return () => scriptMethod.Invoke(null, null);
  75. }
  76. }

Das Kompilieren an sich ist in diesem Code-Ausschnitt nicht wirklich kompliziert. Der C#-Script wird in Form eines CSharpScriptFile Objekts der Compile Methode übergeben. Das Objekt selbst hält den C#-Code einfach nur als String, hat zusätzlich aber noch ein paar andere für den Compiler benötigte Eigenschaften wie z. B. eine Liste der referenzierten Assemblies (Die Klasse CSharpScriptFile ist eine selbst erstellte Klasse). Sobald der Quellcode mit der Methode CompileAssemblyFromSource erfolgreich kompiliert wurde, wird per Reflection der Einstiegspunkt ermittelt und in Form eines Delegaten zurückgegeben.

 

Zusammenfassung

In diesem Artikel bin ich auf die wichtigsten Punkte eingegangen, die zu beachten sind, um mit wenig Aufwand einen einfachen kleinen C#-Scripteditor zu schreiben, welcher C#-Code auch kompilieren und ausführen kann. Alle weiteren Details sind im angehängten Beispielprojekt zu finden. In den hierauf folgenden Artikeln werden weitere Themen behandelt, wie dieser Scripteditor noch um IntelliSense erweitert werden kann und wie man den Code auch debuggen kann.

Quellen

  • AvalonEdit
    Artikel auf CodeProject, der die AvalonEdit Komponente sehr gut beschreibt.
  • SharpDevelop
    Die Komponenten dieser freien C#-IDE dienen als Hilfsmittel für das hier umgesetzte Programm.
  • Nuget
    Packetverwaltungssystem für Visual Studio, auf das in den Artikeln öfters verwiesen wird.
  • SharpDevelopCodeCompletion
    OpenSource-Projekt, welches die Umsetzung eines Intellisense innerhalb des AvalonEdit Controls deutlich vereinfacht.
  • Visual Studio 2010 Shell
    Kostenlose Version von Visual Studio, welche als reiner Debugger genutzt werden kann.
 

Kommentar hinzufügen

Ihr Name:
Kommentar: