┏第39号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ──<メニュー>─────────────────────── ■.NET Tips ・プラグイン機能を持つアプリケーションを作成する - その1 ─────────────────────────────── ─────────────────────────────── ■.NET Tips ─────────────────────────────── ●プラグイン機能を持つアプリケーションを作成する - その1 Adobe PhotoshopやBecky! Internet Mailなどのアプリケーションで は、「プラグイン」(または、「アドイン」、「エクステンション」 等)と呼ばれるプログラムをインストールすることにより、機能を追 加することができるようになっています。ここでは、このようなプラ グイン機能を持ったアプリケーションの作り方を考えます。 [URL]Adobe Photoshop http://www.adobe.co.jp/products/photoshop/main.html [URL]RimArts - Becky! Internet Mail http://www.rimarts.co.jp/becky-j.htm (プラグインが何だか分からないという方は、「アスキー デジタル 用語辞典」や「IT用語辞典 e-Words」等をご覧ください。) [URL]ASCII24 - アスキー デジタル用語辞典 - プラグイン http://yougo.ascii24.com/gh/67/006745.html [URL]IT用語辞典 e-Words : プラグイン 【plug-in】 http://e-words.jp/w/E38397E383A9E382B0E382A4E383B3.html 早速ですが、プラグイン機能の実現のために参考になりそうな記事を 以下にいくつか紹介します。 [URL]Developer Fusion - Writing Plugin-Based Applications http://www.developerfusion.com/show/4371/ [URL]DevSource - Building Plug-ins with C# .NET http://www.devsource.ziffdavis.com/article2/0,1759,1594318,00.asp [URL]SA Developer .Net - PluginFX - An extensible plugin framework in C# http://www.sadeveloper.net/viewarticle.aspx?articleID=143 [URL]The Code Project - Plugin Architecture using C# http://www.codeproject.com/csharp/C__Plugin_Architecture.asp [URL]The Code Project - Plug-ins in C# http://www.codeproject.com/csharp/PluginsInCSharp.asp これらの記事で紹介されている方法は基本的にはほとんど同じで、そ れは、インターフェイスを使用するという方法です。つまり、プラグ インとして必要となる機能をプロパティやメソッド(さらに、イベン トやインデクサ)として持つインターフェイスをあらかじめ用意して おき、このインターフェイスを実装したクラスとしてプラグインを作 成するのです。 ★インターフェイスの作成 理屈は置いておき、実際にプラグインを作成してみましょう。 ここで作成するプラグインは、プラグインの名前を返す機能と、渡さ れた文字列を処理して、結果を文字列として返す機能を有するものと し、それぞれの機能のためにNameプロパティとRunメソッドを持つイ ンターフェイスを作ります。 Visual Studio .NETでは、クラスライブラリのプロジェクトを作成し、 次のようなコードを書き、ビルドします。.NET SDKの場合は、 /target:libraryコンパイラオプションにより、コードライブラリを 作成します。なお、ここで作成されたアセンブリファイル名は、 "Plugin.dll"であるとします。 (Visual Studio .NETのVB.NETの場合は、プロジェクトのプロパティ の「ルート名前空間」が空白になっているものとします。デフォルト ではプロジェクト名となっています。) ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ Imports System Namespace Plugin Public Interface IPlugin ReadOnly Property Name() As String Function Run(ByVal str As String) As String End Interface End Namespace ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ using System; namespace Plugin { public interface IPlugin { string Name {get;} string Run(string str); } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ★プラグインの作成 次に、このインターフェイスを基に、プラグインを作ります。プラグ インもクラスライブラリとして作成します。プラグインは今作成した IPluginインターフェイスを実装する必要がありますので、"Plugin. dll"を参照に追加します。具体的には、Visual Studio .NETの場合は、 ソリューションエクスプローラの「参照設定」に"Plugin.dll"を追加 します。.NET SDKの場合は、/referenceコンパイラオプションを使用 します。 ここでは渡された文字列の文字数を返すプラグインを作成することに します。IPluginインターフェイスを実装した次のようなCountChars クラスを作成します。このクラスライブラリは"CountChars.dll"とい う名前のアセンブリファイルとしてビルドするものとします。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ Imports System Namespace CountChars Public Class CountChars Implements Plugin.IPlugin Public ReadOnly Property Name1() As String _ Implements Plugin.IPlugin.Name Get Return "文字数取得" End Get End Property Public Function Run1(ByVal str As String) As String _ Implements Plugin.IPlugin.Run Return str.Length.ToString() End Function End Class End Namespace ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ using System; namespace CountChars { public class CountChars : Plugin.IPlugin { public string Name { get { return "文字数取得"; } } public string Run(string str) { return str.Length.ToString(); } } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ★プラグインを使うアプリケーションの作成 残るはプラグインを使用するメインアプリケーションの作成です。メ インアプリケーションでは、インストールされている有効なプラグイ ンをどのように探すか、そして、プラグインクラスのインスタンスを どのように作成するかということが問題となります。 まずインストールされているプラグインを探す方法についてです。プ ラグインは必ず".dll"という拡張子を持ち、指定されたフォルダに置 かれていなければならないという約束にしておきます(ここでは、メ インアプリケーションがインストールされているフォルダにある "plugins"フォルダにプラグインを置くものとします)。これにより、 メインアプリケーションはプラグインフォルダにある".dll"の拡張子 を持つファイルのみを調べればよいことになります。さらに、アセン ブリにIPluginインターフェイスを実装したクラスがあるか調べるに は、リフレクションを使用します。 またプラグインクラスのインスタンスを作成するには、Activator. CreateInstanceメソッドなどを使用すればよいでしょう(下のサンプ ルでは、Assembly.CreateInstanceメソッドを使用しています)。 それでは実際に作成してみましょう。ここではメインアプリはコンソー ルアプリケーションとして作成します。また、"Plugin.dll"を参照に 追加します。 まずはプラグインを扱うクラス("PluginInfo")を作成します。このク ラスは、プラグインのアセンブリファイルのパスとクラス名を返すプ ロパティ(それぞれ"Location"と"ClassName")と、有効なプラグイ ンを探すメソッド("FindPlugins")、IPluginのインスタンスを作成 するメソッド("CreateInstance")を持ちます。このクラスのコード を以下に示します。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ Imports System Namespace MainApplication ''' ''' プラグインに関する情報 ''' Public Class PluginInfo Private _location As String Private _className As String ''' ''' PluginInfoクラスのコンストラクタ ''' ''' アセンブリファイルのパス ''' クラスの名前 Private Sub New(ByVal path As String, ByVal cls As String) Me._location = path Me._className = cls End Sub ''' ''' アセンブリファイルのパス ''' Public ReadOnly Property Location() As String Get Return _location End Get End Property ''' ''' クラスの名前 ''' Public ReadOnly Property ClassName() As String Get Return _className End Get End Property ''' ''' 有効なプラグインを探す ''' ''' 有効なプラグインのPluginInfo配列 Public Shared Function FindPlugins() As PluginInfo() Dim plugins As New System.Collections.ArrayList 'IPlugin型の名前 Dim ipluginName As String = _ GetType(Plugin.IPlugin).FullName 'プラグインフォルダ Dim folder As String = _ System.IO.Path.GetDirectoryName( _ System.Reflection.Assembly. _ GetExecutingAssembly().Location) folder += "\plugins" If Not System.IO.Directory.Exists(folder) Then Throw New ApplicationException( _ "プラグインフォルダ""" + folder + _ """が見つかりませんでした。") End If '.dllファイルを探す Dim dlls As String() = _ System.IO.Directory.GetFiles(folder, "*.dll") Dim dll As String For Each dll In dlls Try 'アセンブリとして読み込む Dim asm As System.Reflection.Assembly = _ System.Reflection.Assembly.LoadFrom(dll) Dim t As Type For Each t In asm.GetTypes() 'アセンブリ内のすべての型について、 'プラグインとして有効か調べる If t.IsClass AndAlso t.IsPublic AndAlso _ Not t.IsAbstract AndAlso _ Not (t.GetInterface(ipluginName) _ Is Nothing) Then 'PluginInfoをコレクションに追加する plugins.Add( _ New PluginInfo(dll, t.FullName)) End If Next t Catch End Try Next dll 'コレクションを配列にして返す Return CType(plugins.ToArray( _ GetType(PluginInfo)), PluginInfo()) End Function ''' ''' プラグインクラスのインスタンスを作成する ''' ''' プラグインクラスのインスタンス Public Function CreateInstance() As Plugin.IPlugin Try 'アセンブリを読み込む Dim asm As System.Reflection.Assembly = _ System.Reflection.Assembly.LoadFrom(Me.Location) 'クラス名からインスタンスを作成する Return CType(asm.CreateInstance(Me.ClassName), _ Plugin.IPlugin) Catch End Try End Function End Class End Namespace ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ using System; namespace Plugin { /// /// プラグインに関する情報 /// public class PluginInfo { private string _location; private string _className; /// /// PluginInfoクラスのコンストラクタ /// /// アセンブリファイルのパス /// クラスの名前 private PluginInfo(string path, string cls) { this.Location = path; this.ClassName = cls; } /// /// アセンブリファイルのパス /// public string Location { get {return _location;} } /// /// クラスの名前 /// public string ClassName { get {return _className;} } /// /// 有効なプラグインを探す /// /// 有効なプラグインのPluginInfo配列 public static PluginInfo[] FindPlugins() { System.Collections.ArrayList plugins = new System.Collections.ArrayList(); //IPlugin型の名前 string ipluginName = typeof(Plugin.IPlugin).FullName; //プラグインフォルダ string folder = System.IO.Path.GetDirectoryName( System.Reflection.Assembly .GetExecutingAssembly().Location); folder += "\\plugins"; if (!System.IO.Directory.Exists(folder)) throw new ApplicationException( "プラグインフォルダ\"" + folder + "\"が見つかりませんでした。"); //.dllファイルを探す string[] dlls = System.IO.Directory.GetFiles(folder, "*.dll"); foreach (string dll in dlls) { try { //アセンブリとして読み込む System.Reflection.Assembly asm = System.Reflection.Assembly.LoadFrom(dll); foreach (Type t in asm.GetTypes()) { //アセンブリ内のすべての型について、 //プラグインとして有効か調べる if (t.IsClass && t.IsPublic && !t.IsAbstract && t.GetInterface(ipluginName) != null) { //PluginInfoをコレクションに追加する plugins.Add( new PluginInfo(dll, t.FullName)); } } } catch { } } //コレクションを配列にして返す return (PluginInfo[]) plugins.ToArray(typeof(PluginInfo)); } /// /// プラグインクラスのインスタンスを作成する /// /// プラグインクラスのインスタンス public Plugin.IPlugin CreateInstance() { try { //アセンブリを読み込む System.Reflection.Assembly asm = System.Reflection.Assembly.LoadFrom(this.Location); //クラス名からインスタンスを作成する return (Plugin.IPlugin) asm.CreateInstance(this.ClassName); } catch { return null; } } } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ FindPluginsメソッドが多少複雑ですので、簡単に説明しておきます。 FindPluginsメソッドでは、Assembly.LoadFromメソッドでアセンブリ を読み込んだ後、アセンブリ内のすべての型について、その型がクラ スであり、パブリックであり、抽象クラスでないことを確認し、さら にType.GetInterfaceメソッドにより、IPluginインターフェイスを実 装していることを確認します。これらすべての条件に当てはまった場 合に有効なプラグインと判断します。(よってこの例では、1つのア センブリファイルに複数のプラグインクラスを定義することができま す。) ここまで来たら、後は簡単です。PluginInfoクラスを使ったメインの クラス(エントリポイント)のサンプルは、次のようになります。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ Imports System Namespace MainApplication Public Class PluginHost '/ '/ エントリポイント '/ _ Public Shared Sub Main() 'インストールされているプラグインを調べる Dim pis As PluginInfo() = PluginInfo.FindPlugins() 'すべてのプラグインクラスのインスタンスを作成する Dim plugins(pis.Length - 1) As Plugin.IPlugin Dim i As Integer For i = 0 To plugins.Length - 1 plugins(i) = pis(i).CreateInstance() Next i '有効なプラグインを表示し、使用するプラグインを選ばせる Dim number As Integer = -1 Do For i = 0 To plugins.Length - 1 Console.WriteLine("{0}:{1}", i, plugins(i).Name) Next i Console.WriteLine( _ "使用するプラグインの番号を入力してください。:") Try number = Integer.Parse(Console.ReadLine()) Catch End Try Loop While number < 0 Or number >= pis.Length Console.WriteLine(plugins(number).Name + " を使用します。") 'プラグインに渡す文字列の入力を促す Console.WriteLine("文字列を入力してください。:") Dim str As String = Console.ReadLine() 'プラグインのRunメソッドを呼び出して結果を取得する Dim result As String = plugins(number).Run(str) '結果の表示 Console.WriteLine("結果:") Console.WriteLine(result) Console.ReadLine() End Sub End Class End Namespace ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ using System; namespace MainApplication { public class PluginHost { /// /// エントリポイント /// [STAThread] static void Main(string[] args) { //インストールされているプラグインを調べる PluginInfo[] pis = Plugin.PluginInfo.FindPlugins(); //すべてのプラグインクラスのインスタンスを作成する Plugin.IPlugin[] plugins = new Plugin.IPlugin[pis.Length]; for (int i = 0; i < plugins.Length; i++) plugins[i] = pis[i].CreateInstance(); //有効なプラグインを表示し、使用するプラグインを選ばせる int number = -1; do { for (int i = 0; i < plugins.Length; i++) Console.WriteLine("{0}:{1}", i, plugins[i].Name); Console.WriteLine( "使用するプラグインの番号を入力してください。:"); try { number = int.Parse(Console.ReadLine()); } catch { Console.WriteLine("数字を入力してください。"); } } while (number < 0 || number >= pis.Length); Console.WriteLine(plugins[number].Name + " を使用します。"); //プラグインに渡す文字列の入力を促す Console.WriteLine("文字列を入力してください。:"); string str = Console.ReadLine(); //プラグインのRunメソッドを呼び出して結果を取得する string result = plugins[number].Run(str); //結果の表示 Console.WriteLine("結果:"); Console.WriteLine(result); Console.ReadLine(); } } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ メインアプリ実行前に実行ファイルのあるフォルダに"plugins"とい うフォルダを作り、そこに先ほど作成した"CountChars.dll"をコピー しておいてください。これで、"CountChars.dll"をプラグインとして 認識できます。 基本的なプラグイン機能の実現方法は以上です。しかしより高度なプ ラグイン機能が必要な場合には、これだけでは不十分かもしれません。 例えば、プラグインからメインアプリにフィードバックしたいなど、 プラグインからメインアプリケーションの機能(メソッド)を呼び出 したいケースもあるでしょう。そのような場合には、プラグインを使 う側で実装するインターフェイスを定義するという方法があります(実 は一番初めに紹介した「プラグイン機能の実現のために参考になりそ うな記事」のすべてでこの方法が使われています)。 この方法については、次回、Windowsアプリケーションを例に紹介す る予定です。 =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  http://www.mag2.com/m/0000104516.htm ■発行人・編集人:どぼん!  (Microsoft MVP for Visual Basic, Oct 2003-Oct 2004)  http://dobon.net  dobon_info@yahoo.co.jp ■ご質問等はメールではなく、掲示板へお願いいたします。  http://dobon.net/vb/bbs.html ■上記メールアドレスへのメールは確実に読まれる保障はありません  (スパム、ウィルス対策です)。メールは下記URLのフォームメール  から送信してください。  http://dobon.net/mail.html Copyright (c) 2003 - 2004 DOBON! All rights reserved. ===============================