System.Addin - seznamte se
Už několik let píšu rozšiřitelné aplikace dosti primitivním stylem - jedna sdílená assembly mezi hostující aplikaci a addiny. Ostatně tento způsob jsem popisoval v článku C# aplikace s podporou pluginů. V průběhu času se ukázalo jako zásadní problém řešit kompatibilitu - stačí pozměnit rozhraní a všechno jde do kytek. Také bylo velice obtížné zpracovávat chyby v addinech. Naštěstí nám vývojáři .NET Frameworku 3.5 nadělili řešení podobě tzv. pipeline, která vypadá takto:
Samotná pipeline se skládá ze segmentů. Obvykle představuje každý segment jednu assembly. Pipeline také vyžaduje následující strukturu adresářů:
Dle mého názoru se věci nejlépe vysvětlují na příkladě. Pro naše účely nám poslouží jednoduchá rozšířitelná konzolová aplikace.
Založíme si novou prázdnou Solution s názvem ExtensibleApp.
Krok 1: segment Contract
Jádrem pipeline je segment
Contract. Jedná se o neverzovatelný protokol popisující komunikaci mezi hostovací aplikací a addiny. V praxi se implementuje jako rozhraní, které dědí od
IContract. Dále musí být označeno atributem
AddInContract. Založme si tedy nový projekt typu
Class Library s názvem
ExtensibleApp.Contracts. Mějme na paměti požadovanou adresářovou strukturu, takže v nastavení projektu změníme
Output path na
..\output\Contracts.
Dále do projektu přidáme referenci na
System.AddIn a
System.AddIn.Contract. Assembly obsahuje jedno rozhraní:
using System.AddIn.Contract;
using System.AddIn.Pipeline;
namespace ExtensibleApp.Contracts
{
[AddInContract]
public interface IAddInContract : IContract
{
string OperationName { get; }
string Operate(string value);
}
}
Kontraktem lze přenášet pouze serializovatelné datový typy z mscorlib ( boo, int, string apod. ), výčtové typy ( buď definované v mscorlib nebo v kontraktu ) a jiné kontrakty.
Krok 2: segment HostView
Tento segment reprezentuje pohled hostující aplikace na addiny a musí se jednat o ne-sealed třídu ( popř. rozhraní ). Nemusí být obsažen ve speciální assembly a proto my ho vložíme do samotné host. aplikace. Založme tedy nový projekt typu
Console Application nazvaný
ExtensibleApp . V nastavení projektu upravíme
Output path na
..\output a přidáme referenci na
System.AddIn. Pohled host. aplikace vypadá následovně:
namespace ExtensibleApp
{
public abstract class AddIn
{
public abstract string OperationName { get; }
public abstract string Operate(string value);
}
}
Krok 3: segment AddInView
Vytvoříme nový projekt typu
Class Library nazvaný
ExtensibleApp.AddInViews a opět upravíme jeho nastavení -
Output path na
..\output\AddInViews . Taky nezapomeneme přidat referenci
System.AddIn. Samotný pohled addinů na hostující aplikaci se téměř shoduje s pohledem hostující aplikace na addiny. Rozdíl je v tom, že tento pohled musí být označen atributem
AddInBase.
using System.AddIn.Pipeline;
namespace ExtensibleApp.AddInViews
{
[AddInBase]
public abstract class AddIn
{
public abstract string OperationName { get; }
public abstract string Operate(string value);
}
}
Krok 4: segment HostSideAdapter
Nyní musíme nějak provázat kontrakt s našimi pohledy - to mají na starosti adaptéry, které adaptují pohled na kontrakt a naopak.
Opět založíme nový projekt typu
Class Library nazvaný
ExtensibleApp.HostSideAdapters. Nastavíme
Output path na
..\output\HostSideAdapters a přidáme reference na
System.AddIn,
System.AddIn.Contract,
ExtensibleApp a
ExtensibleApp.Contracts ( u posledních dvou nastavíme
Copy local na
False ). Adaptér vypadá následovně:
using System.AddIn.Pipeline;
using ExtensibleApp.Contracts;
namespace ExtensibleApp.HostSideAdapters
{
[HostAdapter]
class AddInContractToView : AddIn
{
private IAddInContract contract;
private ContractHandle handle;
public AddInContractToView(IAddInContract contract)
{
this.contract = contract;
this.handle = new ContractHandle(contract);
}
public override string OperationName
{
get { return this.contract.OperationName; }
}
public override string Operate(string value)
{
return this.contract.Operate(value);
}
}
}
Adaptér na hostující straně musí být označen atributem
HostAdapter. Třída nemusí být veřejná a v konstruktoru se očekává instance adaptovaného typu ( nejedná se o nic jiného než návrhový vzor
Adapter 
).
Krok 5: segment AddInSideAdapter
Založíme nový projekt typu
Class Library nazvaný
ExtensibleApp.AddInSideAdapters. Nastavíme
Output path na
..\output\AddInSideAdapters a přidáme reference na
System.AddIn,
System.AddIn.Contract,
ExtensibleApp.AddInViews a
ExtensibleApp.Contracts ( u posledních dvou nastavíme
Copy local na
False ). Adaptér vypadá následovně:
using System.AddIn.Pipeline;
using ExtensibleApp.AddInViews;
using ExtensibleApp.Contracts;
namespace ExtensibleApp.AddInSideAdapters
{
[AddInAdapter]
class AddInViewToContract : ContractBase, IAddInContract
{
private AddIn view;
public AddInViewToContract(AddIn view)
{
this.view = view;
}
public string OperationName
{
get { return this.view.OperationName; }
}
public string Operate(string value)
{
return this.view.Operate(value);
}
}
}
Tato třída označená atributem AddInAdapter mimo jiné dědí od třídy ContractBase (což je výchozí implementaci rozhraní IContract ).
Řekli jsem si, že adaptér adaptuje kontrakt na pohled a naopak. Z této činnosti vyplývá, že pávě díky adaptérům máme k dispozici volné ruce při změnách v hostující aplikaci nebo addinech - pokud se změní kontrakt, stačí upravit adaptér a druhé strany se to vůbec nedotkne. O tom ale někdy jindy.
Máme tedy segment kontraktu a dva pohledy na něj z každé strany. Máme adaptéry které nám slepují celou pipeline. Zbývá tedy vytvořit hostující aplikaci a nějaké addiny.
Krok 6: segment AddIn
Vytvoříme nový projekt typu Class Library nazvaný AddIn1 a nastavíme Ouput path na ..\output\AddIns\Addin1. Dále přidáme referenci na ExtensibleApp.AddInViews ( Copy local na False ) a System.AddIn. AddIn je odvozená třída od pohledu:
using ExtensibleApp.AddInViews;
namespace ToUpper
{
[System.AddIn.AddIn("ToUpper add-in", Version = "1.0.0.0")]
public class ToUpper : AddIn
{
public override string OperationName
{
get { return "vsechny pismena velka"; }
}
public override string Operate(string value)
{
return value.ToUpper();
}
}
}
Třída musí být označena atributem AddIn, který obsahuje vlastnosti jako název rozšíření, verze, popis apod.
Krok 7: segment Host
V této chvíli je pipeline vlastně hotova. Projekt hostovací aplikace ExtensibleApp jsme již vytvořili v kroku 2, takže nyní jenom doplníme kód do metody Main.
Myšlenka aplikace je taková, že uživatel napíše libovolný text a ten bude předán každému addinu ke zpracování.
Console.WriteLine("Zadejte text ktery bude zpracovan kazdym modulem:");
string input = Console.ReadLine();
Console.WriteLine();
.NET Framework si uchovává informace o pipeline v cache a jako první krok v práci s addiny bychom měli cache aktualizovat:
AddInStore.Update(PipelineStoreLocation.ApplicationBase);
Najdeme všechny addiny, které vyhovují HostSideView:
IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(AddIn), PipelineStoreLocation.ApplicationBase);
V této chvíli máme kolekci tzv. tokenů, což jsou struktury popisující addiny ( tzn. že se nejedná o aktivní addiny ). Všechny v cyklu projdeme a aktivujeme je metodou Activate. Pak máme k dispozici samotné instance addinů:
foreach (AddInToken token in tokens)
{
Console.WriteLine("Rozsireni: {0}, verze: {1}", token.Name, token.Version);
AddIn addin = token.Activate<AddIn>(AddInSecurityLevel.Internet);
Console.WriteLine("Operace: {0}", addin.OperationName);
Console.WriteLine("Vystup: {0}", addin.Operate(input));
Console.WriteLine();
}
V této konfiguraci se každý addin načte do vlastní AppDomain.
Celý tento příklad je k dispozici v příloze. Jako základní pro pochopení doporučuji .NET Application Extensibility - Part 1 a .NET Application Extensibility - Part 2.