WinForms - Aplikace s podporou pluginů
Co tento článek ukazuje
- Jak tvořit aplikace rozšiřitelné pomocí pluginů
- Jak načítat vlastní nastavení z konfiguračního souboru
- Jak vytvářet objekty dynamicky pomocí reflection
Ú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 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í.