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 Tutorials C#-Scripting C#-Scripting 2 - C#-Einbinden des Intellisense-Features für C#




















































C#-Scripting 2 - C#-Einbinden des Intellisense-Features für C#
Mittwoch, den 03. Oktober 2012 um 17:40 Uhr

 

Allgemeines

Im letzten Artikel dieser Serie habe ich gezeigt, wie man mithilfe relativ leichter Bausteine eine Scripting-IDE für C#-Code erstellen kann. Dieser Artikel baut auf den Quellcode des letzten Artikels auf und erweitert den Editor um eine Intellisense-Funktion für C#-Code. Nachfolgend ein Screenshot des erweiterten Editors, auf dessen Basis dieser Artikel geschrieben wurde.

 

 

Nötige Komponenten

Zunächst erscheint es relativ kompliziert, ein solches Feature umzusetzen. Zwar bringt es schon fast gewaltigen Nutzen für den Benutzer und ist aus der aktuellen Editor-Welt nicht mehr weg zu denken, doch steht man als Entwickler zunächst blank da, wenn man so etwas umsetzen möchte. An der Stelle gibt es aber eine gute Nachricht: Um Details brauchen wir uns dank nützlicher OpenSource Projekte nicht kümmern. Für dieses Beispiel habe ich per Nuget die Bibliothek SharpDevelopCodeCompletion eingebunden. Dabei handelt es sich um eine OpenSource Bibliothek, welche selbst die Bibliotheken von SharpDevelop nutzt, um möglichst einfach Intellisense bereitstellen zu können. Nachfolgend ein Screenshot zu meinen installierten Paketen.

 

 

Als weitere Abhängigkeit kommt die Assembly System.Windows.Interactivity dazu. Diese Assembly wird dann benötigt, wenn man sog. Behaviors verwenden möchte (z. B. über Expression Blend). Sie ist nicht im Standard .Net Framework enthalten, kann aber kostenlos ins eigene Deployment integriert werden. Für diesen Artikel ist diese Assembly wichtig, da die Bibliothek SharpDevelopCodeCompletion eine Behavior verwendet, um den C#-Editor um Intellisense zu erweitern.

 

Anpassungen an Klasse CSharpScriptFile

Die Klasse CSharpScriptFile bildete schon im letzten Artikel die Datenquelle für den C#-Editor. Sie speichert den Quellcode und die aktuell referenzierten Assemblies. Für die Intellisense kommen zwei neue wichtige Member hinzu.

  1. /// <summary>
  2. /// Gets the current filter strategy for intellisense.
  3. /// </summary>
  4. public IFilterStrategy FilterStrategy
  5. {
  6. get { return m_filterStrategy; }
  7. }
  8.  
  9. /// <summary>
  10. /// Gets the IProjectContent object describing all code items
  11. /// that may be accessed through intellisense.
  12. /// </summary>
  13. public IProjectContent ProjectContent
  14. {
  15. get { return m_projectContent; }
  16. }

Diese beiden Schnittstellen kommen aus den SharpDevelop Assemblies und dienen dazu, die Datenquelle für das Intellisense bereitzustellen. Das Objekt hinter der IProjectContent Schnittstelle enthält alle im Projekt enthaltenen und verwiesenen Klassen, Methoden usw.. Das Anlegen dieser Objekte ist zunächst relativ einfach.

  1. m_contentRegistry = new ProjectContentRegistry();
  2. m_filterStrategy = new NonFilterStrategy();
  3. m_projectContent = new DefaultProjectContent();

Alles einfache Standardkonstruktoren. Hier sei noch darauf hingewiesen, dass ein weiteres rein privates Objekt vom Typ ProjectContentRegistry benötigt wird. In diesem Beispiel wird diese Klasse wie eine Factory verwendet, näheres dann im nächsten Snippet. Weiter geht es mit einer Methode, welche den Inhalt einer referenzierten Assembly ausließt und der Intellisense zur Verfügung stellen kann. Nachfolgend das Coding dazu.

  1. /// <summary>
  2. /// Parses the assembly behind the given assembly name and returns a IProjectContent
  3. /// object for using it for CodeCompletion feature.
  4. /// </summary>
  5. /// <param name="assemblyName">The name of the assembly (e. g. System.Windows.Forms).</param>
  6. /// <param name="registry">The registry used for loading assembly information.</param>
  7. private static IProjectContent ParseReferencedAssembly(
  8. string assemblyName, ProjectContentRegistry registry)
  9. {
  10. try
  11. {
  12. assemblyName = Path.GetFileNameWithoutExtension(assemblyName);
  13.  
  14. //Search within all currently loaded assemblies for assembly information
  15. foreach (Assembly actAssembly in AppDomain.CurrentDomain.GetAssemblies())
  16. {
  17. if (actAssembly.GetName().Name == assemblyName)
  18. {
  19. IProjectContent result = registry.GetProjectContentForReference(
  20. assemblyName, actAssembly.Location);
  21. return result;
  22. }
  23. }
  24.  
  25. //Try to load the assembly to get all information from it
  26. Assembly loadedAssembly = Assembly.LoadWithPartialName(assemblyName);
  27. if (loadedAssembly != null)
  28. {
  29. return registry.GetProjectContentForReference(assemblyName, loadedAssembly.Location);
  30. }
  31. }
  32. catch (Exception)
  33. {
  34. //Don't handle exception here, just return null for nothing found
  35. return null;
  36. }
  37.  
  38. return null;
  39. }

Übergeben wird der Name der Assembly, so wie er auch im C#-Editor angezeigt wird. Anschließend wird in diesem Beispiel versucht, die Assembly in der eigenen AppDomain zu finden oder zu laden. Der Zweck dahinter ist nicht, dass nur geladene Assemblies analysiert werden können, sondern das einfache Problem, den vollen Datei-Pfad zu den Assemblies herauszufinden. Die wirkliche Arbeit passiert bei dem Aufruf von der Methode GetProjectContentForReference der Klasse ProjectContentRegistry. Diese bekommt den Pfad zur Assembly und gibt nach etwas Rechenzeit das Ergebnis als IProjectContent zurück, welches auch direkt der Rückgabewert der eigenen Methode ist.

 

Jetzt gilt es noch, die eben beschriebene Methode aufzurufen, sobald der Benutzer eine weitere Referenz hinzufügt. Umgesetzt werden kann das durch nachfolgenden Ereignis-Behandler.

  1. m_referencedAssemblies.CollectionChanged += OnReferencedAssembliesCollectionChanged;
  2.  
  3. /// <summary>
  4. /// Called when the collection of referenced assemblies has changed.
  5. /// </summary>
  6. /// <param name="sender">The sender.</param>
  7. /// <param name="e">
  8. /// The <see cref="NotifyCollectionChangedEventArgs" />
  9. /// instance containing the event data.
  10. /// </param>
  11. private async void OnReferencedAssembliesCollectionChanged(
  12. object sender, NotifyCollectionChangedEventArgs e)
  13. {
  14. if ((e.NewItems != null) &amp;&amp; (e.NewItems.Count > 0))
  15. {
  16. foreach (string actNewItem in e.NewItems)
  17. {
  18. if (!m_standardReferences.ContainsKey(actNewItem))
  19. {
  20. string asyncLoadedContentName = actNewItem;
  21.  
  22. //Load IProjectContent object asynchron
  23. IProjectContent asyncLoadedContent = await Task.Run<IProjectContent>(() =>
  24. {
  25. return ParseReferencedAssembly(
  26. asyncLoadedContentName, m_contentRegistry);
  27. });
  28.  
  29. //Append IProjectContent to main IProjectContent object
  30. if (asyncLoadedContent != null)
  31. {
  32. m_standardReferences[asyncLoadedContentName] = asyncLoadedContent;
  33. m_projectContent.AddReferencedContent(asyncLoadedContent);
  34. }
  35. }
  36. }
  37. }
  38. }

Achtung: Diese Methode verwendet bereits das neue Schlüsselwort async. Es bewirkt hier im Prinzip nur, dass das Parsen der referenzierten Assembly im Hintergrund gemacht wird. Eine genauere Beschreibung der Schlüsselworte async und await erfolgen an anderer Stelle. Unabhängig davon passiert in der Methode folgendes: Es wird versucht, die verwiesene Assembly zu parsen und das IProjectContent Objekt dafür zu bekommen. Sobald das Objekt erzeugt wurde, wird es zum eigentlichen IProjectContent per AddReferencedContent Methode hinzugefügt - mit anderen Worten: es wird gemerged. Bei letzterem handelt es sich auch um das Objekt, auf das die Intellisense zugreift bzw. welches als Eigenschaft in der Klasse CSharpScriptFile zur Verfügung gestellt wird.

 

Anpassungen an MainWindow

Die Anpassungen am Hauptfenster fallen sehr klein aus, da hier sehr viel Arbeit von den verwiesenen Bibliotheken übernommen wird. Erster Schritt hier ist die Einbindung eines neuen Namensraumes im Xaml-Code

  1. xmlns:codecomplete="clr-
  2. namespace:ICSharpCode.AvalonEdit.CodeCompletion;assembly=ICSharpCode.AvalonEdit.CodeCompletion"

Als nächstes muss man nur noch eine Anpassung am TextEditor-Element im Xaml-Code machen. Durch die SharpDevelopCodeCompletion Bibliothek wird ein Behavior zur Verfügung gestellt, welche sich eigentlich um alles kümmert. Es reicht für dieses Beispiel hier also, das Behavior an den TextEditor ran zu hängen.

  1. <avalonedit:TextEditor
  2. Name="TextEditor"
  3. SyntaxHighlighting="C#"
  4. FontFamily="Courier New"
  5. FontSize="10pt"
  6. Document="{Binding CodeDocument}">
  7. <avalonedit:TextEditor.Options>
  8. <avalonedit:TextEditorOptions
  9. ConvertTabsToSpaces="True"
  10. EnableTextDragDrop="True" />
  11. </avalonedit:TextEditor.Options>
  12. <i:Interaction.Behaviors>
  13. <codecomplete:CodeCompletionBeahvior
  14. FilterStrategy="{Binding FilterStrategy}"
  15. ProjectContent="{Binding ProjectContent}" />
  16. </i:Interaction.Behaviors>
  17. </avalonedit:TextEditor>

Das war es dann auch schon. Wichtig sind hier hauptsächlich die beiden Bindings auf FilterStrategy und ProjectContent, bei denen es sich um die Verknüpfungen zu den weiter oben hinzugefügten Eigenschaften handelt.

 

Zusammenfassung

In diesem Artikel habe ich beschrieben, wie man mithilfe einer guten OpenSource Bibliotheken den C#-Editor aus dem letzten Artikel dieser Serie um Intellisense erweitern kann. Alle weiteren Details und ein lauffähiges Beispiel finden sich im angehängten Beispielprojekt. Im nächsten Artikel gehe ich noch darauf ein, wie man den Script-Code 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: