带GUI的插件架构

时间:2009-11-18 15:30:07

标签: c# .net plugins

我正在开发一个大量使用插件的应用程序。该应用程序在C#中,我正在考虑在WPF中构建配置GUI。在如何管理实际插件本身方面,我得到了插件架构。但是,每个插件都有自己的配置,这就是我在寻求帮助的地方。

插件架构很简单 - 有一个插件工具的接口,我只是加载目录中的所有插件。但是,插件从哪里获取配置?我想有一些通用的方法来处理这个,所以每个插件都不负责读取自己的配置文件。此外,我希望GUI随每个插件一起扩展 - 也就是说,安装的每个插件都应该向GUI添加一个选项卡,其中包含该插件的特定配置选项。然后在保存时,将保存配置文件。

我最好的办法是什么?

12 个答案:

答案 0 :(得分:8)

为可配置插件定义接口:

public interface IConfigurable
{
  public void LoadConfig(string configFile);

  public void ShowConfig();

  // Form or whatever, allows you to integrate it into another control
  public Form GetConfigWindow();
}

只需为可配置插件调用IConfigurable接口。

如果你想要你可以让界面以另一种方式工作,让主应用程序为插件提供一个容器(例如一个框架或一个扩展坞),但是我会推荐相反的方法。 / p>

public interface IConfigurable
{
  void LoadConfig(string configFile);

  void ShowConfig(DockPanel configurationPanel);
}

最后,您可以通过严格定义插件可以作为配置选项提供的内容来实现。

public interface IMainConfigInterop
{
  void AddConfigurationCheckBox(ConfigurationText text);
  void AddConfigurationRadioButton(ConfigurationText text);
  void AddConfigurationSpinEdit(Confguration text, int minValue, int maxValue);
}

public interface IConfigurable
{
  void LoadConfig(string configFile);

  void PrepareConfigWindow(IMainConfigInterop configInterop);
}

当然这个选项是限制性更强,更安全的选项,因为你可以完全限制插件如何与配置窗口进行交互。

答案 1 :(得分:7)

我执行实现以完全解决相同问题的方式(因为dll文件无法读取其配置文件)是我为插件IPlugin定义了两个接口,为主机{{1}定义了一个接口}。插件是通过对主机的引用创建并初始化的(实现IHost接口)。

IHost
然后,

interface IHost { string[] ReadConfiguration(string fileName); } interface IPlugin { void Initialize(IHost host); } class MyPlugin : IPlugin { public void Initialize(IHost host) { host.ReadConfiguration("myplygin.config"); } } 可以在所有插件之间提供所需的任何常用功能(作为读取和写入配置),并且插件也更加了解您的主机应用程序,而不会被锁定到特定的主机应用程序。例如,您可以编写一个Web Service应用程序和一个使用相同插件API的Forms应用程序。

另一种方法是使用主机应用程序读取的配置信息初始化插件,但是,我需要插件能够读取/写入其配置,并根据需要执行其他操作。

答案 2 :(得分:4)

看看Composite WPF。使用这样的东西应该允许你让每个插件定义标签。然后shell应用程序只负责将每个模块加载到该模块的相应视图。

答案 3 :(得分:4)

不希望每个插件读取自己的配置文件的问题是您必须有一个中央文件知道要包含哪些配置设置;这违背了插件的概念,我给你一个插件,你对它一无所知。

当然,这不一样,作为一个中央配置文件,其中包含有关要加载的插件的信息。

与其他人所说的相反,有些方法可以让DLL从他们自己的配置文件中读取配置设置,但是如果你特别不想这样做,我就不会进入它们。

我很惊讶没有人提到注册表。这是每个人都希望锁定在地下室的家庭秘密,但这正是它的意义所在:用于存储任何人(即任何插件)都可以访问的应用程序设置的集中位置。


然而。如果您希望主应用程序访问和存储插件配置,最好的方法(IMO)是有一个接口来查询插件的属性(并将其传回)。如果您保持通用,则可以将其重复用于其他应用。

interface IExposedProperties {
    Dictionary<string,string> GetProperties();
    void SetProperties(Dictionary<string,string> Properties);
}


主应用程序将查询每个插件的一组属性,然后将/更新设置添加到其配置文件中,如下所示:

private void SavePluginSettings(List<IExposedProperties> PluginProperties) {

    //  Feel free to get fancy with LINQ here
    foreach (IExposedProperties pluginProperties in PluginProperties) {

        foreach (KeyValuePair<string,string> Property in pluginProperties.GetProperties()) {

            //  Use Property.Key to write to the config file, and Property.Value
            //  as the value to write.
            //
            //  Note that you will need to avoid Key conflict... you can prepend
            //  the plugin name to the key to avoid this

        } 

    }

}


我还没有测试过,只是在几个空闲的时刻快速地把它扔掉了;我稍后会检查,以确保没有任何愚蠢的错误。但是你明白了。

至于扩展GUI,其他建议几乎已经确定了......选择是你是想将应用程序的控件传递给插件,还是查询插件页面并添加它。

通常你会看到应用程序将一个对象传递给插件......请注意,它必须是一个允许插件完全控制的对象。 I.E.你不会通过标签控件本身,但你可以让应用程序创建标签页并将其传递给插件,基本上说'用它做你想做的'。

“如果没有选项卡返回null”参数在这里有一些权重,但我更喜欢让应用程序处理它使用的所有对象的创建和清理。不能给你一个很好的理由......也许是那些随意的风格偏好之一。我不会批评任何一种方法。

interface IPluginDisplay {
    bool BuildTab(TabPage PluginTab);   // returns false if couldn't create 
}                                       // tab or no data

interface IPluginDisplay {
    TabPage GetTab();                   // creates a tab and returns it
}


HTH,
詹姆斯


P.S。以为我会提到所有插件应用的母亲:Windows资源管理器

你说出来,探险家得到了它。任何应用程序都可以将属性页添加到任何文件类型,右键单击菜单动态查询DLL以查找条目...如果您想了解插件的灵活性,请阅读Shell编程。 Code Project的一些优秀文章......第五部分讨论了如何在文件的属性页面对话框中添加标签。

http://www.codeproject.com/Articles/830/The-Complete-Idiot-s-Guide-to-Writing-Shell-Extens

答案 4 :(得分:3)

我认为,这个问题最好解决两个不同的问题。

  1. 启用每个插件以指定配置对话框(或类似的内容,可能以标签的形式,如您所建议的那样)。
  2. 为插件提供集中配置文件/数据库和API,以存储其配置信息。
  3. 允许每个插件指定配置对话框

    这里的基本方法是让每个插件定义一个属性,该属性将返回Panel,以便主机在选项卡中显示。

    interface IPlugin
    {
        ...
    
        System.Windows.Controls.Panel ConfigurationPanel { get; }
    }
    

    主机可以为每个加载的插件创建一个选项卡,然后将每个插件的配置面板放在正确的选项卡中。

    这里有很多可能性。例如,插件可以通过返回null来表示它没有任何配置选项。主持人当然需要检查这个并决定是否创建标签。

    另一种类似的方法是定义方法而不是属性。在这种情况下,每次显示配置对话框时都会调用该方法,并返回一个新的Panel实例。这可能会更慢,并且不是一个好主意,除非属性方法导致多次向可视树添加/删除元素的问题。我没有遇到过那些问题,所以你应该对房产有好处。

    可能没有必要提供这种灵活性。如上所述,您可以为插件提供API,以便从文本框,复选框,组合框等字段构建其配置对话框。这可能最终会成为比简单{{1}更复杂的方法。 } property。

    当配置关闭时,你可能还需要回调插件,告诉它保存配置数据,我很快就会知道。这是一个例子:

    Panel

    提供中央配置数据库

    这可以通过以下API解决:

    interface IPlugin
    {
        ...
    
        // Inside this method you would get the data from the Panel,
        // or, even better, an object bound to the Panel.
        // Then you would save it with something like in the next section.
        void SaveConfiguration();
    }
    

    然后主机将控制读取并将配置写入磁盘。

    插件的使用示例:

    static class Configuration
    {
        static void Store(string key, object data);
    
        static object Retrieve(string key);
    }
    

    这将使配置数据充当字典。您可能能够使用序列化来存储文件中的任意对象。或者,您可以将配置限制为存储字符串或类似的东西。这取决于需要存储的配置类型。

    最简单的方法存在一些安全问题。插件可以读取和修改其他插件的数据。但是,如果主机通过插件ID或类似名称为密钥添加前缀,则可以将配置有效地分配到每个插件的单独容器中。

答案 5 :(得分:2)

Microsoft促使开发人员使用此方法 - Composite Application Guidance for WPF and Silverlight

答案 6 :(得分:2)

我通常遵循向插件添加IConfigurable(或其他)类型接口的方法。但是,我不认为我会让它显示它自己的GUI。相反,允许它公开您在自己的GUI中正确呈现的设置集合,以便配置插件。该插件应该能够使用默认设置加载。但是,在配置之前不需要正常工作。基本上,您最终会加载插件,其中一些插件具有设置管理器的插件。那有意义吗?您的设置管理器可以保留所有插件的设置(通常用于编程数据或用户数据),并可以在应用程序启动时注入这些值。

“设置”界面需要包含呈现它所需的信息。这将包括类别,标题,描述,帮助ID,小部件,小部件参数,数据范围等。我通常使用某种形式的分隔标题来分隔成子类别。我还发现有一个有效属性和“应用值与未应用值”支持允许取消所有设置更改的表单被删除是有帮助的。在我当前的产品中,我有Real,Bool,Color,Directory,File,List,Real和String类型的设置。它们都继承自相同的基类。我还有一个渲染引擎,它将每个引擎绑定到一个匹配的控件上,并绑定每个参数上的各种参数。

答案 7 :(得分:2)

如何为实现共享接口的每个类创建自定义配置部分?

类似的东西:

<configuration>
  <configSections>
     <add name="myCustomSection" type="MySharedAssembly.MyCustomSectionHandler, MySharedAssembly"/>
  </configSections>
  <myCustomSection>
    <add name="Implementation1" 
         type="Assembly1.Implementation1, Assembly1"/>
         settings="allowRead=false;allowWrite=true;runas=NT AUTHORITY\NETWORK SERVICE"/>
    <add name="Implementation2" 
         type="Assembly1.Implementation2, Assembly1"/>
         settings="allowRead=false;allowWrite=false;runas=NT AUTHORITY\LOCAL SERVICE"/>
  </myCustomSection>
  ....

使用Initialize(IDictionary<string,string> keyValuePairs)方法,将配置节名称传递给它。它可以使用

加载其关联的部分
var section = ConfigurationManager.GetSection("/myCustomSection") as MyCustomSection

foreach( var addElement in section.Names ) 
{
    var newType = (IMyInterface)Activator.CreateInstance(addElement.Type);
    var values =  (from element in addElement.Value.Split(';')
                  let split = element.Split('=')
                  select new KeyValuePair<string,string>(split[0], split[1]))
                  .ToDictionary(k => k.Key, v => v.Value);

    newType.Initialize( values );
    //... do other stuff
}

答案 8 :(得分:1)

我建议看看Castle Windsor DI / IoC框架如何处理您所需的额外要求,并考虑使用它。

但一般情况下,您的插件应该支持接口,属性或构造函数,您可以通过它从公共源(例如app.config文件)注入配置对象。

答案 9 :(得分:1)

Mono.Addins是一个非常好的库,用于实现任何类型的插件(甚至可以从网站更新自己的插件)。 MEF也可以做到这一点,但它的水平有点低。但MEF将成为.NET 4的一部分。

答案 10 :(得分:1)

MEF是创建可扩展应用程序(插件体系结构)的最佳选择。

对于较小的应用程序,另一种方法是使用反射,我们可以采用可插拔的设计。 This的文章讲得很好。

答案 11 :(得分:0)

我将回答有关GUI的问题,其中每个插件都有一个标签。 基本上,我为我的WPF控件选项卡附加了一个行为类,然后针对每个插件进行迭代,其中包含一个viewmodel包装器,其中包含 IsEnabled 属性和方法 Icon()在其界面内。

行为类

    class TabControlBehavior : Behavior<TabControl>
{
    public static readonly DependencyProperty PluginsProperty =
       DependencyProperty.RegisterAttached("Plugins", typeof(IEnumerable<PluginVM>), typeof(TabControlBehavior));


    public IEnumerable<PluginVM> Plugins
    {
        get { return (IEnumerable<PluginVM>)GetValue(PluginsProperty); }
        set { SetValue(PluginsProperty, value); }
    }

    protected override void OnAttached()
    {
        TabControl tabctrl = this.AssociatedObject;

        foreach (PluginVM item in Plugins)
        {
            if (item.IsEnabled)
            {

                byte[] icon = item.Plugin.Icon();
                BitmapImage image = new BitmapImage();

                if (icon != null && icon.Length > 0)
                {
                    image = (BitmapImage)new Yemp.Converter.DataToImageConverter().Convert(icon, typeof(BitmapImage), null, CultureInfo.CurrentCulture);
                }

                Image imageControl = new Image();

                imageControl.Source = image;
                imageControl.Width = 32;
                imageControl.Height = 32;

                TabItem t = new TabItem();

                t.Header = imageControl;
                t.Content = item.Plugin.View;

                tabctrl.Items.Add(t);

            }                           
        }
    }

    protected override void OnDetaching()
    {

    }
}

插件界面

public interface IPlugin
{
    string Name { get; }

    string AuthorName { get; }

    byte[] Icon();

    object View { get; }
}

和PluginVM类

public class PluginVM : ObservableObjectExt
{
    public PluginVM(IPlugin plugin)
    {
        this.Plugin = plugin;
        this.IsEnabled = true;
    }

    public IPlugin Plugin { get; private set; }

    private bool isEnabled;

    public bool IsEnabled
    {
        get { return isEnabled; }
        set {
            isEnabled = value;
            RaisePropertyChanged(() => IsEnabled);
        }
    }

}

tabcontrol将仅显示已启用的插件及其Icon()方法返回的自定义图标。

插件实现中的Icon()方法的代码应如下所示:

public byte[] Icon()
{
    var assembly = System.Reflection.Assembly.GetExecutingAssembly();
    byte[] buffer = null;
    using (var stream = assembly.GetManifestResourceStream("MyPlugin.icon.png"))
    {
        buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
    }
    return buffer;
}