Mai 2016
Band 31, Nummer 5
.NET Compiler Platform – Optimieren der Model-View-ViewModel-Umgebung mit Roslyn
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.
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.
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.
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.
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.
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.