May 2016

Volume 31 Number 5

.NET コンパイラ プラットフォーム - Roslyn でモデル - ビュー - ビューモデルを使いやすくする

Alessandro Del Del

モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) はよく使われるアーキテクチャ パターンで、Windows Presentation Foundation (WPF) やユニバーサル Windows プラットフォーム (UWP) などの XAML アプリケーション プラットフォームとは完ぺきに連携します。MVVM を使用してアプリケーションを設計するメリットはたくさんありますが、中でも特筆すべきメリットは、データ、アプリケーション ロジック、UI を明確に分離できることです。このような分離は、アプリケーションのメンテナンスやテストを容易にするだけでなく、コードの再利用性を高め、デザイナーがロジックやデータを考えずに UI を設計できるようにします。長い年月を経て、数多くのライブラリ、プロジェクト テンプレート、Prism や MVVM Light Toolkit などのフレームワークが誕生し、MVVM は今までよりも簡単かつ効率的に実装できるようにはなりましたが、それでも、場合によっては、外部ライブラリを使えなかったり、パターンをすばやく実装したのに、コードに時間をかけなければならなかったりすることもあります。MVVM の実装はさまざまですが、ほとんどが多くの共通オブジェクトを共有します。こうした共通オブジェクトは Roslyn API を使って簡単に自動生成することができます。今回は、あらゆる MVVM 実装の共通の要素を簡単に生成できるように、Roslyn のカスタム リファクタリングを作成する方法について説明します。ここで MVVM の概要を完全にまとめるのは不可能なので、MVVM パターン、関連用語、Roslyn コード分析 API についての基礎知識があることを前提に話を進めます。復習が必要な方は、「パターン: Model-View-ViewModel デザイン パターンによる WPF アプリケーション」、「C# と Visual Basic: Roslyn を使用した API 向けライブ コード アナライザーの作成」、および「C#: Roslyn アナライザーへのコード修正の追加」 を参照してください。

サンプル コードは C# と Visual Basic の 2 つのバージョンを用意しました。本稿では両方のコードを併記します。

MVVM の共通クラス

典型的な MVVM 実装では、少なくとも以下のクラスが必要になります (MVVM の種類によって、若干名前が異なる場合もあります)。

ViewModelBase: アプリケーションのすべてのビューモデルに共通のメンバーを公開する、基本抽象クラス。共通メンバーはアプリケーションのアーキテクチャによって異なる場合がありますが、最も基本的な実装では、すべての派生ビューモデルに変更通知を行います。

RelayCommand: ビューモデルがメソッドを呼び出す際に使用するコマンドを表すクラス。RelayCommand には通常、ジェネリックとジェネリック以外の 2 種類がありますが、今回はジェネリックを使用します (RelayCommand<T>)。

この 2 つのクラスの知識があることを前提とし、詳しい説明は行いません。図 1a は ViewModelBase の C# コードを、図 1b は Visual Basic コードを示します。

図 1a ViewModelBase クラス (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));
  }
}

図 1b ViewModelBase クラス (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

これは ViewModelBase の最も基本的な実装です。INotifyPropertyChanged インターフェイスに基づいて、プロパティの変更通知を提供しているだけです。もちろん、必要に応じてメンバーを追加することもできます。図 2a では RelayCommand<T> の C# コードを、図 2b では Visual Basic コードを示します。

図 2a RelayCommand<T> クラス (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);
  }
}

図 2b RelayCommand(Of T) クラス (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

これは RelayCommand<T> の最も一般的な実装で、ほとんどの MVVM シナリオに適しています。ここで重要なのは、このクラスが、CanExecute というメソッドの実装を求める System.Windows.Input.ICommand インターフェイスを実装することです。CanExecute の目的は、コマンドが実行可能かどうかを呼び出し元に通知することです。

Roslyn が便利な理由

外部フレームワークを使用しない場合、Roslyn があれば非常に便利です。カスタムのコード リファクタリングを作成して、クラス定義を置き換えて必要なオブジェクトを自動的に実装することができます。また、モデル プロパティに基づいてビューモデル クラスの生成を簡単に自動化できます。図 3 に、本稿の最終結果の例を示します。

Roslyn のカスタム リファクタリングによる MVVM オブジェクトの実装
図 3 Roslyn のカスタム リファクタリングによる MVVM オブジェクトの実装

このアプローチのメリットは、コード エディターから目を離すことなく、必要なオブジェクトを非常にすばやく実装できる点です。さらに、後ほど扱いますが、モデル クラスに基づいてカスタム ビューモデルを生成することもできます。では、リファクタリング プロジェクトを作成してみましょう。

Roslyn のリファクタリング プロジェクトの作成

最初に、Roslyn のリファクタリングを新規作成します。このためには、[新しいプロジェクト] ダイアログ ボックスで、選択した言語下の [機能拡張] ノードにある、[Code Refactoring (VSIX)] プロジェクト テンプレートを使用します。新しいプロジェクトに「MVVM_Refactoring」という名前を付けます (図 4 参照)。

Roslyn のリファクタリング プロジェクトの作成
図 4 Roslyn のリファクタリング プロジェクトの作成

準備ができたら [OK] をクリックします。Visual Studio 2015 でプロジェクトが生成されると、MVVMRefactoringCodeRefactoringProvider というクラスが自動的に追加され、CodeRefactoringProvider.cs (Visual Basic の場合拡張子が .vb) ファイル内で定義されます。クラス名を「MakeViewModelBaseRefactoring」、ファイル名を「MakeViewModelBaseRefactoring.cs」に変更します。わかりやすくなるように、自動生成された ComputeRefactoringsAsync メソッドと ReverseTypeNameAsync メソッドを削除します (後者はデモ目的で自動生成されます)。

構文ノードの調査

コード リファクタリングのメイン エントリ ポイントは ComputeRefactoringsAsync メソッドです。このメソッドは、構文ノードのコード分析が必要な規則を満たしている場合に、コード エディターの電球メニューに接続される「クイック操作」を作成する役割があります。今回の例では、ComputeRefactoringsAsync メソッドによって、開発者がクラス宣言で電球メニューを呼び出しているかどうかを検出する必要があります。どの構文要素を操作すればよいかは、Syntax Visualizer ツール ウィンドウを見れば一目瞭然です。具体的には、C# では構文ノードが ClassDeclaration かどうかを検出しなければならず、これは Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax 型のオブジェクトで表されます (図 5参照)。Visual Basic では、構文ノードが ClassStatement かどうかを判断しますが、これは Microsoft.CodeAnalysis.VisualBasic.Syntax.ClassStatementSyntax 型のオブジェクトで表されます。実のところ Visual Basic では、ClassStatement は ClassBlock の子ノードで、クラスのコード全体を表します。C# と Visual Basic のオブジェクトが異なる理由は、それぞれがクラス定義を表す方法にあります。C# は中かっこを区切り文字として class キーワードを使用するのに対し、Visual Basic は End Class ステートメントを区切り文字として Class キーワードを使用します。

クラス宣言について
図 5 クラス宣言について

操作の作成

最初に扱うコード リファクタリングは、ViewModelBase クラスと関連しています。まず、MakeViewModelBaseRefactoring クラスに ComputeRefactoringsAsync メソッドを記述します。このメソッドで、構文ノードがクラス宣言を表しているかどうかを確認します。表していれば、電球メニューに表示される操作を作成して登録できます。図 6a では、これを C# で実行する方法を示しています。図 6b には Visual Basic コードを示します (コメントを参照してください)。

図 6a メイン エントリ ポイント: ComputeRefactoringsAsync メソッド (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);
}

図 6b メイン エントリ ポイント: ComputeRefactoringsAsync メソッド (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

このコードで、構文ノードがクラス宣言の場合に呼び出すことができる操作を登録します。この操作は、リファクタリングのロジックを実装して新しいクラスを提供する MakeViewModelBaseAsync メソッドによって実行されます。

コードの生成

Roslyn を使用すると、オブジェクト指向の構造化された方法でソース コードを表せるようになるだけでなく、ソース テキストを解析して構文を正確に生成することもできます。純粋なテキストから新しい構文ツリーを生成するには、SyntaxFactory.ParseSyntaxTree メソッドを呼び出します。これは、SyntaxTree 生成元のソース コードを含む、System.String 型の引数を取ります。

VisualBasicSyntaxTree.ParseText メソッドと CSharpSyntaxTree.ParseText メソッドを使用しても同じ成果を達成できますが、今回のケースでは、コードは SyntaxFactory から別の Parse メソッドを呼び出すため、SyntaxFactory.ParseSyntaxTree を使用するのが適切です。

新しい SyntaxTree インスタンスが作成されたら、そこでコード分析などのコード関連の操作を実行できます。たとえば、クラス全体のソース コードを解析して、そこから構文ツリーを生成し、クラスの構文ノードを置き換えて新しいクラスを返すことができます。MVVM パターンの場合、共通のクラスには固定の構造があるため、ソース テキストを解析してクラス定義を新しい定義に置き換えるプロセスはすばやく簡単に実行できます。いわゆる複数行の文字列リテラルを活用すれば、クラス定義全体を System.String 型のオブジェクトに貼り付けて、そこから SyntaxTree を取得後、クラス定義に対応する SyntaxNode を取得してツリー内の元のクラスを新しいクラスに置き換えることができます。まず、これを ViewModelBase クラスで実現する方法を示します。図 7a に C# のコードを、図 7b に Visual Basic のコードを示します。

図 7a MakeViewModelBaseAsync: ソース テキストから新しい構文ツリーを生成 (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;
}

図 7b MakeViewModelBaseAsync: ソース テキストから新しい構文ツリーを生成 (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

SyntaxFactory 型は何度も使うので、静的インポートを実行してもかまいません。したがって、Visual Basic では Imports Microsoft.CodeAnalisys.VisualBasic.SyntaxFactory ディレクティブ、C# では using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory ディレクティブを追加して、コードをシンプルにします。SyntaxFactory が提供するメソッドの一部が見つけやすくなるように、ここでは静的なインポートを使用していません。

MakeViewModelBaseAsync メソッドは、以下 3 つの引数を受け取ります。

  • 現在のソース コード ファイルを表す Document
  • 実行されるコード分析についてのクラス宣言を表す ClassDeclarationSyntax (Visual Basic では ClassStatementSyntax)
  • 操作をキャンセルする必要がある場合に使用する CancellationToken

このコードはまず SyntaxFactory.ParseSyntaxTree を呼び出して、ViewModelBase クラスを表すソース テキストに基づいて SyntaxTree の新しいインスタンスを取得します。GetRoot は、構文ツリーのルート SyntaxNode インスタンスを取得するために呼び出す必要があります。このシナリオでは、解析後のソース テキストにクラス定義が 1 つしかないことが事前にわかっているので、コードは OfType<T> で FirstOrDefault<T> を呼び出して、必要な型 (C# では ClassDeclarationSyntax、Visual Basic では ClassBlockSyntax) の先祖ノードを 1 つ取得します。この時点で、元のクラス定義を ViewModelBase クラスで置き換える必要があります。そのために、コードはまず Document.GetSyntaxRootAsync を呼び出して、ドキュメントの構文ツリーのルート ノードを非同期に取得します。その後 ReplaceNode を呼び出して、以前のクラス定義を新しい ViewModelBase クラスに置き換えます。コードは System.ComponentModel 名前空間に using ディレクティブ (C#) または Imports ディレクティブ (Visual Basic) が存在しているかどうかを検出するために、CompilationUnitSyntax.Usings コレクション (C#)、または CompilationUnitSyntax.Imports コレクション (Visual Basic) を調査します。存在していない場合は、適切なディレクティブを追加します。これは、ディレクティブがない場合にコード ファイル レベルで追加するのに便利です。

Roslyn ではオブジェクトは変更不可です。これは String クラスと同じ考え方で、文字列は絶対に変更されないため、文字列を編集したり、Replace、Trim、Substring などのメソッドを呼び出したりすると、指定された変更を加えた新しい文字列が生成されます。このため、構文ノードを編集する必要がある場合は必ず、更新したプロパティを持つ新しい構文ノードを作成します。

Visual Basic のコードでは、ClassStatementSyntax 型ではなく、現在の構文ノードの親 ClassBlockSyntax を取得することも必要です。これは、実際に置換される SyntaxNode のインスタンスを取得するために必要です。RelayCommand<T> クラスの一般的な実装を提供するとまったく同じように動作しますが、新しいコード リファクタリングを追加する必要があります。このためにはソリューション エクスプローラーでプロジェクト名を右クリックし、[追加] をポイントして [新しい項目] をクリックします。[新しい項目の追加] ダイアログ ボックスで Refactoring テンプレートを選択して、新しいファイルに「MakeRelayCommandRefactoring.cs」(Visual Basic の場合は拡張子が .vb) という名前を付けます。リファクタリングのロジックは ViewModelBase クラスと同じです (もちろん、ソース テキストは異なります)。図 8a に、ComputeRefactoringsAsync メソッドと MakeRelayCommandAsync メソッドが含まれる新しいリファクタリングを実行している、完全な C# コードを示します。図 8b は Visual Basic コードです。

図 8a RelayCommand<T> クラスを実装するコード リファクタリング (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;
  }
}

図 8b RelayCommand(Of T) クラスを実装するコード リファクタリング (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

これで、カスタム リファクタリングが 2 つ正しく完成しました。自身の MVVM パターン (メッセージ ブローカー、サービス ロケーター、サービス クラスなど) の実装に基づいて、追加のリファクタリングを実装する基礎知識も身に付いたことでしょう。

この方法とは別に、SyntaxGenerator クラスを使用することもできます。これにより、言語に依存しない API が提供されるため、Visual Basic と C# の両方を対象とするリファクタリングを作成できます。ただし、このアプローチでは、ソース テキストの構文要素を 1 つずつすべて生成する必要があります。SyntaxFactory.ParseSyntaxTree を使用すると、任意のソース テキストを解析できます。これは、前もってわからないソース テキストを操作しなければならないデベロッパー ツールを作成する場合に特に便利です。

UWP アプリと WPF との対応付け

カスタム リファクタリングを広範に使えるようにするよりも、WPF や UWP など、MVVM を実際に使用するプラットフォームのみで電球メニューに表示されるようにする方が現実的です。ComputeRefactoringsAsync メソッドでは、現在のドキュメントのセマンティック モデルのインスタンスを取得して、Compilation プロパティから GetTypeByMetadataName メソッドを呼び出すことができます。たとえば、以下のコードは、UWP アプリのみでリファクタリングを使用できるようにする方法を示しています。

// 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);
  // ...
}

Windows.UI.Xaml.AdaptiveTrigger 型は UWP アプリのみに存在しているため、コード分析エンジンがこの型が参照されていることを検出すると、リファクタリングが使用可能になります。リファクタリングを WPF で使用できるようにするには、次のチェックを記述します。

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

同様に、System.Windows.Navigation.JournalEntry は WPF に一意に存在するため、GetTypeByMetadataName から null 以外の結果が返された場合、コード分析を WPF プロジェクトに対して実行していることになります。もちろん、両方のチェックを組み合わせて、そのリファクタリングを 2 つのプラットフォームで利用できるようにすることも可能です。

コードのテスト

F5 キーを押せば、ここまでの内容を Visual Studio の実験的なインスタンスでテストすることができます。たとえば、WPF プロジェクトを作成して、以下の非常に簡単なクラスを追加するとします。

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

クラス宣言を右クリックし、コンテキスト メニューで [クイック アクション] をクリックします。この時点で、電球メニューは期待どおりに 2 つの新しいリファクタリングを表示して、適切な推測を提供します (図 3 参照)。

カスタム リファクタリングを発行する場合、Code Refactoring (VSIX) プロジェクト テンプレートは、Visual Studio ギャラリーに発行できる VSIX パッケージの生成を自動化します。作業を NuGet パッケージとして発行する場合、鍵となるのは、Analyzer with the Code Fix プロジェクトを作成して Code Fix 項目テンプレートを追加することです。

Roslyn によるカスタム ビューモデルの生成

静的テキストをクラスに追加するためだけに Roslyn を使用するのが、なぜ最適なアプローチなのか疑問に思われるかもしれません。そこで、ビジネス クラスがモデルで、そこからのビューモデルの生成を自動化するとします。このような場合、新しいビューモデル クラスを生成して、モデルが公開するデータに基づいて必要なプロパティを追加します。わかりやすいように、MakeViewModel クラスというリファクタリングを生成する方法を図 9 に示します。ここでは、ビューモデルを単純化したバージョンを作成する方法を示しています。

図 9 モデルに基づいてビューモデルを生成

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;
  }

このコードは、model 型の ObservableCollection を公開するビューモデル クラスを生成するとともに、ロジックの実装対象となる空のコンストラクターを生成します (図 10 参照)。もちろんこのコードは、データのプロパティやコマンドなど、必要なメンバーを追加して拡張したり、より効率的な複数形化アルゴリズムに改善する必要があります。

ビューモデル クラス生成の自動化
図 10 ビューモデル クラス生成の自動化

マイクロソフトの INotifyPropertyChanged リファクタリングについて

MVVM で繰り返し行う作業の 1 つに、System.ComponentModel.INotifyPropertyChanged インターフェイスを経由してデータ モデルのクラスに変更通知を実装するというものがあります。たとえば、以下の Customer クラスがあるとします。

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

バインド オブジェクトがデータの変更を通知されるように、INotifyPropertyChanged を以下のように実装する必要があります。

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

多数のプロパティを持つ多くのクラスで構成されるデータ モデルでは、この作業に時間がかかることがあります。マイクロソフトは、Roslyn 用のガイド サンプルに、単純なクリック操作で INotifyPropertyChanged インターフェイスの実装を自動化するコード リファクタリングを含めています。github.com/dotnet/roslyn (英語) の Roslyn リポジトリの Src/Samples サブフォルダーに、ImplementNotifyPropertyChanged という名前で C# 用と Visual Basic 用のリファクタリングがあります。例をコンパイルしてテストすれば、INotifyPropertyChanged インターフェイスの実装がどれだけすばやく効率的になるか、おわかりいただけるはずです (図 11 参照)。

INotifyPropertyChanged インターフェイスの実装
図 11 INotifyPropertyChanged インターフェイスの実装

この例は、Roslyn API を使用してオブジェクト定義を確認したり、特定のメンバーを解析したり、まったく新しいクラス定義を提供することなく既存のプロパティを編集したりする方法を示しているので、特に便利です。より複雑なコード生成シナリオを理解するために、この例のソース コードを学習することを強くお勧めします。

まとめ

Roslyn の用途はほぼ無限大ですが、MVVM パターンのサポートを驚くほど簡単にします。今回と取り上げたように、Roslyn API を活用すれば、あらゆる MVVM 実装に必要な特定のクラス (ViewModelBase や RelayCommand<T> など) のソース コードを解析し、既存のクラス定義に代わる新しい構文ノードを生成することが可能です。そして、Visual Studio 2015 では電球メニューにプレビューを表示するというのも、コードをたいへん記述しやすくする要素の 1 つです。


Alessandro del Sole は 2008 年から Microsoft MVP の一員です。彼は年間 MVP を 5 度受賞し、Visual Studio による .NET 開発に関する、書籍、電子ブック、説明ビデオ、記事を手がけてきました。彼は、Brain-Sys (brain-sys.it、英語) でソリューション デベロッパー エキスパートとして活躍し、.NET 開発、トレーニング、コンサルティングに特化しています。Twitter は、@progalex (英語) からフォローできます。

この記事のレビューに協力してくれた技術スタッフの Jason Bock (Magenic) と Anthony Green (マイクロソフト) に心より感謝いたします。

Jason Bock は、Magenic (magenic.com、英語) のプラクティス リードで、執筆者兼講演者でもあります。彼のサイトは jasonbock.net (英語) です。