Подтвердить что ты не робот

Архитектура плагина с графическим интерфейсом

Я разрабатываю приложение, которое сильно использует плагины. Приложение находится на С#, и я думаю о создании графического интерфейса конфигурации в WPF. Я получил архитектуру плагина с точки зрения того, как управлять самими плагинами. Тем не менее, каждый плагин имеет свою собственную конфигурацию и там, где я ищу помощь.

Архитектура плагина проста - есть интерфейс, который реализуют плагины, и я просто загружаю все плагины внутри каталога. Однако, где плагины получают свою конфигурацию? Я хотел бы иметь общий способ обработки этого, так что каждый плагин не отвечает за чтение собственного файла конфигурации. Кроме того, я хотел бы, чтобы графический интерфейс расширялся с каждым плагином, то есть каждый плагин должен добавить вкладку в графический интерфейс с определенными параметрами конфигурации для этого плагина. Затем после сохранения файлы конфигурации будут сохранены.

Какой мой лучший способ сделать это?

4b9b3361

Ответ 1

Определите интерфейс для настраиваемых плагинов:

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 для настраиваемых плагинов.

Если вы хотите, вы можете заставить интерфейс работать в обратном порядке, заставляя основное приложение предоставить контейнер (например, фрейм или док-станцию) для плагина, но я бы порекомендовал другой способ.

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);
}

конечно, этот параметр является более ограничительным и более безопасным, поскольку вы можете полностью отличить, как плагин может взаимодействовать с окном конфигурации.

Ответ 2

То, как я выполнил свою реализацию для обработки точно такой же проблемы (поскольку DLL файлы не могут читать их файлы конфигурации) было то, что я определил два интерфейса один для плагина IPlugin и один для хоста 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");
     }
}

IHost может затем предоставить любые общие функции, которые вам нужны между всеми вашими плагинами (как конфигурация чтения и записи), а плагины также более осведомлены о вашем хост-приложении, не будучи заблокированным для конкретного хост-приложения. Например, вы можете написать приложение веб-службы и одно приложение Forms, которое использует тот же API-интерфейс плагина.

Другим способом было бы инициализировать плагин с информацией о конфигурации, прочитанной хост-приложением, однако у меня возникла необходимость в том, чтобы плагин мог читать/записывать его конфигурацию и выполнять другие действия по требованию.

Ответ 3

Взгляните на Composite WPF. Используя что-то вроде этого, вы должны иметь каждую вставку для определения вкладок. Тогда приложение оболочки будет отвечать только за загрузку каждого модуля в соответствующий модуль.

Ответ 4

Как я вижу, этот вопрос лучше всего решать как две отдельные проблемы.

  • Включите каждый плагин, чтобы указать диалог конфигурации (или что-то подобное, возможно, в виде вкладок, как вы предположили).
  • Предоставить централизованный файл конфигурации/базу данных и API для плагинов для хранения их сведений о конфигурации.

Разрешить каждому плагину задавать диалог конфигурации

Основной подход заключается в том, чтобы каждый плагин определял свойство, которое вернет Panel для хоста для отображения на вкладке.

interface IPlugin
{
    ...

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

Хост может создать вкладку для каждого загруженного плагина, а затем поместить каждую панель конфигурации плагина на правильную вкладку.

Здесь много возможностей. Например, плагин может сигнализировать, что у него нет параметров конфигурации, возвращая null. Хозяин, конечно, должен будет проверить это и решить, создавать ли вкладку.

Другим подобным подходом является определение метода вместо свойства. В этом случае метод будет вызываться каждый раз, когда отображается диалоговое окно конфигурации, и будет возвращать новый экземпляр Panel. Это, вероятно, будет медленнее, и это не очень хорошая идея, если подход свойств не вызывает проблем с добавлением/удалением элемента в визуальное дерево несколько раз. У меня не было этих проблем, поэтому вы должны быть хорошо с имуществом.

Возможно, нет необходимости обеспечивать такую ​​гибкость. Вы могли бы, как уже упоминалось, предложить API для плагинов для создания своего диалогового окна конфигурации из таких полей, как текстовые поля, флажки, комбинированные поля и т.д. Это, вероятно, окажется более сложным, чем простое свойство Panel.

Вероятно, вам также понадобится обратный вызов для плагина, когда конфигурация будет закрыта, сообщив ему сохранить свои данные конфигурации, и я скоро приму это. Вот пример:

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();
}

Предоставление центральной базы данных конфигурации

Это может быть разрешено API следующим образом:

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

    static object Retrieve(string key);
}

Затем хост будет контролировать чтение и запись конфигурации на диск.

Пример использования плагина:

Configuration.Store("Maximum", 10);
int max = (int)Configuration.Retrieve("Maximum");

Это позволит данным конфигурации действовать как словарь. Вероятно, вы сможете использовать сериализацию для хранения произвольных объектов в файле. Кроме того, вы можете ограничить конфигурацию хранением строк или чего-то подобного. Это будет зависеть от того, какой тип конфигурации необходимо сохранить.

Простейший подход имеет некоторые проблемы с безопасностью. Плагины смогут читать и изменять данные других плагинов. Однако, если хост префикс ключей с помощью идентификатора плагина или чего-то подобного, вы сможете эффективно разделить конфигурацию на каждый отдельный контейнер плагина.

Ответ 5

Проблема с тем, что каждый плагин не должен читать собственный файл конфигурации, заключается в том, что у вас должен быть один центральный файл, который знает, какие параметры конфигурации должны включать; это противоречит концепции плагина, где я даю вам плагин, и вы ничего не знаете об этом.

Это не то же самое, конечно, как центральный файл конфигурации с информацией о загрузке плагинов.

В отличие от того, что говорили другие, есть способы, чтобы библиотеки 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, другие предложения в значительной степени прибивают его... выбор заключается в том, хотите ли вы передать управление приложениям плагину или запросить плагин для закладки и добавить его.

Как правило, вы увидите, что приложение передает объект плагину... caveat - это объект, над которым плагин разрешен полный контроль. то есть. вы сами не проходите сам элемент управления вкладками, но вы могли бы создать приложение на закладке и передать его плагину, в основном говоря "сделайте то, что вы хотите с ним".

Здесь аргумент 'return 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 Explorer

Вы называете это, исследователь получил его. Любое приложение может добавлять страницы свойств к любому типу файла, меню правой кнопки мыши динамически запрашивает библиотеки DLL для записей... если вы хотите, чтобы были видны гибкие плагины, прочитайте в программе Shell Programming. Некоторые превосходные статьи в Code Project... Часть V рассказывает о добавлении вкладок в диалоговое окно страницы свойств для файлов.

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

.

Ответ 7

Я обычно следую подходу добавления к моему плагину IConfigurable (или любого другого) типа интерфейса. Тем не менее, я не думаю, что позволю ему показать свой собственный графический интерфейс. Скорее, позвольте ему предоставить набор настроек, которые вы соответствующим образом визуализируете в своем собственном графическом интерфейсе для настройки плагинов. Плагин должен иметь возможность загружать настройки по умолчанию. Однако он не должен функционировать должным образом, прежде чем он будет настроен. По сути, вы в конечном итоге загружаете плагины, где у некоторых плагинов есть плагины для менеджера настроек. Имеет ли это смысл? Менеджер настроек может сохранять настройки для всех плагинов (как правило, для программирования данных или пользовательских данных) и может вводить эти значения при запуске приложения.

Интерфейс "настройки" должен содержать информацию, необходимую для ее отображения. Это будет включать категорию, название, описание, идентификатор справки, виджет, параметры виджета, диапазоны данных и т.д. Я обычно использовал какую-то форму разделительного названия для разделения на подкатегории. Я также считаю полезным использовать свойство Valid и "прикладное значение по сравнению с непримененным значением", чтобы разрешить отмену формы, в которой все измененные настройки были отброшены. В моем текущем продукте у меня есть настройки типа Real, Bool, Color, Directory, File, List, Real и String. Все они наследуются от одного базового класса. У меня также есть механизм рендеринга, который связывает каждого с подходящим элементом управления и связывает различные параметры на каждом.

Ответ 8

Как создать пользовательский раздел конфигурации для каждого класса, который реализует общий интерфейс?

Что-то вроде:

<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
}

Ответ 9

Я предлагаю взглянуть на то, как Castle Windsor Система DI/IoC обрабатывает дополнительные требования, которые вам нужны, и, возможно, подумайте об использовании Это.

Но в целом ваши плагины должны поддерживать интерфейс, свойство или конструктор, с помощью которого вы можете вводить объект конфигурации из общего источника, такого как ваш файл app.config.

Ответ 10

Mono.Addins - очень хорошая библиотека для реализации плагинов любого типа (даже те, которые могут обновляться с веб-сайта). MEF также может выполнить это, но это немного ниже уровня. Но MEF будет частью .NET 4.

Ответ 11

Я отвечу на вопрос о 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;
}