c# .net plugins

c# - Arquitectura de plugin con GUI



.net plugins (11)

Estoy desarrollando una aplicación que hace un uso intensivo de los complementos. La aplicación está en C # y estoy pensando en crear la GUI de configuración en WPF. Obtuve la arquitectura de los complementos en términos de cómo administrar los propios complementos reales. Sin embargo, cada complemento tiene su propia configuración, y ahí es donde busco ayuda.

La arquitectura de los complementos es simple: hay una interfaz que implementan los complementos y yo solo cargo todos los complementos dentro de un directorio. Sin embargo, ¿de dónde obtienen su configuración los complementos? Me gustaría tener alguna forma genérica de manejar esto, para que cada complemento no sea responsable de leer su propio archivo de configuración. Además, me gustaría que la GUI se expanda con cada complemento, es decir, cada complemento instalado debe agregar una pestaña a la GUI con las opciones de configuración específicas para ese complemento. Luego, al guardar, los archivos de configuración se guardarán.

¿Cuál es mi mejor manera de hacer esto?


¿Qué pasa con la creación de una sección de configuración personalizada para cada clase que implementa la interfaz compartida?

Algo como:

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

Tenga un Initialize(IDictionary<string,string> keyValuePairs) , al cual le pasa el nombre de la sección de configuración. Puede cargar su sección asociada luego usando

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 }


A mi modo de ver, esta pregunta se resuelve mejor como dos problemas separados.

  1. Habilite cada complemento para especificar un cuadro de diálogo de configuración (o algo similar, tal vez en forma de pestañas, como sugirió).
  2. Proporcione un archivo / base de datos de configuración centralizada y una API para que los complementos almacenen su información de configuración.

Permitiendo que cada plugin especifique un diálogo de configuración

El enfoque básico aquí es que cada complemento defina una propiedad que devolverá un Panel para que el host lo muestre en una pestaña.

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

El host podría crear una pestaña para cada complemento cargado y luego colocar el panel de configuración de cada complemento en la pestaña correcta.

Hay muchas posibilidades aquí. Por ejemplo, un complemento podría indicar que no tiene ninguna opción de configuración devolviendo null . El anfitrión, por supuesto, deberá verificar esto y decidir si desea crear una pestaña.

Otro enfoque similar es definir un método en lugar de una propiedad. En este caso, el método se llamará cada vez que se muestre el cuadro de diálogo de configuración y devolverá una nueva instancia del Panel. Esto probablemente sería más lento y no sería una buena idea a menos que el enfoque de propiedad cause problemas al agregar / eliminar un elemento al árbol visual varias veces. No he tenido esos problemas, por lo que debe ser bueno con una propiedad.

Puede que no sea necesario proporcionar esta flexibilidad. Podría, como se ha mencionado, ofrecer una API para que los complementos construyan su cuadro de diálogo de configuración desde campos como cuadros de texto, cuadros de verificación, cuadros combinados, etc. Esto probablemente terminaría siendo un enfoque más complicado que una simple propiedad del Panel .

Es probable que también necesite una devolución de llamada al complemento cuando se cierre la configuración, indicándole que guarde sus datos de configuración, a los que llegaré pronto. Aquí hay un ejemplo:

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

Proporcionar una base de datos de configuración central

Esto podría resolverse mediante una API similar a la siguiente:

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

El host entonces controlaría la lectura y escritura de la configuración en el disco.

Ejemplo de uso del plugin:

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

Esto permitiría que los datos de configuración actúen como un diccionario. Es probable que pueda utilizar la serialización para almacenar objetos arbitrarios en un archivo. Alternativamente, podría restringir la configuración para almacenar cadenas o algo similar. Dependería de qué tipo de configuración se deba almacenar.

El enfoque más simple tiene algunos problemas de seguridad. Los complementos podrían leer y modificar los datos de otros complementos. Sin embargo, si el host prefiere las claves por una ID de complemento o algo similar, sería capaz de dividir efectivamente la configuración en el contenedor separado de cada complemento.


Defina una interfaz para complementos configurables:

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

Simplemente invoca la interfaz IConfigurable para los complementos configurables.

Si lo desea, puede hacer que la interfaz funcione de la otra manera, haciendo que la aplicación principal proporcione un contenedor (un marco o una base de acoplamiento, por ejemplo) para que el complemento también se sienta, pero yo recomendaría lo contrario.

public interface IConfigurable { void LoadConfig(string configFile); void ShowConfig(DockPanel configurationPanel); }

Finalmente, puede hacerlo de la manera más difícil, definiendo exactamente lo que el complemento puede ofrecer como opción de configuración.

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

Por supuesto, esta opción es la más restrictiva y más segura, ya que puede limitar perfectamente la forma en que el complemento puede interactuar con la ventana de configuración.



El problema de no querer que cada complemento lea su propio archivo de configuración es que debe tener un archivo central que sepa qué ajustes de configuración incluir; esto va en contra del concepto de un complemento, donde te doy un complemento y no sabes nada al respecto.

Eso no es lo mismo, por supuesto, como un archivo de configuración central con información sobre qué complementos cargar.

Contrariamente a lo que otros han dicho, hay formas de hacer que las DLL lean la configuración de configuración desde su propio archivo de configuración, pero si específicamente no quieres hacer eso, no entraré en ellas.

Me sorprende que nadie haya mencionado el registro. Es el secreto familiar que todos quieren mantener encerrados en el sótano, pero eso es exactamente para lo que estaba destinado: una ubicación centralizada para almacenar configuraciones de aplicaciones a las que cualquier persona (es decir, cualquier complemento) puede acceder.


Sin embargo. Si desea que la aplicación principal acceda y almacene las configuraciones del complemento, la mejor manera (IMO) es tener una interfaz para consultar las propiedades del complemento (y devolverlas). Si lo mantiene genérico, puede reutilizarlo para otras aplicaciones.

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


La aplicación principal consultaría a cada complemento para un conjunto de propiedades, luego agregaría / actualizaría las configuraciones a su archivo de configuración, como tal:

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


Todavía no he probado eso, solo lo tiré rápido en unos pocos momentos libres; Lo comprobaré más tarde para asegurarme de que no haya errores tontos. Pero se entiende la idea.

En cuanto a la expansión de la GUI, otras sugerencias lo han determinado bastante ... la opción es si desea pasar el control de la aplicación al complemento, o consultar el complemento para una página de pestaña y agregarlo.

Por lo general, vería que la aplicación pasa un objeto al complemento ... la advertencia es que debe ser un objeto sobre el cual se le permita al control completo . IE no pasaría el control de pestañas en sí mismo, pero podría hacer que la aplicación cree la página de pestañas y se la pase al complemento, diciendo básicamente "haga lo que quiera con él".

El argumento de ''devolver nulo si no hay pestaña'' tiene cierto peso aquí, pero prefiero que la aplicación maneje la creación y limpieza de todos los objetos que usa. No puedo darle una buena razón por la cual ... una de esas preferencias de estilo arbitrarias, tal vez. No criticaría ninguno de los dos enfoques.

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,
James


Pensé PS : mencionaría a la madre de todas las aplicaciones de complementos: Explorador de Windows

Lo que sea, el explorador lo tiene. Cualquier aplicación puede agregar páginas de propiedades a cualquier tipo de archivo, los menús de clic derecho consultan dinámicamente los archivos DLL para obtener entradas ... si desea una perspectiva de qué tan flexibles pueden ser los complementos, lea la Programación de Shell. Algunos artículos excelentes en Code Project ... Parte V se refieren a agregar pestañas al cuadro de diálogo de la página de propiedades para los archivos.

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

.


La forma en que hice mi implementación para manejar exactamente el mismo problema (ya que los archivos dll no pueden leer sus archivos de configuración) fue que IPlugin dos interfaces, una para el complemento IPlugin y otra para el host IHost . El complemento se creó e inicializó con una referencia al host (implementando la interfaz 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 puede proporcionar las funciones comunes que necesite entre todos sus complementos (como configuración de lectura y escritura), y los complementos también son más conscientes de su aplicación host, sin estar bloqueados a una aplicación host específica. Por ejemplo, podría escribir una aplicación de servicio web y una aplicación de formularios que use el mismo complemento API.

Otra forma sería inicializar el complemento con la información de configuración leída por la aplicación host, sin embargo, tuve la necesidad de que el complemento pudiera leer / escribir su configuración y realizar otras acciones, a pedido.


Le sugiero que analice cómo el marco DI / IoC de Castle Windsor maneja los requisitos adicionales que busca, y quizás considere usarlo.

Pero en general, sus complementos deben ser compatibles con una interfaz, una propiedad o un constructor mediante el cual puede inyectar un objeto de configuración desde una fuente común, como su archivo app.config.



Mono.Addins es una muy buena biblioteca para implementar complementos de cualquier tipo (incluso los que pueden actualizarse desde un sitio web). MEF también puede lograr esto, pero es un nivel un poco más bajo. Pero MEF va a ser parte de .NET 4.


Por lo general, sigo el enfoque de agregar un tipo de interfaz IConfigurable (o lo que sea) a mi complemento. Sin embargo, no creo que lo deje mostrar su propia GUI. En su lugar, permita que exponga una colección de configuraciones que usted representa adecuadamente en su propia GUI para configurar los complementos. El complemento debe poder cargar con la configuración predeterminada. Sin embargo, no necesita funcionar correctamente antes de que se haya configurado. Esencialmente, terminas cargando complementos donde algunos complementos tienen complementos para el administrador de configuración. ¿Tiene sentido? Su administrador de configuración puede conservar la configuración de todos los complementos (generalmente para programar datos o datos de usuario) y puede inyectar esos valores en el inicio de la aplicación.

Una interfaz de "configuración" debería contener la información necesaria para representarla. Esto incluirá la categoría, el título, la descripción, la ID de ayuda, el widget, los parámetros del widget, los rangos de datos, etc. Comúnmente he usado alguna forma de título delimitado para separar en subcategorías. También me parece útil tener una propiedad válida y un soporte de "valor aplicado vs. valor no aplicado" para permitir la cancelación de un formulario donde se descartan todas las configuraciones modificadas. En mi producto actual tengo configuraciones de tipo Real, Bool, Color, Directorio, Archivo, Lista, Real y Cadena. Todos ellos heredan de la misma clase base. También tengo un motor de renderizado que vincula a cada uno con un control coincidente y une los distintos parámetros de cada uno.


Responderé a la pregunta sobre si la GUI tiene una pestaña para cada complemento. Básicamente, he adjuntado una clase de comportamiento para mi pestaña de control de WPF y luego he iterado para cada complemento, que tiene un envoltorio de viewmodel con una propiedad IsEnabled y un método Icon () declarado dentro de su interfaz.

la clase de comportamiento

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

la interfaz del plugin

public interface IPlugin { string Name { get; } string AuthorName { get; } byte[] Icon(); object View { get; } }

y la clase 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); } } }

El control de tabulación mostrará solo los complementos habilitados con su ícono personalizado devuelto por el método Icon ().

El código del método Icon () dentro de la implementación del complemento debería tener este aspecto:

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