MEF插件架构中的Fire-and-forget方法

9
这个问题可能与设计或代码有关,但我陷入困境,所以我接受任何类型的答案;指向正确方向的指针!
我使用MEF(托管可扩展性框架)开发了一款作为插件协调器的WPF软件。该应用程序仅根据用户选择在插件之间重定向数据,因此根本不知道插件的功能(特别是因为它们可以由第三方开发者开发)。应用程序和插件共享一个接口来了解要调用哪些方法,因此流量是双向的:插件调用主应用程序中的方法发送数据,然后主应用程序将此数据传递给另一个插件。
到目前为止,这是有效的,但我在同步行为方面遇到了问题。界面定义的所有方法都缺少返回值(Void),我正在努力实现“点火并忘记”这种方法,即调用应用程序无需等待插件接收函数完成执行代码(以及回到主应用程序的调用!)。
那么解决这个问题的最佳方法是什么?让每个插件(和主应用程序)将其工作负荷放在某种“堆栈”上,以便能够将控制权返回到调用方,然后有一些机制单独运行,按项处理堆栈项(并且将此堆栈方法作为异步执行)?
值得注意的其他事情是插件在单独的线程中运行(根据调试器线程窗口),并且当它们被初始化时,它们会从调用主应用程序获取引用,以便可以在主应用程序中触发函数。插件还经常需要告诉主应用程序它们处于什么状态(空闲、工作、错误等),并发送要由主应用程序记录的数据,因此这往往会创建一个嵌套的调用层次结构(如果你跟我走,很难解释)。
我在这个问题上使用了.Net 4.5。
以下是某些代码的简化示例。我替换了一些名称,因此如果某处存在拼写错误,则只存在于此处而不是真实代码中。 :)
接口:
public interface IMyPluggableApp 
{
    void PluginStatus(string PluginInstanceGuid, PluginInstanceState PluginInstanceState);
    void DataReceiver(string PluginInstanceGuid, string ConnectorGuid, object Data);
    void Logg(string PluginInstanceGuid, LoggMessageType MessageType, string Message);
}

public interface IPluginExport
{
    PluginInfo PluginInfo { get; set; }
    void Initialize(string PluginInstanceGuid, Dictionary<string, string> PluginUserSettings, IMyPluggableApp MyPluggableApp);
    void Start(string PluginInstanceGuid, List<ConnectorInstanceInfo> ConnectedOutputs);
    void Stop(string PluginInstanceGuid);
    void PluginClick(string PluginInstanceGuid);
    void PlugginTrigger(string ConnectorGuid, object Data);
}

插件:
    public static IMyPluggableApp _MyPluggableApp

[PartCreationPolicy(CreationPolicy.NonShared)]
[Export(typeof(IPluginExport))]
public class PluginExport : IPluginExport
{
     public void Initialize(string PluginInstanceGuid, Dictionary<string, string> pluginUserSettings, IMyPluggableApp refMyPluggableApp)
    {
        _MyPluggableApp = refMyPluggableApp; // Populate global object with a ref to the calling application

        // some code for setting saved user preferences

        _MyPluggableApp.PluginStatus(PluginInfo.PluginInstanceGuid, PluginInstanceState.Initialized); // Tell main app we're initialized
    }
    public void Start(string PluginInstanceGuid, List<ConnectorInstanceInfo> ConnectedOutputs)
    {
        // Some code for preparing the plugin functionality

        _MyPluggableApp.PluginStatus(PluginInfo.PluginInstanceGuid, PluginInstanceState.Initialized); // Tell main app we started
    }
    public void PlugginTrigger(string ConnectorGuid, object Data)
    {
        _MyPluggableApp.PluginStatus(AvailablePlugins.PluginInfo.PluginInstanceGuid, PluginInstanceState.Running_Busy); // Tell main app we're busy

                // Run the code that actually provides the functionality of this plugin

        _MyPluggableApp.PluginStatus(AvailablePlugins.PluginInfo.PluginInstanceGuid, PluginInstanceState.Running_Idle); // Tell main app we're idle
    }

    // and so on ...
}

还有主要的应用程序:

public partial class MainWindow : IMyPluggableApp 
    {
        [ImportMany(typeof(IPluginExport))]
        IPluginExport[] _availablePlugins;

        public void PluginStatus(string PluginInstanceGuid, PluginInstanceState PluginInstanceState)
        {
                    // Code for setting status in GUI
        }

        public void DataReceiver(string PluginInstanceGuid, string ConnectorGuid, object Data)
        {
                    ConnectorInfo connector_source = GetConnectorInfo(ConnectorGuid);
                    PluginInfo plugin_source = GetPluginInfo_ByPluginInstanceGuid(PluginInstanceGuid);

                        ConnectorInstanceInfo connector_destination = (from i in _project.PluginInstances
                                    from y in i.ConnectedConnectors
                                    where i.PluginInstanceGuid == PluginInstanceGuid
                                    && y.ConnectedFromOutput_ConnectorGuid == ConnectorGuid
                            select y).FirstOrDefault();

                        _availablePlugins.Where(xx => xx.PluginInfo.PluginInstanceGuid == connector_destination.ConnectedToInput_PluginInstanceGuid).First().PlugginTrigger(ConnectorGuid, Data);
        }

        public void Logg(string PluginInstanceGuid, LoggMessageType MessageType, string Message)
        {
                    // Logg stuff
          }
}

这是主应用程序中的DataReceiver功能,它接收数据,查看哪个插件应该拥有它,然后通过PlugginTrigger函数发送它。


你能否发布一个简化版本的插件接口以及一个应用程序当前如何调用其中一个插件方法的示例? - Matt
1个回答

5

几点观察:

  • “Fire and forget”是主机的要求,因此不应该成为插件实现需要担心的事情。
  • 如果我错了,请纠正我,我认为CLR不支持以“fire-and-forget”的方式在同一AppDomain中调用方法。如果您的插件被加载到单独的进程中,并且使用WCF与它们进行通信,则可以简单地在OperationContractAttribute上设置IsOneWay属性

第二点提出了一个解决方案,这似乎对于您的情况有点过度 - 但让我们还是提一下吧。您的插件可以托管进程内的WCF服务,并且所有WPF应用程序与插件之间的通信都可以通过WCF服务代理完成。然而,这会带来配置噩梦,并且真的向其他问题打开了一个潘多拉魔盒,您必须解决这些问题。

让我们从初始问题的简单示例开始,并尝试从那里解决它。以下是具有插件的控制台应用程序的代码:

public class Program
{
    private static void Main(string[] args)
    {
        var host = new CompositionHost();
        new CompositionContainer(new AssemblyCatalog(typeof(Plugin).Assembly)).ComposeParts(host);
        var plugin = host.Plugin;
        plugin.Method();
        Console.ReadLine();

    }

    private class CompositionHost: IPartImportsSatisfiedNotification
    {
        [Import(typeof (IPlugin))] private IPlugin _plugin;

        public IPlugin Plugin { get; private set; }

        public void OnImportsSatisfied()
        {
            Plugin = _plugin;
        }
    }
}

public interface IPlugin
{
    void Method();
}

[Export(typeof(IPlugin))]
public class Plugin : IPlugin
{
    public void Method()
    {
        //Method Blocks
        Thread.Sleep(5000);
    }
}

问题在于调用 plugin.Method() 是阻塞的。为了解决这个问题,我们更改向控制台应用程序公开的接口如下:

public interface IAsyncPlugin
{
    Task Method();
}

实现该接口的调用不会阻塞。唯一需要改变的是CompositionHost类:
    private class CompositionHost: IPartImportsSatisfiedNotification
    {
        [Import(typeof (IPlugin))] private IPlugin _plugin;

        public IAsyncPlugin Plugin { get; private set; }

        public void OnImportsSatisfied()
        {
            Plugin = new AsyncPlugin(_plugin);
        }

        private sealed class AsyncPlugin : IAsyncPlugin
        {
            private readonly IPlugin _plugin;

            public AsyncPlugin(IPlugin plugin)
            {
                _plugin = plugin;
            }

            public Task Method()
            {
                return Task.Factory.StartNew(() => _plugin.Method());
            }
        }
    }
}

显然,这是一个非常简单的例子,当将其应用于您的WPF场景时,实现可能需要稍作变化 - 但总体概念仍然适用。

不错!我并不认为自己是一个超级程序员,也许这就是为什么我不知道如何将您的CompositionHost类与我的解决方案相匹配,因为我正在使用另一种组合方式(使用DirectoryCatalog)。但这是我的错。但我确实尝试了Task.Factory.StartNew的方法来调用方法,至少看起来是这样!但是如果我不包括您提出的解决方案的其余部分,是否安全?或者也许我应该先尝试一些压力测试(使用许多插件相互调用),以查看它是否“似乎”可以? - RobertN
我认为你可以在当前的MEF框架中“导出”CompositionHost类,并将此实例导入到你的应用程序中以使用你的插件。你当然不必包含我的解决方案的其余部分 - 只需使用你觉得有用的部分即可。进行一些测试是绝对没错的... - Lawrence

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接