自作のホットプラグ可能なWPFプラグインシステム(MSF)

自作のホットプラグ可能なWPFプラグインシステム(MSF)

プラグイン化の必要性は主にソフトウェアアーキテクチャの柔軟性の追求に起因し、特に大規模で複雑、または常に更新が必要なソフトウェアシステムを開発する場合に、プラグイン化はソフトウェアシステムの拡張性、カスタマイズ性、独立性、セキュリティ、保守性、モジュール化、アップグレードと更新の容易さ、サードパーティ開発のサポートなどを向上させ、変化し続けるビジネス要件や技術的課題に対応します。

最終更新 2024/05/07 23:15
趋时软件
読了目安 7 分
カテゴリ
WPF
タグ
.NET WPF アーキテクチャ設計 セキュリティ プラグインシステム

プラグイン化のニーズは主にソフトウェアアーキテクチャの柔軟性を追求することに起因しており、特に大規模で複雑、または絶えず更新が必要なソフトウェアシステムを開発する際、プラグイン化によりソフトウェアシステムの拡張性、カスタマイズ性、分離性、セキュリティ、保守性、モジュール化、アップグレードと更新の容易さ、サードパーティ開発のサポートなどの能力を向上させ、変化し続けるビジネス要件や技術的課題に対応できます。

1. プラグイン化の探求

WPF でプラグイン化されたプログラムを開発する場合、通常 2 つの選択肢があります。1 つは MEF、もう 1 つは MAF です。それぞれに長所と短所があるため、以下で分析します。

1.1. MEF(Managed Extensibility Framework)

1.1.1. 利点

  1. 習得が容易: 比較的シンプルで、開発者は簡単な属性マークを使用してコンポーネントを定義およびエクスポートでき、複雑なコードを大量に記述する必要はありません。

  2. 軽量: MEF は軽量フレームワークであり、パフォーマンスのオーバーヘッドは小さくなります。

  3. 低結合性: アプリケーションを複数の独立したプラグインに分割し、各プラグインが特定の機能を実装することで、モジュール間の結合性を低減します。これによりコードの理解と保守が容易になり、あるモジュールの変更が他のモジュールに予期せぬ影響を与えるリスクも低減されます。

  4. 並行開発: MEF を使用すると、異なる開発チームがプラグイン間の依存関係を気にすることなく、並行して異なるプラグインを開発できます。各チームは自身の機能実装に集中でき、他のチームの作業完了を待つ必要がありません。これにより開発効率が大幅に向上します。

  5. テストと保守が容易: 各プラグインは独立したユニットであるため、個別にテストおよび保守できます。これによりテストと保守の複雑さが軽減され、問題発生時の迅速な特定と解決が可能になります。

  6. 新機能の拡張が容易: 新機能を追加する必要がある場合は、新しいプラグインを開発してアプリケーションに追加するだけです。アプリケーション全体を大幅に変更したり再コンパイルしたりする必要がないため、開発サイクルが短縮され、コストが削減されます。

1.1.2. 欠点

  1. プラグインの分離: プラグインの分離をサポートしません。つまり、1 つのプラグインで問題が発生すると、アプリケーション全体に影響を及ぼす可能性があります。また、ホットスワップ(実行中の動的更新)もできません。

  2. ライフサイクル: プラグインのライフサイクル管理をサポートせず、プラグインの開始・停止を細かく制御できません。

1.2. MAF(Managed AddIn Framework)

MAF は MEF プラグインと同様に、低結合性、並行開発、テストと保守の容易さ、新機能の拡張の容易さなどの利点を持ちますが、さらにいくつかの利点があります。

1.2.1. 利点

  1. プラグインの分離: MAF はアプリケーションドメインレベルおよびプロセスレベルのプラグイン分離をサポートします。プラグインの実行に異常が発生してもアプリケーション全体には影響せず、プラグインの更新時にアプリケーション全体を再起動する必要はありません。

  2. ライフサイクル: MAF は完全なライフサイクル管理を提供し、プラグインの開始、停止、アンロードなどの操作を制御できます。

  3. プラグインバージョン: MAF は 1 つのプラグインの複数バージョンを同時に実行できます。この機能によりプラグインの動的ロールバックが可能となり、新しいプラグインに問題が発生した場合、瞬時に古いバージョンに戻せます。

1.2.2. 欠点

  1. 複雑さ: MAF の使用と設定は比較的複雑です。開発者はアプリケーションドメイン、プラグインのアクティブ化、サンドボックス実行などの概念を理解し、プラグインの読み込みとアンロードのプロセスを管理するためのコードを記述する必要があります。

  2. パフォーマンスオーバーヘッド: 各プラグインが独立したアプリケーションドメインで実行されるため、追加のパフォーマンスオーバーヘッドが発生する可能性があります。特に多数のプラグインを読み込む場合や頻繁にプラグインを読み込む場合、アプリケーションのパフォーマンスに影響を与える可能性があります。

1.3 まとめ

比較により、プラグインシステムについて基本的な理解が得られました。プラグインの分離実行の要件がない場合、MEF は優れた選択肢です。比較的シンプルで複雑な理論を理解する必要がなく、サンプルコードを参考にすればすぐにプロジェクトで使用できます。より安全性が高くパフォーマンスの優れたアプリケーションを構築する必要がある場合は、MAF の方が適しています。しかし MAF には大きな問題があります。たとえば、非常に単純な機能を実装する場合でも、固定のプロジェクト構造に従わなければならず、柔軟性に乏しく、使用は極めて複雑で敷居が高いです。アプリケーションがある程度の規模に達すると、プログラムの読み込み速度が問題になる可能性があります。これらの欠点により、実際のプロジェクト開発で MAF を選択する人はほとんどいません。

上記の理由から、MEF と MAF の特徴を融合したプラグインシステムが必要です。軽量なフレームワークでパフォーマンスが良く、使いやすさ、拡張性、安全性、信頼性を備えていることが求められます。これが今回のテーマです。

2. システム設計

2.1. システムアーキテクチャ

画像

2.2. 起動フロー

画像

2.3. 詳細設計

2.3.1. コンテナ

コンテナはプラグインシステムの中核であり、プラグインの検出、プラグインの読み込み、プロセス間通信サービス、異常報告、メッセージ転送、プラグインのライフサイクル管理などのサービスを提供します。

2.3.2. プラグイン起動プログラム

これはコンソールアプリケーションであり、プラグインの実行を担当します。具体的には、プラグイン設定ファイルの読み込み、プラグインの異常情報のコンテナへの報告、プラグインのホットスワップなどの機能を持ちます。

2.3.3. プラグイン

プラグインは dll アセンブリまたは exe プログラムです。このアセンブリまたは exe プログラムは、Plugin 抽象クラスを継承するクラスを 1 つ以上持つ必要があり、これによりコンテナがプラグインを検出する際に認識できるようになります。プラグインクラス内では、独自の UI(任意の FrameworkElement 要素)またはサービスを定義でき、コンテナから呼び出されることが可能です。

3. インスタンス分析

3.1. コンテナの作成と設定

// コンテナを作成
var container = new Container();
// 設定パラメータ
container.Configure(options =>
{
    // プラグインディレクトリ
    options.PluginDirectory = "Plugins";
    // プラグインプロセス起動のタイムアウト時間
    options.PluginProcessTimeout = 6000;
    // 単一プラグインの複数インスタンス起動を許可するか
    options.PluginAllowsMultipleInstances = false;
    // ホットスワップを有効にするか
    options.IsEnableHotSwap = true;
    // コンソールを表示するか
    options.IsShowConsole = false;
});
// プロセス間通信サービスを登録
container.RegisterIpcService<RemotingService>();
// プラグインエラーハンドラ
container.PluginError += Container_PluginError;
// コンテナを起動
container.Run();

3.2. プラグインの実行結果

画像

3.3. 複数プラグインの分離実行

各プラグインは起動後、独立した exe プログラムとして動作し、互いに影響を与えません。

画像

3.4. プラグイン異常

プラグインに異常が発生すると、プラグイン起動プロセスは異常情報をコンテナに報告します。コンテナはプラグインをアンロードし、プラグインを再起動するかどうかの判断をホストプログラムに委ねます。

3.4.1. 手動で例外をスロー

画像

3.4.2. ゼロ除算例外

画像

3.5. プラグインプロセスの予期せぬ終了

コンテナはプラグインの実行状態を常時監視しています。プラグインプロセスが予期せず終了した場合、コンテナはその情報をホストプログラムに報告し、ホストプログラムがプラグインを再起動するかどうかを決定します。

画像

3.6. プラグインのホットスワップ

3.6.1. 実行時に新しいプラグインを検出

デフォルトでは 4 つのプラグインのみ認識されています。別のフォルダからプラグインの dll ファイルをプラグインディレクトリにコピーすると、新しいプラグインが検出されたことがホストプログラムに通知され、ホストプログラムはこのプラグインを読み込むかどうかを決定できます。

画像

3.6.2. 実行時にプラグインを削除

プラグインファイルを削除すると、コンテナは通知を受け取りますが、すぐにプラグインをアンロードするわけではありません。アンロードの判断はホストプログラムに委ねられ、ホストプログラムが削除されたプラグインをアンロードするかどうかを決定します。ホストがアンロードを望まない場合、削除されたプラグインは引き続き実行され、作業は中断されません。

画像

3.6.3. 実行時にプラグインを更新

プラグイン 1 は白い背景、プラグイン 1 の新しいバージョンは赤い背景です。新しいバージョンで古いバージョンを置き換えると、コンテナはホストに通知してプラグインを置き換えるかどうかを問い合わせます。

画像

3.7. プラグイン間通信

プラグイン通信の部分には、メッセージの登録、メッセージの受信、メッセージの送信が含まれます。メッセージの受信と送信は、メッセージタイプのみを気にすればよく、送信者や受信者が誰であるかを意識する必要はありません。登録されたメッセージタイプがあれば、そのタイプのメッセージが発生したときに通知を受け取れます。プラグインはプラグイン間だけでなく、ホストとの通信も可能です。

3.7.1. メッセージの登録

以下のコードは、Notice タイプのメッセージを登録し、登録メソッドに ReceiveMessages というコールバックメソッドを渡し、そのメソッド内でメッセージ受信を処理します。

plugin.ReregisterMessage<Notice>(ReceiveMessages);

3.7.2. メッセージの受信

private void ReceiveMessages(Notice notice){
}

3.7.3. メッセージの送信

plugin.SendMessage(notice);

3.7.4. 効果のデモ

画像

3.8. プラグインの未保存警告

ホストがプラグインを閉じる前に、プラグインの状態に応じて閉じるかどうかを決定できます。未保存の作業がある場合は、ホストに通知してプラグインのクローズをキャンセルできます。

画像

3.9. プラグインで独立した App.config ファイルを使用

各アプリケーションはデフォルトで、アプリケーションファイル名と同じ名前の設定ファイルを 1 つだけ読み込みますが、プラグインは独自のアプリケーション設定ファイルを作成できます。

画像

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="setting1" value="value1" />
    <add key="setting2" value="value2" />
  </appSettings>
</configuration>

実行結果

画像

3.10. プラグインの複数インスタンス起動

単一のプラグインで複数のインスタンスを同時に実行できるかどうかは、コンテナのパラメータで設定できます。

画像

3.11. ホストウィンドウから切り離して実行

画像

3.12. プロセス間通信サービスの拡張

プラグインシステムはデフォルトで RemotingIpcChannel を使用してプロセス間通信を行いますが、拡張を容易にするため、Ipc サービスをコンテナに直接組み込まず、オープンな設計を採用しています。IpcChannel を使用したくない場合は、コンテナ作成後に独自の Ipc サービスを登録できます。

画像

4. プロジェクト実践

以下の例は、プラグインシステムをメニュー、ツールバー、ドキュメントを持つ典型的なソフトウェアに適用したものです。プラグインが読み込まれると、プラグイン内のメニュー、ツールバー、ドキュメントがホストプログラムに読み込まれます。プラグインが予期せず終了したり、明示的に閉じられたりすると、プラグイン内のメニュー、ツールバー、ドキュメントは自動的にアンロードされます。

画像

4.1. メニュー

プラグインには 2 つのコマンドが追加されています。ファイルメニュー下の「開く」メニューと、ビューメニュー下の「ドキュメントビュー」メニューです。メニューをクリックすると、コマンドはプラグインに転送されて実行されます。

private MSFCommand[] CreateCommands()
{
    var openCommand = new MSFCommand(() => MessageBox.Show("メニュー"), () => true)
    {
        Id = Guid.NewGuid().ToString(),
        Name = "開く",
        Type = "Menu",
        Target = "MainWindow",
        Location = "ファイル(_F).開く(_O)",
        Order = 0
    };

    var editorViewCommand = new MSFCommand(() => MessageBox.Show("ドキュメントビュー"))
    {
        Id = Guid.NewGuid().ToString(),
        Name = "ドキュメントビュー",
        Type = "Menu",
        Target = "MainWindow",
        Location = "表示(_V).ドキュメントビュー(_D)",
        Order = 0
    };

    return new MSFCommand[]
    {
        openCommand,
        editorViewCommand
    };
}

4.2. ツールバー

ツールバーの複雑さ(多数の種類のコントロールが追加される可能性)を考慮し、ここではコマンドを使用せず、Button をホストプログラムに渡しています。

internal class CopyButtonWrapper : IWrapper
{
    private PluginContractElement contractElement;

    public CopyButtonWrapper(DocumentViewModel documentViewModel)
    {
        var button = new Button()
        {
            Content = new Image { Width = 16, Height = 16, Source = new BitmapImage(new Uri("pack://application:,,,/EditorPlugin;component/Images/copy.png")) },
            BorderThickness = new System.Windows.Thickness(0),
            BorderBrush = Brushes.Transparent,
            Command = documentViewModel.CopyCommand
        };
        contractElement = new PluginContractElement()
        {
            Id = Guid.NewGuid().ToString(),
            Name = "コピー",
            Type = "ToolBar",
            Order = 2,
            Location = "MainWindow.ToolBar.Copy",
            Description = "コピー",
            UIContract = new NativeHandleContractInsulator(button)
        };
    }

    public PluginContractElement PluginContractElement => contractElement;
}

4.3. ドキュメントビュー

ドキュメントは、UserControl をホストプログラムに渡すことで実現します。

internal class DocumentViewWrapper : IWrapper
{
    private PluginContractElement documentContractElement;

    public DocumentViewWrapper(DocumentView documentView)
    {
        documentContractElement = new PluginContractElement()
        {
            Id = Guid.NewGuid().ToString(),
            Name = "ドキュメント",
            Type="Document",
            Location = "MainWindow.Document",
            Description = "これはドキュメントです",
            UIContract = new NativeHandleContractInsulator(documentView)
        };
    }

    public PluginContractElement PluginContractElement => documentContractElement;
}

4.4. 依存性注入

実際のプロジェクトでは、依存性注入機能を提供する Prism などのフレームワークを使用することが多いため、設計時には互換性を十分に考慮しています。ホストでもプラグインでも、Prism などのフレームワークを使用できます。

public class EditorPlugin : PluginBase
{
    private readonly DryIoc.Container container;
    private readonly PluginContractElement[] _elements;
    private readonly IMSFCommand[] _commands;
    public EditorPlugin()
    {
        container = new DryIoc.Container();

        RegisterTypes();
        RegisterInstances();

        _commands = CreateCommands();
        _elements = CreateUIElement();
    }

    private void RegisterTypes()
    {
        container.Register<DocumentViewModel>();
        container.Register<DocumentView>();
        container.Register<PluginContractElementBuilder>();
        container.Register<DocumentViewWrapper>();
        container.Register<CopyButtonWrapper>();
        container.Register<CutButtonWrapper>();
        container.Register<PasteButtonWrapper>();
        container.Register<SaveButtonWrapper>();
    }

    ...........
}

5. おわりに

本プラグインシステムを使用すると、低コストでサンドボックス実行、例外分離、プロセス通信などの高度な機能を利用できます。これらの高度な機能により、ソフトウェア開発におけるいくつかの難題(メモリ使用量、マルチコア利用率、未知の問題によるソフトウェアクラッシュなど)を解決できます。同時に、無限の想像力を与えてくれ、これを基盤としてより強力な機能を持つソフトウェアを構築することが可能になります。

さらに探索

関連読書

その他の記事
同じカテゴリ / 同じタグ 2025/09/13

WPF から Avalonia への移行シリーズ:なぜ WPF プログラムを Avalonia に移行しなければならないのか

過去数年間、当社の上位機ソフトウェアは主に WPF と WinForm で開発されてきました。これらの技術は Windows プラットフォームで非常に便利であり、小規模試作から現在の規模拡大による納品まで、私たちを支えてきました。しかし、ビジネスの発展や顧客ニーズの変化に伴い、単一の Windows テクノロジースタックは私たちが必ず乗り越えなければならない壁となってきました。

続きを読む
同じカテゴリ / 同じタグ 2025/01/26

WPF カスタムXMLファイルによる国際化

この記事では、WPFプログラムでカスタムXMLファイルを使用して国際化を実現する方法について詳しく説明します。必要なNuGetパッケージのインストール、言語リストの動的取得、言語の動的切り替え、コードおよびXAMLインターフェースでの翻訳文字列の使用などを含み、ソースコードのリンクも提供し、開発者がWPFアプリケーションの国際化を簡単に実装できるように支援します。

続きを読む