Pokud programujete pro platformu .NET Framework a chcete se dozvědět, jak jednoduše implementovat do své aplikace podporu pluginů, čtěte dál! Vše si ukážeme a vysvětlíme na názorném příkladě.
Platforma NET Framework je svou architekturou a jednoduchostí při manipulaci s assembly (dll knihovnami) přímo ideální pro vytváření aplikací modulárně. Určitě i vy poznáte, jak jednoduché to celé je. Předpokládám však alespoň základní znalosti programování v NET Frameworku. Takže dejme se do práce.
Celý postup budu demonstrovat na našem velmi jednoduchém příkladu modulární aplikace PluginApp. Pokud jste registrovaní uživatelé, můžete si celý příklad i s pluginy stáhnout jako projekt do Visual Studia 2003.NET nebo Visual Studia 2005 (Express) v sekci Download. Zdrojové kódy jsou podrobně okomentovány.
Trochu teorie
Ná následujícím schématu si popíšeme, jak vlastně taková aplikace funguje.
Aplikaci tvoří mimo jiné hlavní formulář, který obsahuje jádro. Jádro komunikuje s hlavním formulářem a pluginy. Je tedy jakýmsi sprostředkovatelem mezi pluginy a hlavním formulářem aplikace. Dále si ukážeme, že nám poslouží i ke komunikaci mezi jednotlivými pluginy.
Praxe
Nejdříve ze všeho si musíme nadefinovat, přes jaké rozhraní bude probíhat komunikace. Každý plugin bude muset implementovat rozhraní IPLuginBase.
///
/// Základní rozhraní všech pluginů. Přes toto rozhraní jádro komunikuje s pluginy.
///
public interface IPluginBase
{
//První metoda, kterou jádro volá a předává i instanci jádra.
void Load(IApplicationBase app);
//Touto metodou předá jádro pluginu pole všech pluginů. Díky tomu může probíhat komunikace mezi pluginy.
void PluginsLoaded(IPluginBase[] plugins);
//Demonstrační metoda, kterou jádro volá po kliknutí v hlavním menu aplikace.
void DelejNeco();
//Metody sloužící k vzájemné komunikaci mezi pluginy.
object Metoda1(object param);
object Metoda2(object param);
//Vlastnost určující název pluginu.
string Nazev
{
get;
}
//Vlastnost určující titulek pluginu.
string Titulek
{
get;
}
}
Metoda Load je volána jako první a předá pluginu instanci jádra (přesněji řečeno rozhraní přes, které bude s jádrem komunikovat), kterou si pak uložíme jako statický člen. Díky tomu pak můžeme z každého místa v pluginu komunikovat s jádrem. Metoda PluginsLoaded je volána, jakmile u všech pluginů proběhla metoda Load. Získáme tím pole rozhraní, které si taktéž uložíme do statického členu a komunikujeme s ostatními pluginy. Metody Metoda1 a Metoda2 jsou více méně dobrovolné. Plugin se implementovat nemusí (jednoduše vrací null) a jádro tyto metody nikdy volat nebude. Slouží výhradně k nějaké speciální funkčnosti, kterou mohou využívat ostatní pluginy.
Nyní si nadefinujeme rozhraní pro komunikaci s jádrem IApplicationBase.
///
/// Rozhraní, kterým budou puginy komunikovat s jádrem aplikace.
///
public interface IApplicationBase
{
void Maximalizuj();
void Ukonci(string duvod);
}
Metoda Maximalizuj maximalizuje hlavní okno aplikace a metoda Ukonci ukončí aplikaci s hlášením obsahujícím důvod.
Tyto dvě rozhraní zkompilujeme do dynamické knihovny PluginBase.dll, která bude takovým „prostředníkem“ mezi pluginy a hlavním exe souborem.
Nyní vytvoříme třídu jádra ApplicationCore, která implementuje rozhraní IApplicationBase.
///
/// Základní třída celé aplikace. Díky ní mohou komunikovat pluginy s jádrem.
///
public class ApplicationCore : IApplicationBase
{
public delegate void MaximalizujEventHandler(object sender, EventArgs e);
public event MaximalizujEventHandler MaximalizujEvent;
public ApplicationCore()
{
}
///
/// Implementace metody Ukonci, kterou mohou volat všechny pluginy.
///
public void Ukonci(string duvod)
{
MessageBox.Show("Aplikace bude ukončena: "+duvod, "PluginApp", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Application.Exit();
}
///
/// Implementace metody Maximalizuj, kterou mohou volat všechny pluginy.
///
public void Maximalizuj()
{
OnMaximalizuj(this);
}
///
/// Metoda vyvolávající údálost MaximalizujEvent.
///
protected void OnMaximalizuj(object sender)
{
if(MaximalizujEvent != null)
MaximalizujEvent(sender, new EventArgs());
}
}
Metoda Ukonci jednoduše ukončí celou aplikaci. Ale metoda Maximalizuj to má složitější. Nemůže se přímo dostat k hlavnímu formuláři, takže si nadefinujeme příslušnou událost, na kterou bude formulář reagovat.
Následuje hlavní formulář MainForm (aby zde byl zdrojový kód přehlednější, vypustil jsme kód generovaný designerem).
///
/// Hlavní formulář aplikace.
///
public class MainForm : System.Windows.Forms.Form
{
//Statická instance jádra.
public static ApplicationCore app;
//Statický seznam načtených pluginů.
public static ArrayList plugins;
.............................
.............................
private void MainForm_Load(object sender, System.EventArgs e)
{
//Inicializace seznamu pluginů.
plugins = new ArrayList();
//Inicializace instance jádra.
app = new ApplicationCore();
//Přihlášení k odběru události MaximalizujEvent.
app.MaximalizujEvent += new PluginApp.ApplicationCore.MaximalizujEventHandler(app_MaximalizujEvent);
//Načtení pluginů.
LoadPlugins();
//Vytvoření položky v hlavním menu pro každý plugin.
CreateMenu();
}
///
/// Obsluha údálosti MaximalizujEvent.
///
private void app_MaximalizujEvent(object sender, EventArgs e)
{
WindowState = FormWindowState.Maximized;
}
///
/// Vytvoření položky v hlavním menu pro každý plugin.
///
private void CreateMenu()
{
//Ověříme si, zda-li má vůbec smysl menu "Pluginy" vytvářet.
if(plugins.Count > 0)
{
//Vytvoříme menu "Pluginy"
MenuItem menuItem = new MenuItem();
menuItem.Text = "Pluginy";
//Pro každý plugin vytvoříme novou položku
foreach(IPluginBase plugin in plugins)
{
MenuItem pluginItem = new MenuItem();
pluginItem.Text = plugin.Nazev;
//Událost "Click" všech položek pluginů obsluhuje jedinná metoda.
pluginItem.Click += new EventHandler(pluginItem_Click);
menuItem.MenuItems.Add(pluginItem);
}
mainMenu.MenuItems.Add(1, menuItem);
}
}
///
/// Načtení pluginů.
///
private void LoadPlugins()
{
//Adresář, kde jsou naše pluginy uloženy.
string pluginDir = Application.StartupPath+@"\Plugins";
if(Directory.Exists(pluginDir))
{
//Zajímají nás pouze dll knihovny.
string[] files = Directory.GetFiles(pluginDir, "*.dll");
for(int i = 0; i < files.Length; i++)
{
//Knihovnu "PluginBase.dll" ignorujeme.
if(files[i] != pluginDir+@"\PluginBase.dll")
{
try
{
//Vytvoříme instanci třídy Assembly.
Assembly asm = Assembly.LoadFile(files[i]);
//Vytvoříme instanci třídy "Plugin", která musí implementovat rozhraní "IPluginBase".
IPluginBase plugin = (IPluginBase)asm.CreateInstance("PluginApp.Plugin");
//Pokud se nám podařilo získat rozhraní ke komunikaci s pluginem, přidáme jej do seznamu.
if(plugin != null)
plugins.Add(plugin);
}
catch(Exception e)
{
MessageBox.Show("Chyba při načítání pluginu "+files[i]+": "+e.Message, "PluginApp", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
//Zavoláme metodu "Load" všech pluginů.
foreach(IPluginBase plugin in plugins)
{
plugin.Load(app);
}
//Vytvoříme si pole rozhraní všech pluginů, abychom jej pohli předat ostatním pluginům.
IPluginBase[] pluginy = new IPluginBase[plugins.Count];
for(int x = 0; x < plugins.Count; x++)
{
pluginy[x] = (IPluginBase)plugins[x];
}
//Jakmile jsou všechny pluginy inicializovány, předáme každému z nich pole rozhraní ke vzájemné komunikaci.
foreach(IPluginBase plugin in plugins)
{
plugin.PluginsLoaded(pluginy);
}
label1.Text = "Počet pluginů: "+plugins.Count.ToString();
}
///
/// Metoda obsluhující události "Click" položek všech pluginů.
///
private void pluginItem_Click(object sender, EventArgs e)
{
//K rozeznání, ke kterému pluginu tato položka patří nám poslouží vlastnost Text.
string pluginName = ((MenuItem)sender).Text;
//Projdeme seznam pluginů.
foreach(IPluginBase plugin in plugins)
{
//Nalezneme plugin, kterého se tato událost týká.
if(plugin.Nazev == pluginName)
{
//Zavoláme samotný plugin.
plugin.DelejNeco();
}
}
}
}
Máme zde tedy statický člen jádra aplikace (třídy ApplicationCore) a statický seznam načtenách pluginů (ArrayList obsahující rozhraní IPluginBase). Metoda LoadPlugins si nejprve ověří, zda-li existuje adresář obsahující pluginy (každý plugin je tvořen jedinou dll knihovnou v daném adresáři). Dále si z instance třídy Assembly vytvoříme instanci rozhraní IPluginBase (každý plugin musí obsahovat třídu Plugin implementující rozhraní IPluginBase ve jmeném prostoru PluginApp). Ověříme si, zda-li se nám to povedlo, a přidáme si ji do seznamu. Zavoláme metody Load všech pluginů. Vytvoříme si pole rozhraní všech pluginů a každému pluginu jej pak předáme. Na ukázku si pro každý plugin vytvoříme položku v hlavním menu, kterou vyvoláme metodu DelejNeco.
Tak, a nyní si vytvoříme nějaké pluginy.
public class Plugin : IPluginBase
{
public static IApplicationBase appBase;
public static ArrayList pluginy;
public Plugin()
{
pluginy = new ArrayList();
}
#region IPluginBase Members
public string Titulek
{
get
{
return "Maximalizuje okno";
}
}
public void Load(IApplicationBase app)
{
appBase = app;
}
public void PluginsLoaded(IPluginBase[] plugins)
{
pluginy.AddRange(plugins);
}
public object Metoda1(object param)
{
return null;
}
public object Metoda2(object param)
{
return null;
}
public string Nazev
{
get
{
return "PluginA";
}
}
public void DelejNeco()
{
//Volání metody Maximalizuj, kterou zprauje jádro.
appBase.Maximalizuj();
}
#endregion
}
Jako první je PluginA. Implementovali jsme rozhraní IPluginBase a metoda DelejNeco volá metodu jádra Maximalizuj. Zde jsou implementace této metody Pluginu B a C.
PluginB
public void DelejNeco()
{
//Volání metody Ukonci, kterou zprauje jádro.
appBase.Ukonci("Zkouška komunikace pluginu s jádrem");
}
PluginC
public void DelejNeco()
{
//Projdeme seznam všech pluginů.
foreach(IPluginBase plugin in pluginy)
{
//Ověříme si, zda-li se nejedná o náš vlastní plugin.
if(plugin.Nazev != Nazev)
{
MessageBox.Show("Spustí se metoda DelejNeco() pluginu "+plugin.Nazev);
//Zavoláme metodu DelejNeco všech pluginů.
plugin.DelejNeco();
}
}
}
Jak jistě vidíte, PluginC volá metody všech ostatních pluginů. Takže nám funguje i komunikace mezi pluginy.
Chtěl bych ještě podotknout, že zde rozhodně doporučuji zaregistrovat assembly PluginBase.dll do GAC.
Zde je výsledek našeho snažení:
Závěr
Tento příklad byl sestaven s ohledem na kompatibilitu mezi NET Frameworkem 1.1 a 2.0. Pro verzi 2.0 bych rozhodně doporučoval mimo jiné nahradit třídu ArrayList generickou třídou List. Pro skutečné nasazení by se měl vytvořit nějaký systém instalace a např. přidělování Id za běhu pluginům jádrem. To už je ale na každém z vás.
Doufám, že byl tento příklad přínosem a že zboří veškeré obavy z vytváření modulárních aplikací.