C#]插件编程框架 MAF 开发总结
Posted Jack_孟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#]插件编程框架 MAF 开发总结相关的知识,希望对你有一定的参考价值。
1. 什么是MAF和MEF?
MEF和MEF微软官方介绍:https://learn.microsoft.com/zh-cn/dotnet/framework/mef/
MEF是轻量化的插件框架,MAF是复杂的插件框架。
因为MAF有进程隔离和程序域隔离可选。我需要插件进程隔离同时快速传递数据,最后选择了MAF。
如果不需要真正的物理隔离还是建议使用简单一点的MEF框架。
2. 如何学习MAF?
MAF其实是一项很老的技术,入门我看的是《WPF编程宝典》第32章 插件模型。里面有MAF和MEF的详细介绍和许多样例。
但是要深入理解还是看了很多其他的东西,下面我详细说明,我自己理解和总结的MAF。
3. MAF框架入门
3.1 MAF框架构成与搭建
MAF框架模式是固定的,这里做一个详细介绍。
首先是要添加几个新项目,下图中不包含主项目。
Addin文件夹是放置插件用的,其余都是必要项目。
假设HostView项目和主项目的输出路径是..\\Output\\
然后修改每个项目的输出文件夹,例如AddInSideAdapter项目输出路径可以设置为..\\Output\\AddInSideAdapters\\
注意插件项目输出到Addin文件夹中的子文件夹是..\\..\\Output\\AddIns\\MyAddin\\
最后项目的输出文件夹结构是:
D:\\Demo\\Output\\AddIns
D:\\Demo\\Output\\AddInSideAdapters
D:\\Demo\\Output\\AddInViews
D:\\Demo\\Output\\Contracts
D:\\Demo\\Output\\HostSideAdapters
来看看MAF框架模型构成。
上图中绿色的是被引用蓝色项目所引用。例如HostSideAdapter就要引用Contract和Hostview,如下图所示。
注意引用时取消勾选复制本地。
这时就完成基本项目结构的搭建。
3.2 MAF框架实现
这里想实现宿主项目和插件项目的双向通信。即插件项目将相关函数接口在宿主实现,然后将宿主项目相关函数接口用委托类的方式注册给插件项目。实现双向通信。
用《WPF编程宝典》样例代码来说,样例中,插件程序实现ProcessImageBytes处理图像数据的函数,处理同时需要向宿主项目报告处理进度,宿主中 ReportProgress函数实现进度可视化。
MAF实现一般是先写Contract协议,明确需要的函数接口。然后写AddlnView和HostView。实际上这两个是将函数接口抽象化,在接口里函数复制过来前面加 public abstract 就行。
之后HostSideAdapter和AddInSideAdapter直接快速实现接口。
首先从Contract开始,Contract是定义接口,需要设置对象标识符[AddInContract],且必须继承IContract。
[AddInContract] public interface IImageProcessorContract : IContract byte[] ProcessImageBytes(byte[] pixels); void Initialize(IHostObjectContract hostObj); public interface IHostObjectContract : IContract void ReportProgress(int progressPercent);
Initialize函数是提供宿主函数注册的接口。
然后在HostView和AddInView分别定义主程序和插件程序的接口抽象类。
public abstract class ImageProcessorHostView public abstract byte[] ProcessImageBytes(byte[] pixels); public abstract void Initialize(HostObject host); public abstract class HostObject public abstract void ReportProgress(int progressPercent);
注意AddlnView需要设置对象标识符[AddInBase]。
[AddInBase] public abstract class ImageProcessorAddInView public abstract byte[] ProcessImageBytes(byte[] pixels); public abstract void Initialize(HostObject hostObj); public abstract class HostObject public abstract void ReportProgress(int progressPercent);
之后在HostSideAdapter实现抽象类。
注意HostSideAdapter继承HostView的抽象类,在构造函数里需设置ContractHandle插件生存周期,ContractHandle不能为readonly。
[HostAdapter] public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView private Contract.IImageProcessorContract contract; private ContractHandle contractHandle; public ImageProcessorContractToViewHostAdapter(Contract.IImageProcessorContract contract) this.contract = contract; contractHandle = new ContractHandle(contract); public override byte[] ProcessImageBytes(byte[] pixels) return contract.ProcessImageBytes(pixels); public override void Initialize(HostView.HostObject host) HostObjectViewToContractHostAdapter hostAdapter = new HostObjectViewToContractHostAdapter(host); contract.Initialize(hostAdapter); public class HostObjectViewToContractHostAdapter : ContractBase, Contract.IHostObjectContract private HostView.HostObject view; public HostObjectViewToContractHostAdapter(HostView.HostObject view) this.view = view; public void ReportProgress(int progressPercent) view.ReportProgress(progressPercent);
在AddInSideAdapter实现Contract接口,基本和HostSideAdapter类似,只是继承的类不同。
[AddInAdapter] public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract private AddInView.ImageProcessorAddInView view; public ImageProcessorViewToContractAdapter(AddInView.ImageProcessorAddInView view) this.view = view; public byte[] ProcessImageBytes(byte[] pixels) return view.ProcessImageBytes(pixels); public void Initialize(Contract.IHostObjectContract hostObj) view.Initialize(new HostObjectContractToViewAddInAdapter(hostObj)); public class HostObjectContractToViewAddInAdapter : AddInView.HostObject private Contract.IHostObjectContract contract; private ContractHandle handle; public HostObjectContractToViewAddInAdapter(Contract.IHostObjectContract contract) this.contract = contract; this.handle = new ContractHandle(contract); public override void ReportProgress(int progressPercent) contract.ReportProgress(progressPercent);
宿主项目中需要实现HostView里HostObject抽象类。
private class AutomationHost : HostView.HostObject private ProgressBar progressBar; public AutomationHost(ProgressBar progressBar) this.progressBar = progressBar; public override void ReportProgress(int progressPercent) // Update the UI on the UI thread. progressBar.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate() progressBar.Value = progressPercent; );
然后是在宿主项目里激活插件,并初始化AutomationHost。
string path = Environment.CurrentDirectory; AddInStore.Update(path);//更新目录中Addins目录里的插件 IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(HostView.ImageProcessorHostView), path);//查找全部插件 lstAddIns.ItemsSource = tokens;//插件可视化 AddInToken token = (AddInToken)lstAddIns.SelectedItem;//选择插件 AddInProcess addInProcess = new AddInProcess();//创建插件进程 addInProcess.Start();//激活插件进程 addin = token.Activate<HostView.ImageProcessorHostView>(addInProcess,AddInSecurityLevel.Internet);//激活插件 //如果只是想隔离程序域,就无需创建AddInProcess,激活插件如下 // HostView.ImageProcessorHostView addin = token.Activate<HostView.ImageProcessorHostView>(AddInSecurityLevel.Host); automationHost = new AutomationHost(progressBar);//创建AutomationHost类 addin.Initialize(automationHost);//初始化automationHost
插件项目中实现AddInView中的抽象类。
[AddIn("Negative Image Processor", Version = "1.0", Publisher = "Imaginomics",Description = "")] public class NegativeImageProcessor : AddInView.ImageProcessorAddInView public override byte[] ProcessImageBytes(byte[] pixels) int iteration = pixels.Length / 100; for (int i = 0; i < pixels.Length - 2; i++) pixels[i] = (byte)(255 - pixels[i]); pixels[i + 1] = (byte)(255 - pixels[i + 1]); pixels[i + 2] = (byte)(255 - pixels[i + 2]); if (i % iteration == 0) host?.ReportProgress(i / iteration); return pixels; private AddInView.HostObject host; public override void Initialize(AddInView.HostObject hostObj) host = hostObj;
这时宿主可以把数据传递给插件程序,插件程序中ProcessImageBytes处理数据然后通过host?.ReportProgress(i / iteration);向宿主传递消息。
这里有提供样例程序。
4. MAF框架常见问题
4.1手动关闭插件
AddInController addInController = AddInController.GetAddInController(addIn);
addInController.Shutdown();
此方法适应于非应用隔离的手动关闭。对于应用隔离式插件,用此方法会抛出异常。
如上面样例就是应用隔离的插件,可以根据进程id直接关闭进程。
public void ProcessClose() try if (process != null) Process processes = Process.GetProcessById(addInProcess.ProcessId); if (processes?.Id > 0) processes.Close(); catch (Exception)
4.2 插件异常
System.Runtime.Remoting.RemotingException: 从 IPC 端口读取时失败: 管道已结束。这是插件最常见的异常,因为插件抛出异常而使得插件程序关闭。
如果是插件调用非托管代码,而产生的异常,可以查Windows应用程序日志来确定异常。其余能捕获的异常尽量捕获保存到日志,方便查看。
4.3 双向通信
实际应用过程中,往往是通过委托来将宿主相关函数暴露給一个类,然后通过在宿主程序初始化后。在插件中实例化后就可以直接调用宿主的相关函数,反之同理。
这里是通过委托暴露宿主的一个函数。
public delegate void UpdateCallBack(string message, bool isclose, int leve); public class VideoHost : HostAddInView public event UpdateCallBack Updatecallback; public override void ProcessVideoCallBack(string message, bool isclose, int leve) Updatecallback?.Invoke(message, isclose, leve);
在插件程序中实例化后调用。
private HostAddInView hostAddInView; public override void Initialize(HostAddInView hostAddInView) this.hostAddInView = hostAddInView; private void ErrorCallback(string message, bool isclose, int leve) hostAddInView?.ProcessVideoCallBack(message, isclose, leve);
5. MAF深入理解
MAF本质是实现IpcChannel通信,在一个期刊中有作者抛弃MAF固定结构自己实现IpcChannel,因为代码很复杂,就不在此详细阐述。
如果要实现应用域隔离,自己实现IpcChannel,MAF中的应用域隔离实现也是非常好的参考资料。
MAF的7层结构主要是实现从插件的宿主函数转换,例如可以在将插件程序的界面放入主界面中渲染,做出像浏览器一样的开一个界面就是一个进程。将插件中的组件在AddInSideAdapter中转换为Stream然后在HostSideAdapter中将Stream实例化为组件。而HostView和AddInView实际上是提供两个转换接口,Contract是定义传输接口。
另外如果传输插件向数组传递图像数据,最后是转换成byte[],或者使用共享内存。
如果有什么遗漏和错误,欢迎指正,批评。
__EOF__
出处:https://www.cnblogs.com/mrf2233/p/17434368.html
.Net插件编程模型:MEF和MAF
.Net插件编程模型:MEF和MAF
MEF和MAF都是C#下的插件编程框架,我们通过它们只需简单的配置下源代码就能轻松的实现插件编程概念,设计出可扩展的程序。这真是件美妙的事情!
MEF(Managed Extensibility Framework)
MEF的工作原理大概是这样的:首先定义一个接口,用这个接口来约束插件需要具备的职责;然后在实现接口的程序方法上面添加反射标记“[Export()]”将实现的内容导出;最后在接口的调用程序中通过属性将插件加载进来。我们还是用代码来描述吧:
1. 定义一个接口:
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:该节主要学习.net下的插件编程框架MEF(managed extensibility framework)
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
namespace chapter28_simplecontract
public interface ICalculator
IList<IOperation> GetOperations();
double Operate(IOperation operation, double[] operands);
public interface IOperation
string Name get;
int NumberOperands get;
public interface ICaculatorExtension
string Title get;
string Description get;
FrameworkElement GetUI();
2. 实现定义的接口(部分一)
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
*/
[Export(typeof(ICalculator))]
public class Caculator:ICalculator
public IList<IOperation> GetOperations()
return new List<IOperation>()
new Operation Name="+",NumberOperands=2,
new OperationName="-",NumberOperands=2,
new OperationName="*",NumberOperands=2,
new OperationName="/",NumberOperands=2
;
public double Operate(IOperation operation, double[] operands)
double result=0;
switch (operation.Name)
case "+":
result = operands[0] + operands[1];
break;
case "-":
result = operands[0] - operands[1];
break;
case "*":
result = operands[0] * operands[1];
break;
case "/":
result = operands[0] / operands[1];
break;
default:
throw new Exception("not provide this method");
return result;
public class Operation:IOperation
public string Name
get;
internal set;
public int NumberOperands
get;
internal set;
实现定义的接口(部分二)
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
*/
[Export(typeof(ICalculator))]
public class Caculator : ICalculator
public IList<IOperation> GetOperations()
return new List<IOperation>()
new Operation Name="+",NumberOperands=2,
new OperationName="-",NumberOperands=2,
new OperationName="*",NumberOperands=2,
new OperationName="/",NumberOperands=2,
new OperationName="%",NumberOperands=2,
new OperationName="**",NumberOperands=1,
;
public double Operate(IOperation operation, double[] operands)
double result = 0;
switch (operation.Name)
case "+":
result = operands[0] + operands[1];
break;
case "-":
result = operands[0] - operands[1];
break;
case "*":
result = operands[0] * operands[1];
break;
case "/":
result = operands[0] / operands[1];
break;
case "%":
result=operands[0]%operands[1];
break;
case "**":
result=operands[0]*operands[0];
break;
default:
throw new Exception("not provide this method");
return result;
public class Operation : IOperation
public string Name
get;
internal set;
public int NumberOperands
get;
internal set;
分析:
标记“[Export(typeof(ICalculator))]”声明表达的意思是:这个类可以编译为插件,并能放入插件容器“ICalculator”中。这里需要注意的是:部分一和部分二的代码分布在不同的程序集中。导出的插件不一定必须是以类的形式,也可以是方法。
通过导出方法来生成插件:
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
*/
public class Bomb
[Export("Bomb")]
public void Fire()
Console.WriteLine("you are dead!!!");
插件的调用者:
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
* 简介:该节主要学习.net下的插件编程框架MEF(managed extensibility framework)
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using chapter28_simplecontract;
namespace chapter28
class Program
[ImportMany(typeof(ICalculator))]
public IEnumerable<ICalculator> Calculators get; set;
[Import("Bomb")]
public Action Bomb get; set;
static void Main(string[] args)
Program pro = new Program();
pro.Run();
pro.Run2();
public void Run()
var catalog = new DirectoryCatalog("c:\\\\plugins");
var container = new CompositionContainer(catalog);
try
container.ComposeParts(this);
catch (Exception ex)
Console.WriteLine(ex.Message);
return;
ICalculator myCalculator = Calculators.ToList<ICalculator>()[1];
var operations = myCalculator.GetOperations();
var operationsDict = new SortedList<string, IOperation>();
foreach(IOperation item in operations)
Console.WriteLine("Name:0,number operands:1"
, item.Name, item.NumberOperands);
operationsDict.Add(item.Name, item);
Console.WriteLine();
string selectedOp = null;
do
try
Console.Write("Operation?");
selectedOp = Console.ReadLine();
if (selectedOp.ToLower() == "exit"
|| !operationsDict.ContainsKey(selectedOp))
continue;
var operation = operationsDict[selectedOp];
double[] operands = new double[operation.NumberOperands];
for (int i = 0; i < operation.NumberOperands; i++)
Console.WriteLine("\\t operand 0?", i + 1);
string selectedOperand = Console.ReadLine();
operands[i] = double.Parse(selectedOperand);
Console.WriteLine("calling calculator");
double result = myCalculator.Operate(operation, operands);
Console.WriteLine("result:0", result);
catch (Exception ex)
Console.WriteLine(ex.Message);
Console.WriteLine();
continue;
while (selectedOp != "exit");
public void Run2()
var catalog = new DirectoryCatalog("c:\\\\plugins");
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
Bomb.Invoke();
Console.ReadKey();
分析:
标记“[ImportMany(typeof(ICalculator))]”,该声明表达的意图是:将所有声明了标记“[Export(typeof(ICalculator))]”的程序集加载进容器。这里“[ImportMany]和”[Import]”的区别就是:前者的容器可以存放多个插件,而后者只能存放一个。
光声明“[Import()]”和”[Export()]”标记是不行的,还必须通过下面的代码将这两个标记的功能联合起来:
//DirectoryCatalog表示这类插件会存放在系统的哪个文件夹下
var catalog = new DirectoryCatalog("c:\\\\plugins");
var container = new CompositionContainer(catalog);
try
//将存放在目录中的插件按“[Export()]和[Import()]”规则装载进当前
//类中。
container.ComposeParts(this);
catch (Exception ex)
Console.WriteLine(ex.Message);
return;
执行结果
Name:+,number operands:2 Name:-,number operands:2 Name:*,number operands:2 Name:/,number operands:2
Operation?+ operand 1? 1 operand 2? 1 calling calculator result:2 Operation?exit
you are dead!!! |
MAF(Managed Addin Framework)
MAF也是.Net为我们提供的一个“插件编程”解决方案。它比MEF复杂,需要配置很多元素。但它也有些优点:1.宿主程序和插件程序可以进行隔离,以此降低运行插件所带来的风险;2。MAF的设计是基于7个程序集组成的管道,这些管道部分可以单独更换,这些管道的详细情况见下图。
图1
使用MAF是需要有些细节需要注意:组成管道的7个程序集在系统中的保存路径有格式要求,并且没个保存它的文件夹内只运行同时出现一个程序集。具体情况如下图所示:
图2
图3
图4
图5
下面我们来看一个小Demo吧,这个demo一共有7个项目,它们分别对应图1描述的管道中的7个部分。具体情况见下图。
图6
插件:Addin_1,Addin_2 插件视图:AddinSideView 插件适配器:AddinSideAdapter 协定:IContract 宿主视图:HostSideView 宿主适配器:HostSideAdapter 宿主程序:Host |
程序代码
Addin_1
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:测试MAF,这段代码是用来定义一个插件的。这个插件可以在宿主程序
中动态加载。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn;
using System.AddIn.Pipeline;
namespace Addin_1
[AddIn("Helloworld",Description="this is helloworld addin"
,Publisher="GhostBear",Version="1.0")]
public class Addin_1:AddinSideView.AddinSideView
public string Say()
return "Helloworld";
Addin_2
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn;
namespace Addin_2
[AddIn("SuperBomb",Description="This is a bigger bomb"
,Publisher="SuperWorker",Version="1.0.0.0")]
public class Addin_2:AddinSideView.AddinSideView
public string Say()
return "B--O--M--B";
AddinSideView
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
* 简介:测试MAF,这段代码是定义插件端的视图类,该视图类的方法和属性必须与协定一致。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn.Pipeline;
namespace AddinSideView
[AddInBase()]
public interface AddinSideView
string Say();
AddinSideAdapter
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
* 简介:测试MAF,这段代码是插件端的适配器类,它用来实现插件端视图类。
* 并组合协定。这样就能让插件和协定解耦,如果插件有所修改就换掉
* 该适配器类就可以了。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn.Pipeline;
namespace AddinSideAdapter
[AddInAdapter]
public class AddinSideAdapter : ContractBase,IContract.IMyContract
private AddinSideView.AddinSideView _handler;
public AddinSideAdapter(AddinSideView.AddinSideView handler)
this._handler = handler;
public string Say()
return this._handler.Say();
IContract
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:测试MAF,这段代码是定义协定。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn.Pipeline;
using System.AddIn.Contract;
namespace IContract
[AddInContract]
public interface IMyContract:System.AddIn.Contract.IContract
string Say();
HostSideView
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:测试MAF,这段代码用来定义宿主段的视图类,该类的所有方法和属性需与协定类一致。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace HostSideView
public interface HostSideView
string Say();
HostSideAdapter
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:测试MAF,这段代码用来定义宿主端的适配器类。该类实现宿主端的
视图类并组合协定。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn.Pipeline;
namespace HostSideAdapter
[HostAdapter()]
public class HostSideAdapter:HostSideView.HostSideView
private IContract.IMyContract _contract;
//这行代码重要
private System.AddIn.Pipeline.ContractHandle _handle;
public HostSideAdapter(IContract.IMyContract contract)
this._contract = contract;
this._handle = new ContractHandle(contract);
public string Say()
return this._contract.Say();
Host
/*
作者:GhostBear
博客:http://blog.csdn.net/ghostbear
简介:测试MAF,这段代码是宿主程序。该程序可以针对保存在某个目录下的插件来进行选择性调用。
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.ObjectModel;
using System.AddIn.Hosting;
using HostSideView;
namespace Host
class Program
static void Main(string[] args)
string path = @"D:\\学习文档\\c#\\c#高级编程7\\MAF\\MAF";
string[] warnings = AddInStore.Update(path);
foreach (var tmp in warnings)
Console.WriteLine(tmp);
//发现
var tokens = AddInStore.FindAddIns(typeof(HostSideView.HostSideView), path);
Console.WriteLine("当前共有0个插件可以选择。它们分别为:",tokens.Count);
var index = 1;
foreach (var tmp in tokens)
Console.WriteLine(string.Format("[4]名称:0,描述:1,版本:2,发布者:3", tmp.Name, tmp.Description, tmp.Version, tmp.Publisher,index++));
var token = ChooseCalculator(tokens);
//隔离和激活插件
AddInProcess process=new AddInProcess(Platform.X64);
process.Start();
var addin = token.Activate<HostSideView.HostSideView>(process, AddInSecurityLevel.FullTrust);
Console.WriteLine("PID:0",process.ProcessId);
//调用插件
Console.WriteLine(addin.Say());
Console.ReadKey();
private static AddInToken ChooseCalculator(Collection<AddInToken> tokens)
if (tokens.Count == 0)
Console.WriteLine("No calculators are available");
return null;
Console.WriteLine("Available Calculators: ");
// Show the token properties for each token in the AddInToken collection
// (tokens), preceded by the add-in number in [] brackets.
int tokNumber = 1;
foreach (AddInToken tok in tokens)
Console.WriteLine(String.Format("\\t[0]: 1 - 2\\n\\t3\\n\\t\\t 4\\n\\t\\t 5 - 6",
tokNumber.ToString(),
tok.Name,
tok.AddInFullName,
tok.AssemblyName,
tok.Description,
tok.Version,
tok.Publisher));
tokNumber++;
Console.WriteLine("Which calculator do you want to use?");
String line = Console.ReadLine();
int selection;
if (Int32.TryParse(line, out selection))
if (selection <= tokens.Count)
return tokens[selection - 1];
Console.WriteLine("Invalid selection: 0. Please choose again.", line);
return ChooseCalculator(tokens);
分析
在上面的7个程序集,起解耦作用的关键还是2个适配器类。调用程序不直接调用协定,而是通过通过调用这2个适配器来间接调用协定。
小结
MEF和MAF为我们实现“插件编程”提供了2中选择,它们设计的出发点也是完全不同的。在使用它们的时候还是需要更加具体需求来权衡使用。
以上是关于C#]插件编程框架 MAF 开发总结的主要内容,如果未能解决你的问题,请参考以下文章