自社開発のホットプラグ対応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.2.2.短所は

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

  2. パフォーマンスのオーバーヘッド各プラグインは別々のアプリケーションドメインで実行されるため、パフォーマンスのオーバーヘッドが追加される可能性があります。特に、プラグインが多数または頻繁にロードされている場合は、アプリケーションのパフォーマンスに影響を及ぼす可能性があります。

1.3まとめまとめまとめ

比較を通じて、プラグインシステムの基本的な理解を持っています。プラグインの分離実行要件がない場合、MEFは良い選択です。それは比較的簡単で、複雑な理論を理解する必要はなく、サンプルコードを参照して、すぐにプロジェクトで使用することができます。より安全でパフォーマンスの高いアプリケーションを構築する必要がある場合は、MAFが適していますが、MAFにはいくつかの大きな問題があります。例えば、非常に単純な機能でも固定されたプロジェクト構造に従わなければならない、柔軟性が低く、使用が非常に複雑で、しきい値が高いなどです。アプリケーションがある程度のサイズに達すると、プログラムのロード速度が問題になります。これらの欠点により、実際のプロジェクト開発ではほとんど選ばれていない。

上記の理由から、MEFとMAFの機能を組み合わせたプラグインシステムが必要です。これは軽量なフレームワークであり、パフォーマンスが良く、使いやすく、拡張性が高く、安全で信頼性が高いことが今日のテーマです。

2. システム設計は

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

图片

2.2.プロセスの開始

图片

2.3.詳細デザインの詳細

2.3.1.コンテナ化

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

2.3.2.プラグインランチャー

プラグインの実行を担当するコンソールアプリケーションであり、プラグイン設定ファイルのロード、プラグインの異常情報のコンテナへの報告、プラグインのホットプラグなどの機能があります。

2.3.3.プラグイン

插件是一个dll程序集或exe程序,该程序集或exe程序必须有一个类继承自Plugin抽象类,以供容器探测插件时被识别到。在插件类中可以定义自己的 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. プロジェクトの実戦

以下の例は、メニュー、ツールバー、ドキュメントを備えた典型的なソフトウェアでプラグインシステムを使用することを示しています。プラグインがロードされると、プラグイン内のメニュー、ツールバー、ドキュメントがホストプログラマにロードされ、プラグインが予期せず終了したり、アクティブに閉じたりすると、プラグイン内のメニュー、ツールバー、ドキュメントが自動的にアンロードされます。

图片

プラグインには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.ドキュメント·ビュー

ドキュメントは、ホストプログラムにUser Controlを渡します。

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. 結びの言葉

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

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2025/09/13

WPFからAvaloniaへの移行シリーズ:WPFプログラムをAvaloniaに移行する必要がある理由

ここ数年、当社のホストソフトウェアは主にWPFとWin Formで開発されてきました。これらのテクノロジーはWindowsプラットフォームで非常にうまく機能し、小規模なパイロット生産から今日の大規模なデリバリまでの段階を経てきました。しかし、ビジネスの成長と顧客のニーズの変化に伴い、単一のWindowsテクノロジースタックは私たちが乗り越えなければならないハードルになりました。

继续阅读
同分类 / 同标签 2025/01/26

WPFはカスタムXMLファイルで国際化を実現

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

继续阅读