2011年11月22日火曜日

IExpressionEditorService と IEXpressionEditorInstance でインテリセンスの導入

ある程度 WF の ExpressionTextBox で式や値の入力中にインテリセンスを利用するように実装できてきたので、その利用方法や注意点をまとめてみます。

まず WF での ExpressionTextBox におけるインテリセンスサポートですが、Visual Studio 上でワークフローを触られている場合は Visual Studio が提供するインテリセンスが自動で利用されています。しかし再ホストを行っているケースでは、自前でインテリセンス部分を用意しなくてはいけません。海外 MSDN だと VIsual Studio のアドイン Dll をそのまま利用してしまえという豪快な方法を目にすることがありますが、配布が出来ませんので自分一人の環境で利用する場合のみ許される方式です。

ソースの全体はかなり大きいので、CodePlex にアップしている WF Designer Express のアップデート(近日中に実施します)を参考にしてください。

WF では編集時のコントロールを制御するための仕組みとして IExpressionEditorInstance インターフェースが、そしてそれをアプリ上でコントロールするための IExpressionEditorService インターフェースが用意されています。これらインターフェースにより多くのメソッドやプロパティを実装しなくてはいけないように思えますが、実際にはそれほど利用する必要はありません。

※使い方がわかっていないメソッドやプロパティがほとんど、とも言えますw

1:IExpressionEditorService の実装

IExpressionEditorService はワークフローデザイナーのサービスとして利用するもので、デザイナに対して発行することにより利用されるようになります。

   1: Dim designer = New WorkflowDesigner()
   2: Dim expEditor As New EditorService() 
   3: designer.Context.Services.Publish(Of IExpressionEditorService)(expEditor)

上記のように発行しますが、その際に必要となるのが IExpressionEditorService を継承して実装されたサービスを制御する処理です。このインターフェースでは大きくまとめると 3 つのメソッドを実装する必要があります。

CreateExpressionEditor メソッドで編集用コントロールを作成、CloseExpressionEditors メソッドでは編集用コントロールの破棄、UpdateContext メソッドは参照設定などが変化した際の状態更新を行います。この CreateExpressionEditor メソッドで編集用コントロールを作成するのですが、ここで作成するものが IExpressionEditorInstance インターフェースを継承したものであり編集用コントロールそのものとは微妙に意味が違います

UpdateContext メソッドは参照設定やインポートした名前空間に変化が発生した際に呼び出されます。恐らくこのメソッドにてインテリセンス用データの更新を行うのでしょうが、このメソッドは非同期でぼこぼこ呼び出されてきますので、無理にここで行う必要はないんじゃないか、とも思っています。

今のところ私が WF Designer Express で利用している方式は、先にインテリセンス用データを生成しておき、IExpressionEditorService のインスタンスを生成する際に引き渡す方法を採用しています。

2:IExpressionEditorInstance の実装

先程も記載しましたが IExpressionEditorInstance を継承したクラスは編集用コントロールそのものではなく、コントロールを制御するためのクラスです。それを踏まえると、継承したクラスのフィールド等で実際の編集用コントロール(TextBox 等)を保持し、そのイベント処理等を行うことになります。

インターフェースを継承すると、大量のメソッドやプロパティ、イベントが生成されますが、全てのものを利用する必要はありません。細かい制御を行いたくなった場合に利用するもの、という認識で構わないと思います。

継承した際に作成されるイベントとして、GotAggregateFocus と LostAggregateFocus があります。この二つのイベントは「編集用コントロールへフォーカスがきた・外れた」際に発生させるものとなります。ここで「等」と書いた理由ですが、インテリセンス等を行う場合には実際に入力する編集用コントロールとは別に、インテリセンス用のポップアップ等が必要になります。IExpressionEditorInstance では実際のコントロールもインテリセンスのポップアップも、同じく編集用コントロールとして扱う必要があるので、このようなイベントとなっています。

これらのイベントをトラップするのは IExpressionEditorService 側です。LostAggregateFocus イベントをトラップした際には、編集処理の終了を意味しますので式や値を反映させる必要があります。例えばこのような感じです。

   1: Private Sub LostFocus(ByVal sender As Object, ByVal e As EventArgs)
   2:     Dim edt = TryCast(sender, TextBox)
   3:     If edt IsNot Nothing Then DesignerView.CommitCommand.Execute(edt.Text)
   4: End Sub

編集されていた値を確定させるのが、DesignerView,CommitCommand フィールドを利用している部分です。このフィールドは静的なものですのでこのような記述になります。

そのあたりを除くと IExpressionEditorInstance では編集コントロールの制御が殆どになるので、それほど戸惑うことはないかと思います。インテリセンスのポップアップの表示や、表示されている候補の絞り込みなどはこのクラス近辺で行うのが適していると思います。

3:インテリセンス用データの作成

インテリセンス用データを作成するのは色々方法があるかと思いますが、私は頭の中でイメージがしやすいこともあり TreeNode 的な形式でのデータ保持を採用しています。

   1: Friend Class TreeNodes
   2:  
   3:     Private _nodes As New List(Of TreeNodes)
   4:  
   5:     Public Property Name As String = ""
   6:     Public Property ItemType As Integer
   7:     Public Property Description As String
   8:     Public Property Parent As TreeNodes
   9:  
  10:     Public ReadOnly Property Nodes As List(Of TreeNodes)
  11:         Get
  12:             Return _nodes
  13:         End Get
  14:     End Property
  15:  
  16: End Class

このようなクラスでインテリセンス用の情報を保持させています。プロパティとして親ノードの情報を持たせていますが、実装方法や利用方法によっては不要です。今回は対象となるクラス等のフルパス取得(名前空間こみ)の際に楽をしたかったので用意しています。

インテリセンス情報の大元となるのは「現在読み込まれているアセンブリ」です。そこから利用できるクラス等を抽出して、保持させることになります。

   1: Dim wfAsm = Reflection.Assembly.GetExecutingAssembly
   2: Dim refAsmList = (From x In wfAsm.GetReferencedAssemblies
   3:                   Select Reflection.Assembly.Load(x)).ToList
   4: Dim typeList = refAsmList.SelectMany(
   5:     Function(a) (From x In a.GetTypes
   6:                Where x.IsPublic AndAlso
   7:                    (x.Namespace IsNot Nothing)
   8:                Select x)).ToList

メインのアセンブリから、参照しているアセンブリを取得、それらから Public な情報を取得しているのが上記ロジックになります。これが基本となる情報になり、続く処理はこの基本情報から、名前空間の抽出、クラスの抽出、プロパティの抽出、メソッドの抽出・・・と続けていく事になります。

実際に編集時で利用するインテリセンスデータとしては、上記のもの+ワークフロー上で定義されている変数や引数になり、これは編集を行うアクティビティがワークフロー上でどこに存在するかによって、スコープが変化します。そのためこれらの情報をインテリセンスに加味するタイミングは、IExpressionEditorService.CreateExpressionEditor メソッドが呼ばれたタイミングになります。

このような感じで実装していくことで、再ホストな WF アプリにおいてもインテリセンスが利用可能になります。今回のように .NET のライブラリをインテリセンスで表示するのではなく、独自の要素に限定して表示させようとするのであれば結構簡単にできると思いますので、まずはそのあたりから試してみるのがいいのではないでしょうか。

ISense

0 件のコメント:

コメントを投稿