插件架構讓應用程式具備擴充性,核心程式不需重新編譯即可整合新功能。本文示範基於 .NET 反射機制的插件框架:定義介面契約、自訂屬性標記、動態載入組件、驗證與例項化插件。
// 共用合約組件
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();
}
[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);
}
}
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}");
}
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);
}
}
}
}
[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
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
關鍵點:插件組件只參考合約組件,不參考主程式,實現真正解耦。
AssemblyLoadContext (.NET Core 3.0+) 實現插件卸載與版本隔離本文展示完整的 C# 插件架構:介面契約定義、自訂屬性中繼資料、反射動態載入驗證、TreeView 整合動態 UI。此架構符合開放封閉原則,核心程式不修改即可擴充功能,適合模組化應用、工具箱類軟體、編輯器擴充等場景。