Mai 2016

Band 31, Nummer 5

.NET Compiler Platform – Optimieren der Model-View-ViewModel-Umgebung mit Roslyn

Von Alessandro Del Del

Model-View-ViewModel (MVVM) ist ein sehr beliebtes Architekturmuster, das perfekt mit XAML-Anwendungsplattformen wie Windows Presentation Foundation (WPF) und der Universellen Windows-Plattform (UWP) zusammenarbeitet. Das Verwenden einer MVVM-Architektur für eine Anwendung bietet neben vielen anderen Vorteilen eine saubere Trennung zwischen den Daten, der Anwendungslogik und der Benutzeroberfläche. Auf diese Weise können Anwendungen einfacher gewartet und getestet werden, die Wiederverwendung von Code wird verbessert, und Designer können an der Benutzeroberfläche arbeiten, ohne mit der Programmlogik oder den Daten interagieren zu müssen. Im Lauf der Jahre wurden zahlreiche Bibliotheken, Projektvorlagen und Frameworks (z. B. Prism und das MVVM Light Toolkit) erstellt, um Entwickler bei der einfachen und effizienten Implementierung von MVVM unterstützen. Unter bestimmten Umständen können Sie jedoch keine externen Bibliotheken verwenden, oder Sie möchten das Muster einfach schnell implementieren und dabei den Fokus auf Ihren Code legen. Es steht eine Vielzahl von MVVM-Implementierungen zur Verfügung. Die meisten verwenden jedoch zahlreiche allgemeine Objekte gemeinsam, deren Generierung auf einfache Weise mit den Roslyn-APIs automatisiert werden kann. In diesem Artikel erläutere ich, wie benutzerdefinierte Roslyn-Refactorings erstellt werden, die das Generieren von Elementen vereinfachen, die für jede MVVM-Implementierung gelten. Ich kann Ihnen hier keine vollständige Zusammenfassung zu MVVM bieten. Ich gehe aber davon aus, dass Sie bereits über Grundkenntnisse des MVVM-Musters, der zugehörigen Terminologie und der Roslyn-Codeanalyse-APIs verfügen. Wenn Sie Ihr Wissen auffrischen möchten, können Sie die folgenden Artikel lesen: „WPF-Anwendungen mit dem Model-View-ViewModel-Entwurfsmuster“, „C# und Visual Basic: Verwenden von Roslyn zum Erstellen eines Live-Codeanalysemoduls für Ihre API” und „C# – Hinzufügen von Codefixing zum Roslyn-Analyzer“.

Der begleitende Code ist in C#- und Visual Basic-Versionen verfügbar. Diese Version des Artikels umfasst C#- und Visual Basic-Listings.

Allgemeine MVVM-Klassen

Jede typische MVVM-Implementierung erfordert mindestens die folgenden Klassen (in einigen Fällen abhängig vom angewendeten MVVM-Typ mit geringfügig anderen Namen):

ViewModelBase – Eine abstrakte Basisklasse, die Member bereitstellt, die allen „ViewModels“ in der Anwendung gemeinsam sind. Die allgemeinen Member unterscheiden sich ggf. abhängig von der Architektur der Anwendung. Die einfachste Implementierung verwendet jedoch schon Änderungsbenachrichtigungen für jedes abgeleitete „ViewModel“.

RelayCommand – Eine Klasse, die einen Befehl darstellt, durch den „ViewModels“ Methoden aufrufen können. Normalerweise sind zwei Versionen von „RelayCommand“ verfügbar: eine generische Version und eine nicht generische. In diesem Artikel verwende ich die generische Version („RelayCommand<T>“).

Ich gehe davon aus, dass Sie bereits mit beiden Versionen vertraut sind, und werde sie daher nicht weiter beschreiben. Abbildung 1a zeigt den C#-Code für „ViewModelBase“, Abbildung 1b den Visual Basic-Code.

Abbildung 1a: ViewModelBase-Klasse (C#)

abstract class ViewModelBase : System.ComponentModel.INotifyPropertyChanged
{
  public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
  // Raise a property change notification
  protected virtual void OnPropertyChanged(string propertyName)
  {
    PropertyChanged?.Invoke(this, new System.ComponentModel.
PropertyChangedEventArgs(propertyName));
  }
}

Abbildung 1b: ViewModelBase-Klasse (Visual Basic)

Public MustInherit Class ViewModelBase
   Implements System.ComponentModel.INotifyPropertyChanged
   Public Event PropertyChanged(sender As Object,
                                e As System.ComponentModel.PropertyChangedEventArgs) _
                                Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
   Protected Sub OnPropertyChanged(propertyName As String)
     RaiseEvent PropertyChanged(Me, New System.ComponentModel.
                                PropertyChangedEventArgs(propertyName))
   End Sub
 End Class

Dies ist die einfachste Implementierung von „ViewModelBase“. Sie stellt nur Eigenschaftenänderungsbenachrichtigungen basierend auf der INotifyPropertyChanged-Schnittstelle bereit. Natürlich können abhängig von Ihren jeweiligen Anforderungen weitere Member vorhanden sein. Abbildung 2a zeigt den C#-Code für „RelayCommand<T>“, Abbildung 2b zeigt den Visual Basic-Code.

Abbildung 2a: RelayCommand<T>-Klasse (C#)

class RelayCommand<T> : System.Windows.Input.ICommand
{
  readonly Action<T> _execute = null;
  readonly Predicate<T> _canExecute = null;
  public RelayCommand(Action<T> execute)
    : this(execute, null)
  {
  }
  public RelayCommand(Action<T> execute, Predicate<T> canExecute)
  {
    if (execute == null)
        throw new ArgumentNullException(nameof(execute));
    _execute = execute;
    _canExecute = canExecute;
  }
  [System.Diagnostics.DebuggerStepThrough]
  public bool CanExecute(object parameter)
  {
    return _canExecute == null ? true : _canExecute((T)parameter);
  }
  public event EventHandler CanExecuteChanged;
  public void RaiseCanExecuteChanged()
  {
    var handler = CanExecuteChanged;
    if (handler != null)
    {
      handler(this, EventArgs.Empty);
    }
  }
  public void Execute(object parameter)
  {
    _execute((T)parameter);
  }
}

Abbildung 2b: RelayCommand(Of T)-Klasse (Visual Basic)

Class RelayCommand(Of T)
   Implements System.Windows.Input.ICommand
   Private ReadOnly _execute As Action(Of T)
   Private ReadOnly _canExecute As Predicate(Of T)
   Public Sub New(execute As Action(Of T))
     Me.New(execute, Nothing)
   End Sub
   Public Sub New(execute As Action(Of T), canExecute As Predicate(Of T))
     If execute Is Nothing Then
       Throw New ArgumentNullException(NameOf(execute))
     End If
     _execute = execute
     _canExecute = canExecute
   End Sub
   <System.Diagnostics.DebuggerStepThrough>
   Public Function CanExecute(parameter As Object) As Boolean _
     Implements System.Windows.Input.ICommand.CanExecute
     Return If(_canExecute Is Nothing, True, _canExecute(parameter))
   End Function
   Public Event CanExecuteChanged As EventHandler Implements _
     System.Windows.Input.ICommand.CanExecuteChanged
   Public Sub RaiseCanExecuteChanged()
     RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
   End Sub
   Public Sub Execute(parameter As Object) Implements ICommand.Execute
     _execute(parameter)
   End Sub
 End Class

Dies ist die am häufigsten verwendete Implementierung von „RelayCommand<T>“. Sie eignet sich für die meisten MVVM-Szenarien. Es sollte erwähnt werden, dass diese Klasse die System.Windows.Input.ICommand-Schnittstelle implementiert, die die Implementierung einer Methode namens „CanExecute“ erfordert, deren Ziel darin besteht, den Aufrufer zu informieren, ob ein Befehl für die Ausführung verfügbar ist.

So kann Roslyn Ihr Leben einfacher machen

Wenn Sie nicht mit externen Frameworks arbeiten, kann Ihnen Roslyn das Leben erheblich erleichtern: Sie können benutzerdefinierte Coderefactorings erstellen, die eine Klassendefinition ersetzen und automatisch ein erforderliches Objekt implementieren. Außerdem können Sie die Generierung von ViewModel-Klassen basierend auf den Modelleigenschaften auf einfache Weise automatisieren. Abbildung 3 zeigt ein Beispiel dafür, was Sie bis zum Ende dieses Artikels erreichen können.

Implementieren von MVVM-Objekten mit einem benutzerdefinierten Roslyn-Refactoring
Abbildung 3: Implementieren von MVVM-Objekten mit einem benutzerdefinierten Roslyn-Refactoring

Der Vorteil dieses Ansatzes besteht darin, dass der Fokus immer auf dem Code-Editor liegt und Sie die erforderlichen Objekte sehr schnell implementieren können. Außerdem können Sie ein benutzerdefiniertes „ViewModel“ basierend auf der Modellklasse (wie weiter unten in diesem Artikel gezeigt) generieren. Beginnen wir mit dem Erstellen eines Refactoringprojekts.

Erstellen eines Projekts für Roslyn-Refactorings

Der erste Schritt besteht im Erstellen eines neuen Roslyn-Refactorings. Sie verwenden zu diesem Zweck die Coderefactoring-Projektvorlage (VSIX-Vorlage), die im Knoten „Erweiterbarkeit“ unter der Sprache Ihrer Wahl im Dialogfeld „Neues Projekt“ verfügbar ist. Nennen Sie das neue Projekt „MVVM_Refactoring“. Abbildung 4 zeigt dies.

Erstellen eines Roslyn-Refactoringprojekts
Abbildung 4: Erstellen eines Roslyn-Refactoringprojekts

Klicken Sie abschließend auf „OK“ Wenn Visual Studio 2015 das Projekt generiert, wird automatisch eine Klasse namens „MVVMRefactoringCodeRefactoringProvider“ hinzugefügt, die in der Datei „CodeRefactoringProvider.cs“ (oder „CodeRefactoringProvider.vb“ für Visual Basic) definiert wird. Benennen Sie die Klasse und die Datei in „MakeViewModelBaseRefactoring“ bzw. „MakeViewModelBaseRefactoring.cs“ um. Entfernen Sie aus Gründen der Klarheit die automatisch generierten ComputeRefactoringsAsync- und ReverseTypeNameAsync-Methoden (letztere wird für Demozwecke automatisch generiert).

Untersuchen eines Syntaxknotens

Wie Sie vielleicht wissen, ist die ComputeRefactoringsAsync-Methode der Haupteinstiegspunkt für ein Coderefactoring Sie ist für das Erstellen einer sogenannten Schnellaktion verantwortlich, die als Plug-In in die Glühbirne des Code-Editors integriert wird, wenn die Codeanalyse eines Syntaxknotens den erforderlichen Regeln genügt. In unserem Fall muss die ComputeRefactoringsAsync-Methode erkennen, ob der Entwickler die Glühbirne über eine Klassendeklaration aufruft. Mithilfe des Fensters des Tools Syntax Visualizer können Sie die Syntaxelemente auf einfache Weise erkennen, mit denen Sie arbeiten müssen. Genauer gesagt: In C# müssen Sie ermitteln, ob der Syntaxknoten eine „ClassDeclaration“ ist, die durch ein Objekt vom Typ „Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax“ dargestellt wird (siehe Abbildung 5). In Visual Basic ermitteln Sie, ob der Syntaxknoten ein „ClassStatement“ ist, das durch ein Objekt vom Typ „Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax“ dargestellt wird. Tatsächlich ist „ClassStatement“ in Visual Basic ein untergeordneter Knoten von „ClassBlock“. Dieses Element stellt den gesamten Code für eine Klasse dar. Der Grund dafür, dass C# und Visual Basic verschiedene Objekte verwenden, liegt in der Art und Weise, wie diese beiden Sprachen eine Klassendefinition darstellen: C# verwendet ein Klassenschlüsselwort mit geschweiften Klammern als Trennzeichen. Visual Basic verwendet das Klassenschlüsselwort mit der End Class-Anweisung als Trennzeichen.

Informationen zum Verständnis einer Klassendeklaration
Abbildung 5: Informationen zum Verständnis einer Klassendeklaration

Erstellen einer Aktion

Das erste Coderefactoring, das ich beschreibe, bezieht sich auf die ViewModelBase-Klasse. Der erste Schritt besteht im Schreiben der ComputeRefactoringsAsync-Methode in der MakeViewModelBaseRefactoring-Klasse. Mit dieser Methode überprüfen Sie, ob der Syntaxknoten eine Klassendeklaration darstellt. Wenn dies der Fall ist, können Sie eine Aktion erstellen und registrieren, die in der Glühbirne verfügbar ist. Abbildung 6a zeigt, wie dieses Ziel in C# erreicht wird, Abbildung 6b zeigt den Visual Basic-Code (siehe Kommentare im Code).

Abbildung 6a: Der Haupteinstiegspunkt: ComputeRefactoringsAsync-Methode (C#)

private string Title = "Make ViewModelBase class";
public async sealed override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
  // Get the root node of the syntax tree
  var root = await context.Document.
    GetSyntaxRootAsync(context.CancellationToken).
    ConfigureAwait(false);
  // Find the node at the selection.
  var node = root.FindNode(context.Span);
  // Is this a class statement node?
  var classDecl = node as ClassDeclarationSyntax;
  if (classDecl == null)
  {
    return;
  }
  // If so, create an action to offer a refactoring
  var action = CodeAction.Create(title: Title,
    createChangedDocument: c =>
    MakeViewModelBaseAsync(context.Document,
      classDecl, c), equivalenceKey: Title);
  // Register this code action.
  context.RegisterRefactoring(action);
}

Abbildung 6b: Der Haupteinstiegspunkt: ComputeRefactoringsAsync-Methode (Visual Basic)

Private Title As String = "Make ViewModelBase class"
 Public NotOverridable Overrides Async Function _
   ComputeRefactoringsAsync(context As CodeRefactoringContext) As Task
   ' Get the root node of the syntax tree
   Dim root = Await context.Document.
     GetSyntaxRootAsync(context.CancellationToken).
     ConfigureAwait(False)
   ' Find the node at the selection.
   Dim node = root.FindNode(context.Span)
   ' Is this a class statement node?
   Dim classDecl = TryCast(node, ClassStatementSyntax)
   If classDecl Is Nothing Then Return
   ' If so, create an action to offer a refactoring
   Dim action = CodeAction.Create(title:=Title,
                                  createChangedDocument:=Function(c) _
                                  MakeViewModelBaseAsync(context.
                                  Document, classDecl, c),
                                  equivalenceKey:=Title)
   ' Register this code action.
   context.RegisterRefactoring(action)
 End Function

Mit diesem Code haben Sie eine Aktion registriert, die für den Syntaxknoten aufgerufen werden kann, wenn es sich um eine Klassendeklaration handelt. Die Aktion wird von der MakeViewModelBaseAsync-Methode ausgeführt, die die Refactoringlogik implementiert und eine brandneue Klasse bereitstellt.

Codegenerierung

Roslyn stellt nicht nur ein objektorientiertes, strukturiertes Verfahren zum Darstellen von Quellcode zur Verfügung, sondern ermöglicht auch die Analyse von Quelltext sowie das Generieren einer Syntaxstruktur mit voller Genauigkeit. Zum Generieren einer neuen Syntaxstruktur aus reinem Text rufen Sie die SyntaxFactory.ParseSyntaxTree-Methode auf. Diese nimmt ein Argument vom Typ „System.String“ an, das den Quellcode enthält, aus dem Sie ein SyntaxTree-Objekt generieren möchten.

Roslyn stellt außerdem die VisualBasicSyntaxTree.ParseText- und CSharpSyntaxTree.ParseText-Methoden zur Verfügung, um das gleiche Ergebnis zu erzielen. In diesem Fall ist es jedoch sinnvoll, „SyntaxFactory.ParseSyntaxTree“ zu verwenden, weil der Code andere Analysemethoden aus „SyntaxFactory“ aufruft, wie Sie bald sehen werden.

Nachdem Sie eine neue SyntaxTree-Instanz erstellt haben, können Sie Codeanalyse und andere codebezogene Vorgänge ausführen. Sie können z. B. den Quellcode einer ganzen Klasse analysieren, aus diesem eine Syntaxstruktur generieren, einen Syntaxknoten in der Klasse ersetzen sowie eine neue Klasse zurückgeben. Im Fall des MVVM-Musters weisen allgemeine Klassen eine feste Struktur auf. Der Vorgang des Analysierens von Quelltext und Ersetzens einer Klassendefinition durch eine neue Definition ist daher schnell und einfach. Durch Nutzen sogenannter mehrzeiliger Zeichenfolgenliterale können Sie eine vollständige Klassendefinition in ein Objekt vom Typ „System.String“ einfügen, dann ein SyntaxTree-Objekt daraus abrufen, den „SyntaxNode“ abrufen, der der Klassendefinition entspricht, und dann die ursprüngliche Klasse in der Struktur durch die neue Klasse ersetzen. Ich zeige zuerst, wie dies in Bezug auf die ViewModelBase-Klasse erreicht wird. Abbildung 7a zeigt den Code für C#, Abbildung 7b den Code für Visual Basic.

Abbildung 7a: MakeViewModelBaseAsync: Generieren einer neuen Syntaxstruktur aus Quelltext (C#)

private async Task<Document> MakeViewModelBaseAsync(Document document,
  ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
  // The class definition represented as source text
  string newImplementation = @"abstract class ViewModelBase : INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propertyName)
{
  PropertyChanged?.Invoke(this, new System.ComponentModel.
    PropertyChangedEventArgs(propertyName));
}
}
";
  // 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
  // 2. GetRoot() gets the root node of the tree
  // 3. OfType<ClassDeclarationSyntax>().FirstOrDefault()
  //    retrieves the only class definition in the tree
  // 4. WithAdditionalAnnotations() is invoked for code formatting
  var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
    GetRoot().DescendantNodes().
    OfType<ClassDeclarationSyntax>().
    FirstOrDefault().
    WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
  // Get the root SyntaxNode of the document
  var root = await document.GetSyntaxRootAsync();
  // Generate a new CompilationUnitSyntax (which represents a code file)
  // replacing the old class with the new one
  CompilationUnitSyntax newRoot = (CompilationUnitSyntax)root.
    ReplaceNode(classDeclaration, newClassNode).NormalizeWhitespace();
  // Detect if a using System.ComponentModel directive already exists.
  if ((newRoot.Usings.Any(u => u.Name.ToFullString() ==
    "System.ComponentModel"))== false)
  {
    // If not, add one
    newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
                    SyntaxFactory.IdentifierName("ComponentModel"))));
  }
  // Generate a new document based on the new SyntaxNode
  var newDocument = document.WithSyntaxRoot(newRoot);
  // Return the new document
  return newDocument;
}

Abbildung 7b: MakeViewModelBaseAsync: Generieren einer neuen Syntaxstruktur aus Quelltext (Visual Basic)

Private Async Function MakeViewModelBaseAsync(document As Document,
   classDeclaration As ClassStatementSyntax,
   cancellationToken As CancellationToken) As Task(Of Document)
   ' The class definition represented as source text
   Dim newImplementation = "Public MustInherit Class ViewModelBase
 Implements INotifyPropertyChanged
 Public Event PropertyChanged(sender As Object,
                              e As PropertyChangedEventArgs) _
                              Implements INotifyPropertyChanged.PropertyChanged
 Protected Sub OnPropertyChanged(propertyName As String)
   RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
 End Sub
 End Class
 "
   ' 1. ParseSyntaxTree() gets a New SyntaxTree from the source text
   ' 2. GetRoot() gets the root node of the tree
   ' 3. OfType(Of ClassDeclarationSyntax)().FirstOrDefault()
   '    retrieves the only class definition in the tree
   ' 4. WithAdditionalAnnotations() Is invoked for code formatting
   Dim newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
     GetRoot().DescendantNodes().
     OfType(Of ClassBlockSyntax)().
     FirstOrDefault().
     WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
   Dim parentBlock = CType(classDeclaration.Parent, ClassBlockSyntax)
   ' Get the root SyntaxNode of the document
   Dim root = Await document.GetSyntaxRootAsync(cancellationToken)
   ' Generate a new CompilationUnitSyntax (which represents a code file)
   ' replacing the old class with the new one
   Dim newRoot As CompilationUnitSyntax = root.ReplaceNode(parentBlock,
                                                           newClassNode).
                                                           NormalizeWhitespace()
   ' Detect if an Imports System.ComponentModel directive already exists
  ' If Not newRoot.Imports.Any(Function(i) i.ImportsClauses.
    Where(Function(f) f.ToString = "System.ComponentModel").Any) Then            
  ' If not, add one
     Dim newImp = SyntaxFactory.
       ImportsStatement(SyntaxFactory.
       SingletonSeparatedList(Of ImportsClauseSyntax)(SyntaxFactory.
       SimpleImportsClause(SyntaxFactory.
       ParseName("System.ComponentModel"))))
     newRoot = newRoot.AddImports(newImp)
   End If
   ' Generate a new document based on the new SyntaxNode
   Dim newDocument = document.WithSyntaxRoot(newRoot)
 ' Return the new document
   Return newDocument
 End Function

Da der SyntaxFactory-Typ mehrmals verwendet wird, könnten Sie ggf. einen statischen Import in Betracht ziehen und auf diese Weise Ihren Code vereinfachen, indem Sie eine Imports Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory-Direktive in Visual Basic bzw. eine using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory-Direktive in C# hinzufügen. Ich verwende hier keinen statischen Import, damit es einfacher wird, Ihnen einige der Methoden vorzustellen, die „SyntaxFactory“ bereitstellt.

Beachten Sie, dass die MakeViewModelBaseAsync-Methode drei Argumente annimmt:

  • Ein Dokument, das die aktuelle Quellcodedatei darstellt.
  • Eine „ClassDeclarationSyntax“ (bzw. eine „ClassStatementSyntax“ in Visual Basic), die die Klassendeklaration darstellt, über die die Codeanalyse ausgeführt wird.
  • Ein „CancellationToken“, das verwendet wird, wenn der Vorgang abgebrochen werden muss.

Der Code ruft zuerst „SyntaxFactory.ParseSyntaxTree“ auf, um eine neue SyntaxTree-Instanz basierend auf dem Quelltext abzurufen, die die neue ViewModelBase-Klasse darstellt. Der Aufruf von „GetRoot“ ist erforderlich, um die SyntaxNode-Stamminstanz für die Syntaxstruktur abzurufen. In diesem speziellen Szenario wissen Sie im Voraus, dass der analysierte Quelltext nur eine Klassendefinition aufweist. Der Code ruft daher „FirstOrDefault<T>“ über „OfType<T>“ auf, um den einen Nachfolgerknoten des erforderlichen Typs abzurufen. Dabei handelt es sich um „ClassDeclarationSyntax“ in C# und „ClassBlockSyntax“ in Visual Basic. An diesem Punkt müssen Sie die ursprüngliche Klassendefinition durch die ViewModelBase-Klasse ersetzen. Zu diesem Zweck ruft der Code zuerst „Document.GetSyntaxRootAsync“ auf, um den Stammknoten für die Syntaxstruktur des Dokuments asynchron abzurufen. Anschließend ruft er „ReplaceNode“ auf, um die alte Klassendefinition durch die neue ViewModelBase-Klasse zu ersetzen. Beachten Sie, wie der Code ermittelt, ob eine using- (C#) oder Imports-Direktive (Visual Basic) für den Namespace „System.ComponentModel“ vorhanden ist, indem er die Sammlungen „CompilationUnitSyntax.Usings“ bzw. „CompilationUnitSyntax.Imports“ untersucht. Wenn dies nicht der Fall ist, wird eine entsprechende Direktive hinzugefügt. Dies ist für das Hinzufügen einer Direktive auf Codedateiebene sinnvoll, wenn diese nicht bereits verfügbar ist.

Denken Sie daran, dass Objekte in Roslyn unveränderlich sind. Dieses gleiche Konzept gilt auch für die String-Klasse: Tatsächlich ändern Sie niemals eine Zeichenfolge. Wenn Sie eine Zeichenfolge bearbeiten oder Methoden wie „Replace“, „Trim“ oder „Substring“ aufrufen, erhalten Sie eine neue Zeichenfolge mit den angegebenen Änderungen. Aus diesem Grund erstellen Sie jedes Mal, wenn Sie einen Syntaxknoten bearbeiten müssen, in der Tat einen neuen Syntaxknoten mit aktualisierten Eigenschaften.

In Visual Basic muss der Code auch das übergeordnete ClassBlockSyntax-Objekt für den aktuellen Syntaxknoten abrufen, der stattdessen vom Typ „ClassStatementSyntax“ ist. Dies ist erforderlich, um die Instanz des aktuellen SyntaxNode-Objekts abzurufen, das ersetzt wird. Das Bereitstellen einer allgemeinen Implementierung der RelayCommand<T>-Klasse funktioniert auf genau die gleiche Weise. Sie müssen jedoch ein neues Coderefactoring hinzufügen. Klicken Sie zu diesem Zweck im Projektmappen-Explorer mit der rechten Maustaste auf den Projektnamen, und wählen Sie dann „Hinzufügen | Neues Element“ aus. Wählen Sie im Dialogfeld „Neues Element hinzufügen“ die Refactoringvorlage aus, und nennen Sie die neue Datei „MakeRelayCommandRefactoring.cs“ (oder „MakeRelayCommandRefactoring.vb“ für Visual Basic). Die Refactoringprogrammlogik ist die gleiche wie für die ViewModelBase-Klasse (natürlich mit anderem Quelltext). Abbildung 8a zeigt den vollständigen C#-Code für das neue Refactoring, das die ComputeRefactoringsAsync- und MakeRelayCommandAsync-Methoden enthält. Abbildung 8b zeigt den Visual Basic-Code.

Abbildung 8a: Coderefactoring, das die RelayCommand<T>-Klasse implementiert (C#)

[ExportCodeRefactoringProvider(LanguageNames.CSharp,
  Name = nameof(MakeRelayCommandRefactoring)), Shared]
internal class MakeRelayCommandRefactoring : CodeRefactoringProvider
{
  private string Title = "Make RelayCommand<T> class";
  public async sealed override Task
    ComputeRefactoringsAsync(CodeRefactoringContext context)
  {
    var root = await context.Document.GetSyntaxRootAsync(context.
      CancellationToken).
        ConfigureAwait(false);
    // Find the node at the selection.
    var node = root.FindNode(context.Span);
    // Only offer a refactoring if the selected node is
    // a class statement node.
    var classDecl = node as ClassDeclarationSyntax;
    if (classDecl == null)
    {
      return;
    }
    var action = CodeAction.Create(title: Title,
      createChangedDocument: c =>
      MakeRelayCommandAsync(context.Document,
      classDecl, c), equivalenceKey: Title);
    // Register this code action.
    context.RegisterRefactoring(action);
  }
  private async Task<Document>
    MakeRelayCommandAsync(Document document,
    ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
  {
    // The class definition represented as source text
    string newImplementation = @"
class RelayCommand<T> : ICommand
{
  readonly Action<T> _execute = null;
  readonly Predicate<T> _canExecute = null;
  public RelayCommand(Action<T> execute)
    : this(execute, null)
  {
  }
  public RelayCommand(Action<T> execute, Predicate<T> canExecute)
  {
    if (execute == null)
      throw new ArgumentNullException(""execute"");
      _execute = execute;
        _canExecute = canExecute;
  }
  [System.Diagnostics.DebuggerStepThrough]
  public bool CanExecute(object parameter)
  {
    return _canExecute == null ? true : _canExecute((T)parameter);
  }
  public event EventHandler CanExecuteChanged;
  public void RaiseCanExecuteChanged()
  {
    var handler = CanExecuteChanged;
    if (handler != null)
    {
      handler(this, EventArgs.Empty);
    }
  }
  public void Execute(object parameter)
  {
    _execute((T)parameter);
  }
}
";
    // 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
    // 2. GetRoot() gets the root node of the tree
    // 3. OfType<ClassDeclarationSyntax>().FirstOrDefault() retrieves the only class
    //      definition in the tree
    // 4. WithAdditionalAnnotations() is invoked for code formatting
    var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
      GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>().
      FirstOrDefault().
      WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
    // Get the root SyntaxNode of the document
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    // Generate a new CompilationUnitSyntax (which represents a code file)
    // replacing the old class with the new one
    CompilationUnitSyntax newRoot = (CompilationUnitSyntax) root.
      ReplaceNode(classDeclaration,
      newClassNode).NormalizeWhitespace();
    if ((newRoot.Usings.Any(u => u.Name.ToFullString() == "System.Windows.Input"))
      == false)
    {
      newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
        QualifiedName(SyntaxFactory.IdentifierName("System"),
          SyntaxFactory.IdentifierName("Windows.Input"))));
    }
    // Generate a new document based on the new SyntaxNode
    var newDocument = document.WithSyntaxRoot(newRoot);
    // Return the new document
    return newDocument;
  }
}

Abbildung 8b: Coderefactoring, das die RelayCommand<Of T>-Klasse implementiert (Visual Basic)

<ExportCodeRefactoringProvider(LanguageNames.VisualBasic,
                                Name:=NameOf(MakeRelayCommandRefactoring)), [Shared]>
 Friend Class MakeRelayCommandRefactoring
   Inherits CodeRefactoringProvider
   Public NotOverridable Overrides Async Function _
     ComputeRefactoringsAsync(context As CodeRefactoringContext) As Task
     Dim root = Await context.Document.
       GetSyntaxRootAsync(context.CancellationToken).
       ConfigureAwait(False)
     ' Find the node at the selection.
     Dim node = root.FindNode(context.Span)
     ' Only offer a refactoring if the selected node is a class statement node.
     Dim classDecl = TryCast(node, ClassStatementSyntax)
     If classDecl Is Nothing Then Return
     Dim action = CodeAction.Create("Make RelayCommand(Of T) class",
                                    Function(c) _
                                    MakeRelayCommandAsync(context.Document,
                                                          classDecl, c))
     ' Register this code action.
     context.RegisterRefactoring(action)
  End Function
   Private Async Function MakeRelayCommandAsync(document As Document, _
    classDeclaration As ClassStatementSyntax, cancellationToken As CancellationToken) _
    As Task(Of Document)
     ' The class definition represented as source text
     Dim newImplementation = "Class RelayCommand(Of T)
   Implements ICommand
   Private ReadOnly _execute As Action(Of T)
   Private ReadOnly _canExecute As Predicate(Of T)
   Public Sub New(ByVal execute As Action(Of T))
     Me.New(execute, Nothing)
   End Sub
   Public Sub New(ByVal execute As Action(Of T), ByVal canExecute As Predicate(Of T))
     If execute Is Nothing Then
       Throw New ArgumentNullException(""execute"")
     End If
     _execute = execute
     _canExecute = canExecute
   End Sub
   <DebuggerStepThrough> _
   Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
     Return If(_canExecute Is Nothing, True, _canExecute(CType(parameter, T)))
   End Function
   Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
   Public Sub RaiseCanExecuteChanged()
     RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
   End Sub
   Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
     _execute(CType(parameter, T))
   End Sub
 End Class"
   ' 1. ParseSyntaxTree() gets a new SyntaxTree from the source text
   ' 2. GetRoot() gets the root node of the tree
   ' 3. OfType(Of ClassDeclarationSyntax)().FirstOrDefault()
   '    retrieves the only class definition in the tree
   ' 4. WithAdditionalAnnotations() Is invoked for code formatting
   Dim newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
   GetRoot().DescendantNodes().
     OfType(Of ClassBlockSyntax)().
     FirstOrDefault().
     WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation)
   Dim parentBlock = CType(classDeclaration.Parent, ClassBlockSyntax)
   Dim root = Await document.GetSyntaxRootAsync(cancellationToken)
   ' Generate a new CompilationUnitSyntax (which represents a code file)
   ' replacing the old class with the new one
   Dim newRoot As CompilationUnitSyntax =
     root.ReplaceNode(parentBlock, newClassNode)
   'Detect if an Imports System.Windows.Input directive already exists
   If Not newRoot.Imports.Any(Function(i) i.ToFullString.
     Contains("System.Windows.Input")) Then
       'If not, add one
       Dim newImp = SyntaxFactory.
         ImportsStatement(SyntaxFactory.
         SingletonSeparatedList(Of ImportsClauseSyntax) _
         (SyntaxFactory.
         SimpleImportsClause(SyntaxFactory.
         ParseName("System.Windows.Input"))))
       newRoot = newRoot.AddImports(newImp)
     End If
     Dim newDocument = document.WithSyntaxRoot(newRoot)
     Return newDocument
   End Function
 End Class

Sie haben erfolgreich zwei benutzerdefinierte Refactorings abgeschlossen, und Sie verfügen nun abhängig von der Implementierung des MVVM-Musters (z. B. ein Nachrichtenbroker, ein Dienstlocator und Dienstklassen) über die Grundlagen zum Implementieren weiterer Refactorings.

Als Alternative können Sie auch die SyntaxGenerator-Klasse verwenden. Diese bietet sprachagnostische APIs. Dies bedeutet, dass der Code, den Sie schreiben, zu einem Refactoring für Visual Basic und C# führt. Bei diesem Ansatz ist es jedoch erforderlich, jedes einzelne Syntaxelement für den Quelltext zu generieren. Wenn Sie „SyntaxFactory.ParseSyntaxTree“ verwenden, können Sie jeden beliebigen Quelltext analysieren. Dies ist insbesondere dann hilfreich, wenn Sie Entwicklertools schreiben, die Quelltext manipulieren müssen, den Sie nicht im Voraus kennen.

Verfügbarkeit: UWP-Apps und WPF

Sie sollten benutzerdefinierte Refactorings nicht universell zur Verfügung stellen. Es ist sinnvoll, ihre Verfügbarkeit in der Glühbirne auf die Plattformen einzuschränken, mit denen MVVM tatsächlich verwendet wird, z. B. auf WPF und UWP. In der ComputeRefactoringsAsync-Methode können Sie eine Instanz des Semantikmodells für das aktuelle Dokument abrufen und dann die GetTypeByMetadataName-Methode aus der Compilation-Eigenschaft aufrufen. Der folgende Code zeigt z. B., wie das Refactoring nur für UWP-Apps zur Verfügung gestellt wird:

// Restrict refactoring availability to Windows 10 apps only
var semanticModel = await context.Document.GetSemanticModelAsync();
var properPlatform = semanticModel.Compilation.
  GetTypeByMetadataName("Windows.UI.Xaml.AdaptiveTrigger");
if (properPlatform != null)
{
  var root = await context.Document.
    GetSyntaxRootAsync(context.CancellationToken).
    ConfigureAwait(false);
  // ...
}

Da der Typ „Windows.UI.Xaml.AdaptiveTrigger“ nur in UWP-Apps vorhanden ist, ist das Refactoring verfügbar, wenn das Codeanalysemodul erkennt, dass ein Verweis auf diesen Typ vorliegt. Wenn Sie das Refactoring für WPF bereitstellen möchten, können Sie die folgende Überprüfung schreiben:

// Restrict refactoring availability to WPF apps only
var semanticModel = await context.Document.GetSemanticModelAsync();
var properPlatform = semanticModel.Compilation.
  GetTypeByMetadataName("System.Windows.Navigation.JournalEntry");

Analog dazu ist „System.Windows.Navigation.JournalEntry“ nur in WPF vorhanden. Ein Nicht-NULL-Ergebnis von „GetTypeByMetadataName“ bedeutet daher, dass die Codeanalyse für ein WPF-Projekt ausgeführt wird. Sie können natürlich auch beide Überprüfungen kombinieren, um die Refactorings für beide Plattformen bereitzustellen.

Testen des Codes

Sie können Ihren bisher erstellten Code in der experimentellen Visual Studio-Instanz testen, indem Sie F5 drücken. Erstellen Sie z. B. ein WPF-Projekt, und fügen Sie die folgende sehr einfache Klasse hinzu:

class EmptyClass
{
  string aProperty { get; set; }
}

Klicken Sie mit der rechten Maustaste auf die Klassendeklaration, und wählen Sie dann „Schnellaktionen“ aus dem Kontextmenü aus. An diesem Punkt zeigt die Glühbirne wie erwartet die beiden neuen Refactorings an und stellt den richtigen Vorschlag bereit (siehe Abbildung 3 mit Details).

Wenn Sie benutzerdefinierte Refactorings veröffentlichen möchten, automatisiert die Coderefactoring-Projektvorlage (VSIX-Projektvorlage) die Generierung eines VSIX-Pakets, das in der Visual Studio Gallery veröffentlicht werden kann. Wenn Sie Ihren Code lieber als NuGet-Paket veröffentlichen möchten, besteht der Trick im Erstellen eines Analyzers mit dem Code Fix-Projekt und dem anschließenden Hinzufügen von Code Fix-Elementvorlagen.

Generieren eines benutzerdefinierten „ViewModels“ mit Roslyn

Wenn Sie sich fragen, warum die Verwendung von Roslyn dem Hinzufügen statischen Texts zu einer Klasse vorzuziehen ist, stellen Sie sich vor, dass Sie die Generierung eines „ViewModels“ aus einer Geschäftsklasse automatisieren möchten, die das Modell darstellt. In diesem Fall können Sie eine neue ViewModel-Klasse generieren und die erforderlichen Eigenschaften basierend auf den vom Modell bereitgestellten Daten hinzufügen. Abbildung 9 zeigt, wie ein Refactoring namens „Make ViewModel class“ generiert wird, das zeigt, wie eine vereinfachte Version eines „ViewModels“ erstellt wird.

Abbildung 9: Generieren eines „ViewModels“ basierend auf einem Modell

private async Task<Document> MakeViewModelAsync(Document document,
  ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
  // Get the name of the model class
  var modelClassName = classDeclaration.Identifier.Text;
  // The name of the ViewModel class
  var viewModelClassName = $"{modelClassName}ViewModel";
  // Only for demo purposes, pluralizing an object is done by
  // simply adding the "s" letter. Consider proper algorithms
  string newImplementation = $@"class {viewModelClassName} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler PropertyChanged;
// Raise a property change notification
protected virtual void OnPropertyChanged(string propname)
{{
  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}}
private ObservableCollection<{modelClassName}> _{modelClassName}s;
public ObservableCollection<{modelClassName}> {modelClassName}s
{{
  get {{ return _{modelClassName}s; }}
  set
  {{
    _{modelClassName}s = value;
    OnPropertyChanged(nameof({modelClassName}s));
  }}
}}
public {viewModelClassName}() {{
// Implement your logic to load a collection of items
}}
}}
";
    var newClassNode = SyntaxFactory.ParseSyntaxTree(newImplementation).
      GetRoot().DescendantNodes().
      OfType<ClassDeclarationSyntax>().
      FirstOrDefault().
      WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
    // Retrieve the parent namespace declaration
    var parentNamespace = (NamespaceDeclarationSyntax) classDeclaration.Parent;
    //Add the new class to the namespace
    var newParentNamespace =
      parentNamespace.AddMembers(newClassNode).NormalizeWhitespace();
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    CompilationUnitSyntax newRoot = (CompilationUnitSyntax)root;
    newRoot = newRoot.
              ReplaceNode(parentNamespace, newParentNamespace).NormalizeWhitespace();
    newRoot = newRoot.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
        SyntaxFactory.IdentifierName("Collections.ObjectModel"))),
        SyntaxFactory.UsingDirective(SyntaxFactory.
      QualifiedName(SyntaxFactory.IdentifierName("System"),
        SyntaxFactory.IdentifierName("ComponentModel"))));
    // Generate a new document based on the new SyntaxNode
    var newDocument = document.WithSyntaxRoot(newRoot);
    // Return the new document
    return newDocument;
  }

Dieser Code generiert eine ViewModel-Klasse, die eine „ObservableCollection“ des Typs „model“ sowie einen leeren Konstruktor bereitstellt, in dem Sie Ihre Programmlogik implementieren sollten. Abbildung 10 zeigt dies. Dieser Code sollte natürlich durch zusätzliche Member erweitert werden, die Sie ggf. benötigen (z. B. Dateneigenschaften und Befehle), und er sollte durch einen effizienteren Pluralisierungsalgorithmus verbessert werden.

Automatisieren der Generierung einer ViewModel-Klasse
Abbildung 10: Automatisieren der Generierung einer ViewModel-Klasse

Lernen Sie von Microsoft: Das INotifyPropertyChanged-Refactoring

Eine der wiederkehrenden Aufgaben, die Sie mit MVVM ausführen, ist die Implementierung von Änderungsbenachrichtigungen für Klassen in Ihrem Datenmodell über die System.ComponentModel.INotifyPropertyChanged-Schnittstelle. Wenn Sie z. B. die folgende Customer-Klasse verwenden:

class Customer 
{
  string CompanyName { get; set; }
}

In diesem Fall sollten Sie „INotifyPropertyChanged“ implementieren, damit gebundene Objekte über Änderungen der Daten benachrichtigt werden:

class Customer : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  string _companyName;
  string CompanyName
  {
    get { return _companyName; }
    set {
      _companyName = value;
      PropertyChanged?.Invoke(this,
        new PropertyChangedEventArgs(nameof(CompanyName))); }
  }
}

Sie können sich sicher vorstellen, dass in einem Datenmodell, das aus zahlreichen Klassen mit Dutzenden von Eigenschaften besteht, diese Aufgabe viel Zeit in Anspruch nehmen kann. Unter den begleitenden Beispielen für Roslyn hat Microsoft ein Coderefactoring bereitgestellt, das die Implementierung der INotifyPropertyChanged-Schnittstelle mit einem einfachen Mausklick automatisiert. Es trägt den Namen „ImplementNotifyPropertyChanged“ und ist für C# und Visual Basic im Unterordner „Src/Samples“ im Roslyn-Repository unter github.com/dotnet/roslyn verfügbar. Wenn Sie das Beispiel kompilieren und testen, werden Sie feststellen, wie schnell und effizient die Implementierung der INotifyPropertyChanged-Schnittstelle erfolgt. Abbildung 11 zeigt dies.

Implementieren der INotifyPropertyChanged-Schnittstelle
Abbildung 11: Implementieren der INotifyPropertyChanged-Schnittstelle

Dieses Beispiel ist besonders nützlich, weil es zeigt, wie die Roslyn-APIs verwendet werden, um eine Objektdefinition zu durchlaufen, bestimmte Member zu analysieren und Bearbeitungen an vorhandenen Eigenschaften vorzunehmen, ohne eine vollständig neue Klassendefinition bereitzustellen. Die Auseinandersetzung mit dem Quellcode dieses Beispiels wird für das Verständnis komplexerer Codegenerierungsszenarien unbedingt empfohlen.

Zusammenfassung

Die denkbaren Verwendungsmöglichkeiten von Roslyn sind beinahe unendlich. Abgesehen davon wird es durch Roslyn unglaublich einfach, das Model-View-ViewModel-Muster zu unterstützen. Wie ich in diesem Artikel gezeigt habe, können Sie die Roslyn-APIs zum Analysieren des Quellcodes bestimmter Klassen nutzen, die in jeder MVVM-Implementierung erforderlich sind (z. B. „ViewModelBase“ und „RelayCommand<T>“), und einen neuen Syntaxknoten generieren, der eine vorhandene Klassendefinition ersetzen kann. Visual Studio 2015 wird außerdem eine Vorschau in der Glühbirne enthalten und auf diese Weise eine weitere verblüffende Codierungserfahrung ermöglichen.


Alessandro del Soleist seit 2008 ein Microsoft MVP. Er wurde bereits fünf Mal als MVP des Jahres ausgezeichnet und ist der Autor zahlreicher Bücher, eBooks, Videoanleitungen und Artikel zur .NET-Entwicklung mit Visual Studio. Del Sole arbeitet als Solution Developer Expert für Brain-Sys mit dem Schwerpunkt .NET-Entwicklung, Training und Consulting. Sie können ihm auf Twitter folgen: @progalex.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Jason Bock (Magenic) und Anthony Green (Microsoft)

Jason Bock arbeitet als Practice Lead für Magenic (magenic.com), ist Autor und hält Vorträge. Besuchen Sie seine Website unter jasonbock.net.