TP

WinForms - Aplikace s podporou pluginů

Co tento článek ukazuje

Ukázková aplikace

Úvod o pluginech (v .Netu)

Pluginy slouží k tomu, aby dodávali již hotové aplikaci nějakou novou funkcionalitu aniž by bylo potřeba mít přístup ke zdrojovým kódům aplikace a nějak do nich zasahovat. Aplikace, které podporují pluginy jsou snadno rozšiřitelné, což je velká výhoda, protože tak umožňují ostatním programátorům snadno vylepšovat aplikaci. Pěkným příkladem takto rozšiřitelné aplikace je Microsoft Visual Studio.Net, které samo o sobě neobsahuje prostředí pro žádný programovací jazyk, ale při instalaci si uživatel volí jaké rozšíření (programovací jazyky) chce instalovat. Krom toho je možné do Visual Studia nové vývojové nástroje doinstalovat i později. Jiným typickým příkladem pluginů jsou obrázkové efekty v grafických editorech.

Aby bylo vůbec možné tvořit pluginy pro nějakou aplikaci musí aplikace jasným způsobem určit jak bude s pluginem komunikovat. V případě grafického editoru musí tvůrci aplikace říct: "Já tímto způsobem předám bitmapu tobě, ty jí jak chceš uprav a vrať mi jí takhle a takhle." Pod platformou .Net lze toto rozhraní popsat pomocí bázové třidy (nebo rozhraní), od které budou odvozeny třídy jednotlivých pluginů. Tato bázová třída (případně rozhraní) musí být v oddělené dll knihovně, aby jí mohly používat pluginy bez závislosti na hlavní části aplikace. Bázová třída pro obrázkový efekt do grafického editoru by mohla vypadat takto:

// Trida je abstract, protoze samotna nic nemuze delat
public abstract class BaseEffect 
{
  // Tato metoda provede efekt s predanou bitmapou
  public abstract void DoEffect(Bitmap bitmapa);
}

Následující diagram znázorňuje jak je možné rozdělit aplikaci, která má podporovat rozšiřování pomocí pluginů do více knihoven (v .Netu assemblies). Bázová třída, která určuje podobu pluginů se nachází ve zvláštní assembly (Core.dll). Aplikace má referenci na tuto základní knihovnu (z ní si bere bázovou třidu pluginů) a při spuštění projde všechny pluginy (odvozené třídy od PluginBase) a pomocí reflection si za běhu vytvoří objekty pluginů.

Diagram ukazuje jak jsou třídy rozdělené mezi soubory
Diagram ukazuje jak jsou třídy rozdělené mezi knihovny

Pluginy v ukázkové aplikaci

V ukázkové aplikaci, kterou vytvoříme budou pluginy jednoduché hry, které bude možné spustit, zastavit a při zastavení vrátí nějaké hlášení o tom, jak si hráč vedl. Protože pluginy budou určovat i uživatelské rozhraní pro hru je bázová třída odvozená od WinForms ovládacího prvku (UserControl).

// Zakladni trida od ktere budou odvozene vsechny pluginy
// Tato trida sama o sobe nic nedela, proto je 'abstract' public class BaseControl : UserControl { // Spousti nejakou aktivitu, kterou plugin dela public virtual void Start() {} // Zastavuje aktivitu pluginu a vraci vysledek public virtual string Stop() { return ""; } // Vraci popis o cem v pluginu jde, ktery se zobrazuje pred spustenim public virtual string Description { get { return ""; } } }

Bázová třída obsahuje metodu Start, která spouští hru a metodu Stop, která hru ukončí a vrátí text s nějakým výsledkem. Dále obsahuje vlastnost Description, která bude v odvozených pluginech vracet popis hry. Navíc se ještě v pluginech používá metoda ToString (obsahuje jí každý objekt), která vrací jméno pluginu zobrazované např. v menu.

Narozdíl od ukázky uvedené na začátku článku (BaseEffect) třída není označená jako abstract. Důvodem pro tuto změnu je to, že designer ve Visual Studiu neumožňuje pracovat s abstraktními ovládacími prvky a proto by nebylo možné v odvozených pluginech používat designer pro tvorbu uživatelského rozhraní. Nebýt tohoto omezení, bylo by rozhodně lepší aby základní třida byla abstract.

Načítání a používání pluginů

Vytvoření pluginů, není nikterak složité. Stačí vytvořit nový projekt typu 'Class library' a v ní třídu odvozenou od BaseControl. Nyní se podíváme jak je možné pomocí reflection načíst takto vytvořené pluginy do aplikace. Toto načítání probíhá dynamicky a díky tomu je možné přidávat pluginy bez úpravy aplikace (nově nahrané pluginy se načtou při dalším spuštění).

Aplikace musí nejprve zjistit, ze kterých assemblies (dll knihoven) má načítat pluginy a jak se jmenujou třídy s pluginem v těchto knihovnách. Nejjednodužší metodou je načítat všechny dll knihovny v nějakém adresáři (např. Plugins) a v každé knihovně hledat plugin pojmenovaný Plugin (to že se jméno bude opakovat nevadí, protože se jedná o různé knihovny). Pokud tedy známe jméno souboru s pluginem (assemblyFile) a jméno třídy včetně namespace (className) lze plugin načíst takto:

using System.Reflection;

// Nacita plugin se jmenem className z assembly assemblyFile
BaseControl LoadPlugin(string assemblyFile, string classFile)
{
  // Nacte assembly z daneho souboru// a vytvori dynamicky objekt daneho typu
  Assembly asm=System.Reflection.Assembly.LoadFile(assemblyFile);
  object obj=asm.CreateInstance(className);

  // Pretypuje nacteny objekt na bazovy typ pluginu
  BaseControl plugin=(BaseControl)obj;
  return plugin;
}

Poté co je plugin tímto způsobem vytvořen není problém volat jeho metody, protože se jedná objekt odvozený od BaseControl a všechny metody (a také vlastnost Description), které je potřeba z aplikace volat jsou virtuální. Například popis pluginu lze tedy zobrazit takto:

Nejprve nacte plugin pomoci LoadPlugin..
BaseControl plugin=LoadPlugin("soubor.dll","Namespace.JmenoPluginu");
.. a pote zobrazi popis (vlastnost Description)
MessageBox.Show(plugin.Description);

Načítání nastavení z konfiguračního souboru

V .Net aplikacích je velmi vhodné ukládat nastavení aplikace do konfiguračního souboru, který se jmenuje jmenoaplikace.exe.config. Pro ukládání pluginů si v konfiguračním souboru vytvoříme vlastní sekci (pluginsSection), ve které bude toto nastavení uloženo. Pro přidání vlastní sekce s nastavením pluginů bude ještě potřeba vytvořit objekt, který bude toto nastavení v aplikaci načítat. Konfigurační soubor s nastavením pluginů vypadá takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  
  <configSections>
    <!-- Zde je urcen objekt pro nacitani sekce pluginsSection -->
    <section name="pluginsSection" 
      type="Plugins.Main.PluginsConfig,Plugins.Main" />
  </configSections>

  <pluginsSection>
    <!-- Nastaveni pluginu (assembly a jmeno tridy) -->
    <plugin assembly="Plugins.Main.exe" 
      class="Plugins.Main.KeyGamePlugin" />
    <plugin assembly="Plugins\Plugins.Demo.dll" 
      class="Plugins.Demo.MouseGamePlugin" />
  </pluginsSection>
  
</configuration>

Nastavení uložené v konfiguračním souboru bude v aplikaci načítáno pomocí objektu, který je již nastavený v konfiguračním souboru a jeho jméno je včetně namespace Plugins.­Main.­PluginsConfig. Tento objekt implementuje rozhraní IConfigurationSectionHandler, které říká, že tento objekt slouží k načítání uživatelských sekcí v konfiguračním souboru. Z tohoto rozhraní pochází metoda Create, která je volána .Net frameworkem při přístupu k nastavení (jako je tomu ve statické metodě GetSettings).

// Objekt pro nacitani sekce, ktera obsahuje nastaveni
// pro pluginy v konfiguracnim souboru aplikace public class PluginsConfig : IConfigurationSectionHandler { // Vytvori objekt PluginsConfig pro aktualni aplikaci public static PluginsConfig GetSettings() { return (PluginsConfig)ConfigurationSettings.GetConfig("pluginsSection"); } // Tato metoda je volana .net frameworkem pri volani
// (PluginsConfig)ConfigurationSettings.GetConfig("pluginsSection") public object Create(object parent, object configContext, XmlNode section) { PluginsConfig ret=new PluginsConfig(); foreach(XmlNode nd in section.SelectNodes("plugin")) { string assemblyName=nd.Attributes["assembly"].Value; string className=nd.Attributes["class"].Value; // Nacist plugin pomoci vyse popsaneho postupu..
// LoadPlugin(assemblyName, className); } } }

Možné problémy a rozšíření

Prvním problémem se kterým se můžete snadno setkat vznikne při změně knihovny obsahující bázovou třídu. Pokud některý z pluginů, používá starou verzi knihovny nebude možné přetypovat načtený objekt na typ bázové třídy. Přestože se bázová třída pluginu i ta na kterou chceme načtený objekt přetypovat jmenují stejně, jedná se o jiný typ (každý z jiné knihovny) a proto přetypování selže.

Pokud se pokusíte používat tuto architekturu v Asp.Net narazíte na problém způsobený tím, že Asp.Net si soubory aplikace kopíruje do různých dočasných adresářů a knihovny s pluginy nebudou moci nalézt správnou knihovnu s bázovou třídou. Toto lze vyřešit pomocí GAC (global assembly cache), kam je možné po digitálním podepsání nahrát knihovnu s bázovou třídou. Aplikace poté bude vždy načítat knihovnu z GAC.

Dalším možným problémem může být potřeba, aby plugin manipuloval nějakým způsobem s aplikací a měl přístup k některým z jejích částí (například můžete chtít pluginu umožnit přidávání pložek do menu). Zde je již potřeba trochu jiného přistupu, protože toto nelze snadno dosáhnout pomocí přidání nějaké metody do bázové třídy pluginu. Ideální by bylo aby plugin mohl volat metody z aplikace, ale vzhledem k tomu, že o aplikaci "neví", není to tak snadné. Možné řešení je vytvořit bázovou třídu - např. AppBase (která bude v knihovně se základní třídou pro pluginy) a ve této třídě vytvořit abstraktní metody pro všechny potřebné operace. V hlavní části aplikace je již možné implementovat tyto metody v odvozené třídě (např. AppObject odvozené od AppBase). Poté již stačí přidat do pluginu vlastnost typu AppBase a po vytvoření předat pluginu pomocí této vlastnosti objekt pro práci s aplikací.

Soubory na stažení

Published: Friday, 1 April 2005, 12:19 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: