首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 插件架構設計與實作:反射、介面與動態載入

     編輯:關於C#

前言

插件架構讓應用程式具備擴充性,核心程式不需重新編譯即可整合新功能。本文示範基於 .NET 反射機制的插件框架:定義介面契約、自訂屬性標記、動態載入組件、驗證與例項化插件。

架構設計

1.1 核心契約介面

// 共用合約組件
public interface IPlugin
{
    string Name { get; }
    string Description { get; }
    UserControl GetUI();           // 提供編輯介面
    object[] GetData();            // 取得資料列表
    bool Save(string path);        // 儲存資料
    bool Print(PrintDocument doc); // 列印功能
}

// 資料項目介面
public interface IPluginData
{
    event EventHandler DataChanged;
    string DisplayName { get; }
}

// 編輯控制項基底
public abstract class PluginEditControl : UserControl
{
    public abstract void LoadData(IPluginData data);
    public abstract IPluginData GetData();
}

1.2 自訂屬性:插件中繼資料


[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class PluginMetadataAttribute : Attribute
{
    public string DisplayName { get; }
    public string Description { get; }
    public string Author { get; }
    public Version Version { get; }

    public PluginMetadataAttribute(string displayName, string description, string author = "", string version = "1.0.0")
    {
        DisplayName = displayName;
        Description = description;
        Author = author;
        Version = Version.Parse(version);
    }
}

主程式插件載入器

2.1 插件載入核心邏輯


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;

public class PluginLoader
{
    private readonly string _pluginDirectory;
    private readonly List _loadedPlugins = new();

    public PluginLoader(string pluginDirectory)
    {
        _pluginDirectory = pluginDirectory;
        if (!Directory.Exists(_pluginDirectory))
            Directory.CreateDirectory(_pluginDirectory);
    }

    public IReadOnlyList LoadPlugins()
    {
        _loadedPlugins.Clear();
        var pluginFiles = Directory.GetFiles(_pluginDirectory, "*.dll");

        foreach (var file in pluginFiles)
        {
            try
            {
                Assembly asm = Assembly.LoadFrom(file);
                foreach (Type type in asm.GetTypes())
                {
                    if (!typeof(IPlugin).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract)
                        continue;

                    // 驗證必要屬性
                    var metaAttr = type.GetCustomAttribute();
                    if (metaAttr == null)
                    {
                        Log($"插件 {type.FullName} 缺少 PluginMetadataAttribute,略過");
                        continue;
                    }

                    // 例項化
                    if (Activator.CreateInstance(type) is IPlugin plugin)
                    {
                        _loadedPlugins.Add(plugin);
                        Log($"成功載入插件:{metaAttr.DisplayName} v{metaAttr.Version}");
                    }
                }
            }
            catch (Exception ex)
            {
                Log($"載入 {file} 失敗:{ex.Message}");
            }
        }
        return _loadedPlugins.AsReadOnly();
    }

    private void Log(string msg) => Console.WriteLine($"[PluginLoader] {msg}");
}

2.2 整合到主視窗 (TreeView + 動態 UI)


public class MainForm : Form
{
    private readonly PluginLoader _loader;
    private TreeView _treePlugins;
    private Panel _panelEditor;

    public MainForm()
    {
        _loader = new PluginLoader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"));
        InitializeComponent();
        LoadPlugins();
    }

    private void InitializeComponent()
    {
        _treePlugins = new TreeView { Dock = DockStyle.Left, Width = 250 };
        _panelEditor = new Panel { Dock = DockStyle.Fill, BorderStyle = BorderStyle.FixedSingle };
        var splitter = new Splitter { Dock = DockStyle.Left };

        _treePlugins.AfterSelect += TreePlugins_AfterSelect;

        this.Controls.AddRange(new Control[] { _panelEditor, splitter, _treePlugins });
        this.Text = "插件架構示範";
        this.ClientSize = new Size(1000, 700);
    }

    private void LoadPlugins()
    {
        var plugins = _loader.LoadPlugins();
        foreach (var plugin in plugins)
        {
            var meta = plugin.GetType().GetCustomAttribute();
            var rootNode = new TreeNode(meta.DisplayName) { Tag = plugin };
            _treePlugins.Nodes.Add(rootNode);

            // 載入資料節點
            foreach (var data in plugin.GetData())
            {
                if (data is IPluginData pData)
                {
                    var dataNode = new TreeNode(pData.DisplayName) { Tag = new { Plugin = plugin, Data = pData } };
                    rootNode.Nodes.Add(dataNode);
                }
            }
            rootNode.Expand();
        }
    }

    private void TreePlugins_AfterSelect(object sender, TreeViewEventArgs e)
    {
        _panelEditor.Controls.Clear();
        if (e.Node.Tag is { } tag)
        {
            if (tag is IPlugin plugin)
            {
                // 顯示插件主介面
                var ui = plugin.GetUI();
                ui.Dock = DockStyle.Fill;
                _panelEditor.Controls.Add(ui);
            }
            else if (tag.GetType().GetProperty("Plugin")?.GetValue(tag) is IPlugin p &&
                     tag.GetType().GetProperty("Data")?.GetValue(tag) is IPluginData data)
            {
                // 顯示資料編輯介面
                var editCtrl = (PluginEditControl)Activator.CreateInstance(
                    p.GetType().Assembly.GetTypes()
                        .First(t => typeof(PluginEditControl).IsAssignableFrom(t) && !t.IsAbstract));
                editCtrl.Dock = DockStyle.Fill;
                editCtrl.LoadData(data);
                _panelEditor.Controls.Add(editCtrl);
            }
        }
    }
}

插件實作範例:員工管理

3.1 資料模型


[PluginMetadata("員工管理", "管理員工基本資料", "Dev Team", "1.2.0")]
public class EmployeePlugin : IPlugin
{
    private readonly List _employees = new()
    {
        new EmployeeData("Jerry", "Seinfeld"),
        new EmployeeData("Bill", "Cosby"),
        new EmployeeData("Martin", "Lawrence")
    };

    public string Name => "EmployeePlugin";
    public string Description => "管理員工基本資料";

    public UserControl GetUI() => new EmployeeMainControl(_employees);

    public object[] GetData() => _employees.Cast().ToArray();

    public bool Save(string path)
    {
        // 實作儲存邏輯 (JSON, XML, DB...)
        return true;
    }

    public bool Print(PrintDocument doc) => true; // 實作列印
}

public class EmployeeData : IPluginData
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string DisplayName => $"{FirstName} {LastName}";
    public event EventHandler DataChanged;

    public EmployeeData(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }
}


3.2 編輯控制項


public class EmployeeEditControl : PluginEditControl
{
    private EmployeeData _currentData;
    private TextBox _txtFirst, _txtLast;

    public EmployeeEditControl()
    {
        _txtFirst = new TextBox { Location = new Point(10, 10), Width = 200 };
        _txtLast = new TextBox { Location = new Point(10, 40), Width = 200 };
        this.Controls.AddRange(new Control[] {
            new Label { Text = "名字:", Location = new Point(10, 10) }, _txtFirst,
            new Label { Text = "姓氏:", Location = new Point(10, 40) }, _txtLast
        });
    }

    public override void LoadData(IPluginData data)
    {
        _currentData = (EmployeeData)data;
        _txtFirst.Text = _currentData.FirstName;
        _txtLast.Text = _currentData.LastName;
    }

    public override IPluginData GetData()
    {
        _currentData.FirstName = _txtFirst.Text;
        _currentData.LastName = _txtLast.Text;
        return _currentData;
    }
}

部署結構

AppRoot/
├── Host.exe                 (主程式)
├── PluginContract.dll       (共用介面 IPlugin, IPluginData, PluginMetadataAttribute)
├── Plugins/
│   ├── EmployeePlugin.dll   (參考 PluginContract)
│   └── CustomerPlugin.dll

關鍵點:插件組件只參考合約組件,不參考主程式,實現真正解耦。

進階擴充建議

  • 依賴注入:主程式提供服務 (ILogger, IDataAccess) 透過建構子注入插件
  • 版本相容:介面加版本號或採用抽象基底類別而非介面
  • 隔離載入:使用 AssemblyLoadContext (.NET Core 3.0+) 實現插件卸載與版本隔離
  • 安全性:簽名驗證、權限沙箱

總結

本文展示完整的 C# 插件架構:介面契約定義、自訂屬性中繼資料、反射動態載入驗證、TreeView 整合動態 UI。此架構符合開放封閉原則,核心程式不修改即可擴充功能,適合模組化應用、工具箱類軟體、編輯器擴充等場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved