Języki programowania na platformie .NET / Programowanie .NET
Rok akademicki: 2026/27
Instrukcja
laboratoryjna nr 4: Architektura Wtyczek (MEF 2.0 /
System.Composition), Modularny Interfejs
(Dirkster.AvalonDock) oraz Komunikacja Zdarzeniowa
(Event Aggregator)
Prowadzący: Tomasz Goluch
Wersja: 1.0
1. Wprowadzenie: Architektura Wtyczek (Plugin Architecture) i Ewolucja MEF w .NET
Cel: Zrozumienie fundamentów Managed Extensibility
Framework (MEF), poznanie podstawowych definicji kompozycji komponentów
oraz porównanie klasycznego MEF 1.0, nowoczesnego MEF 2.0
(System.Composition) i standardowego kontenera DI.
MEF (ang. Managed Extensibility Framework)
to biblioteka pozwalająca na tworzenie rozszerzalnych, modularnych
aplikacji. Umożliwia ona dynamiczne odkrywanie i ładowanie komponentów
(nazywanych „wtyczkami” lub „częściami”) bez potrzeby sztywnego wiązania
ich z główną aplikacją na etapie kompilacji. Pierwsza odsłona biblioteki
powstała wraz z .NET Framework 4.0, zaspokajając potrzeby
rozszerzalności środowiska Visual Studio 2010. W nowoczesnych
aplikacjach .NET 8/9+ jej następcą jest lekka i
wysokowydajna wersja MEF 2.0
(System.Composition).
[ Kontrakt: Biblioteka Wspólna (ModularStudio.Contracts.dll) ]
├── Interfejsy wtyczek i usług: IPlugin, IHostContext, IEventAggregator
└── Definicje metadanych i zdarzeń: PluginMetadata, DataPublishedEvent
▲ ▲
implementuje / używa │ │ implementuje / eksportuje
┌───────────────────────────────┴──────────┐ ┌──────────┴───────────────────────────────┐
│ Aplikacja Hosta (ModularStudio.Host.exe) │ │ Zewnętrzne Wtyczki ( Katalog /Plugins ) │
│ • Kontener kompozycji: CompositionHost │◄───────────────│ • DataGeneratorPlugin.dll [Document] │
│ • Układ dokowania: Dirkster.AvalonDock │ Skanowanie i │ • StatsChartPlugin.dll [Right] │
│ • Nasłuch zmian: FileSystemWatcher │ kompozycja │ • AnomalyAuditPlugin.dll [Bottom] │
└──────────────────────────────────────────┘ └──────────────────────────────────────────┘
Kluczowe pojęcia architektury MEF:
- Usługa (Service) — konkretna funkcjonalność, zasób lub zestaw operacji zdefiniowany przez kontrakt (najczęściej interfejs), który stanowi przedmiot wymiany między komponentami; jest to „coś”, co jedna część oferuje, a druga potrzebuje.
- Część (Part) — klasa (komponent), która
może świadczyć (eksportować) usługi lub korzystać (importować) z usług
innych części. Części mogą pochodzić z samej aplikacji lub z
zewnętrznych bibliotek
.dll. - Eksport (Export) — usługa świadczona przez
część, oznaczana atrybutem
[Export]. Część dostarczająca usługę nazywana jest eksporterem (np. rozszerzenie edytora lub rejestrator logów). - Import (Import) — zapotrzebowanie na
usługę zgłaszane przez część za pomocą atrybutu
[Import](dla pojedynczej usługi) lub[ImportMany](dla kolekcji usług tego samego typu). - Kontrakt (Contract) — unikalny
identyfikator eksportu i importu (zazwyczaj typ interfejsu, np.
typeof(IPlugin)lub dodatkowy ciąg znaków), na podstawie którego kontener paruje eksporterów z importerami. - Konfiguracja i Kontener Kompozycji (ContainerConfiguration / CompositionHost) — mechanizm skanujący wskazane zestawy (Assemblies), budujący graf zależności i dopasowujący eksporterów do importerów w procesie zwanym kompozycją.
- Metadane (Metadata) i Leniwe
ładowanie (Lazy Loading) — dodatkowe dane opisowe
(pary klucz–wartość) dołączone do eksportu. W połączeniu z typem
Lazy<T, TMetadata>pozwalają aplikacji hosta na odczytanie informacji o wtyczce (np. jej nazwy, wersji lub docelowego panelu UI) i podjęcie decyzji o jej użyteczności bez wcześniejszego tworzenia instancji klasy wtyczki.
Porównanie:
MEF 1.0 vs MEF 2.0 (System.Composition) vs
Microsoft.Extensions.DependencyInjection
Dlaczego powstała wersja lekka (MEF 2.0)? Głównym powodem była potrzeba wyeliminowania narzutu wydajnościowego związanego z intensywnym wykorzystaniem refleksji w klasycznym MEF 1.0. MEF 2.0 buduje grafy zależności przy użyciu drzew wyrażeń (Expression Trees), co zapewnia znacznie szybszą inicjalizację kontenera i mniejsze zużycie pamięci w dużych systemach, takich jak kompilator .NET (Roslyn).
| Cecha | MEF 1.0 (Klasyczny / Legacy) | MEF 2.0 (Nowoczesny – używany w lab.) | Microsoft.Extensions.DependencyInjection |
|---|---|---|---|
| Przestrzeń nazw / Pakiet | System.ComponentModel.Composition |
System.Composition
(pakiet NuGet) |
Microsoft.Extensions.DependencyInjection |
| Główna klasa kontenera | CompositionContainer |
CompositionHost |
ServiceProvider |
| Konfiguracja źródeł | Katalogi (DirectoryCatalog,
AssemblyCatalog) |
ContainerConfiguration
(.WithAssemblies(...)) |
ServiceCollection (jawna
rejestracja w kodzie) |
| Odkrywanie atrybutowe | Tak ([Export],
[Import]) |
Tak ([Export],
[Import]) + ConventionBuilder |
Brak wbudowanego (wymaga jawnego
services.Add...) |
| Dziedziczenie eksportów | Wbudowany
[InheritedExport] |
Jawny [Export] lub konwencje
ConventionBuilder |
Rejestracja ręczna w
ServiceCollection |
| Cykl życia: Singleton | [PartCreationPolicy(CreationPolicy.Shared)] |
[Shared] |
services.AddSingleton<TService, TImpl>() |
| Cykl życia: Zakres (Scope) | CreationPolicy.Shared w
kontenerze potomnym |
[Shared("NazwaGranicy")]
+ [SharingBoundary] |
services.AddScoped<TService, TImpl>() |
| Obsługa metadanych | Interfejs z getterami w
Lazy<T, IMetadata> |
Klasa z właściwościami / konstruktorem słownikowym | Brak natywnego wsparcia dla metadanych eksportu |
| Cykliczne zależności | Dopuszczalne (dla właściwości) | Zablokowane (wymusza czystą architekturę DAG) | Zablokowane (rzuca wyjątek podczas rezolucji) |
🧠 Konflikty przestrzeni nazw – najczęstsza pułapka!
Nigdy nie mieszaj atrybutów z przestrzeniSystem.ComponentModel.CompositionorazSystem.Compositionw jednym rozwiązaniu! Jeśli kontener budowany jest za pomocąContainerConfiguration(MEF 2.0), a wtyczka zostanie oznaczona atrybutem[Export]ze starej przestrzeniSystem.ComponentModel.Composition, kontener jej nie wykryje (widgets.Count() == 0), nie zgłaszając przy tym błędu kompilacji. We wszystkich projektach rozwiązania używaj wyłącznie pakietu NuGetSystem.Composition.
2.
Kontener Kompozycji w MEF 2.0 (System.Composition):
Eksporty, Importy i Metadane
Cel: Opanowanie konfiguracji kontenera
CompositionHost, deklarowania eksportów i importów
(właściwości, konstruktory, kolekcje) oraz definiowania silnie
typowanych metadanych w MEF 2.0.
Instalacja pakietu MEF 2.0 w projektach rozwiązania odbywa się za pomocą CLI:
dotnet add package System.Composition
1. Deklarowanie eksportów i importów oraz dopasowywanie kontraktów
Kontrakt stanowi unikalny identyfikator, po którym MEF paruje importerów z eksporterami. Może on być:
- Wywnioskowany automatycznie (w przypadku braku
argumentu w
[Export]lub[Import]) – kontraktem staje się wówczas pełna nazwa typu klasy lub właściwości wraz z przestrzenią nazw. - Podany jawnie poprzez typ interfejsu/klasy bazowej
– np.
[Export(typeof(IPlugin))]. - Podany jawnie jako nazwa tekstowa (i opcjonalnie
typ) – np.
[Export("FormatJson", typeof(IDataExporter))].
using System.Composition;
public interface IDevice
{
string GetStatus();
}
// 1. Eksport na poziomie klasy
[Export(typeof(IDevice))]
public class SensorDevice : IDevice
{
public string GetStatus() => "Sensor Online";
}
// 2. Eksport właściwości oraz metody (delegatu)
public class HardwareProvider
{
// Eksportowana właściwość musi być zainicjalizowana w momencie tworzenia!
[Export("BackupDevice", typeof(IDevice))]
public IDevice DefaultDevice { get; set; } = new SensorDevice();
// Eksport metody wymaga jawnego określenia typu delegatu (np. Func<T, TResult>)
[Export("ChecksumCalculator", typeof(Func<string, int>))]
public int CalculateChecksum(string payload) => payload.Length * 31;
}
2.
Wstrzykiwanie przez konstruktor ([ImportingConstructor]) vs
właściwości ([Import])
Domyślnie silnik kompozycji poszukuje konstruktora bezparametrowego.
Aby zrealizować zalecane w czystej architekturze wstrzykiwanie
zależności przez konstruktor, należy oznaczyć wybrany
konstruktor atrybutem
[ImportingConstructor]. Wszystkie jego
parametry stają się wówczas automatycznie wymaganymi importami:
using System.Composition;
[Export]
public class TelemetryController
{
private readonly IDevice _primaryDevice;
private readonly IEnumerable<IDevice> _allDevices;
// Parametry konstruktora oznaczonego [ImportingConstructor] są importowane automatycznie.
// UWAGA: Dla kolekcji elementów należy jawnie dodać [ImportMany] na parametrze!
// Bez [ImportMany] MEF szukałby pojedynczego eksportu o typie IEnumerable<IDevice>.
[ImportingConstructor]
public TelemetryController(
IDevice primaryDevice,
[ImportMany] IEnumerable<IDevice> allDevices)
{
_primaryDevice = primaryDevice;
_allDevices = allDevices;
}
// Import opcjonalny przez właściwość: brak eksportera nie rzuci CompositionFailedException,
// lecz przypisze wartość domyślną (null dla typów referencyjnych).
[Import("BackupDevice", AllowDefault = true)]
public IDevice? OptionalBackup { get; set; }
// Metoda wywoływana automatycznie przez MEF zaraz po wstrzyknięciu wszystkich właściwości [Import]
[OnImportsSatisfied]
public void OnInitialized()
{
Console.WriteLine($"Załadowano kontroler. Liczba urządzeń: {_allDevices.Count()}");
}
}
3.
Leniwe ładowanie (Lazy<T, TMetadata>) i obsługa
metadanych w MEF 2.0
Gdy liczba wtyczek nie jest znana z góry (np. ładowanie z folderu
Plugins), wykorzystujemy kolekcję
IEnumerable<Lazy<T, TMetadata>>. Pozwala to
aplikacji hosta przejrzeć metadane wszystkich wtyczek bez wywoływania
ich konstruktorów (instancja tworzona jest dopiero w momencie odczytu
właściwości .Value).
⚠️ KRYTYCZNA RÓŻNICA W MEF 2.0 – Klasa widoku metadanych:
W starym MEF 1.0 widokiem metadanychTMetadatabył interfejs z samymi getterami. W MEF 2.0 (System.Composition) użycie interfejsu rzuci wyjątek! Widok metadanych musi być konkretną klasą posiadającą publiczne właściwości z getterem i setterem ({ get; set; }), których nazwy odpowiadają kluczom metadanych.
using System.Composition;
// 1. Klasa widoku metadanych (w MEF 2.0 musi to być klasa, a nie interfejs!)
public class WidgetMetadata
{
public string Name { get; set; } = "Bez nazwy";
public string DefaultLocation { get; set; } = "Document";
public int Priority { get; set; } = 0; // Właściwość z wartością domyślną
}
// 2. Deklaracja wtyczki z atrybutami [ExportMetadata]
[Export(typeof(IWidget))]
[ExportMetadata("Name", "Wykres Liczb")]
[ExportMetadata("DefaultLocation", "Right")]
[ExportMetadata("Priority", 1)]
public class ChartsWidget : IWidget
{
public void Render() => Console.WriteLine("Rysowanie wykresu...");
}
4.
Konfiguracja ContainerConfiguration i budowa
CompositionHost
W MEF 2.0 części odkrywane są za pomocą obiektu
ContainerConfiguration, który pozwala zarejestrować
pojedyncze klasy lub całe zestawy (.dll /
.exe):
.WithAssembly(Assembly)– skanuje wszystkie części w pojedynczym zestawie,.WithAssemblies(IEnumerable<Assembly>)– skanuje kolekcję zestawów,.WithPart<T>()/.WithParts(IEnumerable<Type>)– rejestruje jawnie wskazane typy.
using System.Composition.Hosting;
using System.Reflection;
var configuration = new ContainerConfiguration()
.WithAssembly(typeof(Program).Assembly); // Skanowanie bieżącego zestawu
// CompositionHost implementuje IDisposable - zawsze używamy bloku 'using'
using CompositionHost container = configuration.CreateContainer();
// Pobranie eksportów wraz z metadanymi (bez tworzenia instancji ChartsWidget!)
IEnumerable<Lazy<IWidget, WidgetMetadata>> widgets =
container.GetExports<Lazy<IWidget, WidgetMetadata>>();
foreach (var lazyWidget in widgets.OrderBy(w => w.Metadata.Priority))
{
Console.WriteLine($"Odkryto wtyczkę: {lazyWidget.Metadata.Name} [{lazyWidget.Metadata.DefaultLocation}]");
// Dopiero odwołanie do .Value wywołuje konstruktor wtyczki i wstrzykuje jej zależności:
IWidget instance = lazyWidget.Value;
instance.Render();
}
💡 Zaawansowane: Silnie typowane atrybuty metadanych ([MetadataAttribute]) oraz konwencje (ConventionBuilder) zamiast [InheritedExport] w MEF 2.0 (Kliknij, aby rozwinąć/zwinąć)
1.
Własny, silnie typowany atrybut metadanych
([MetadataAttribute])
Zamiast podatnych na literówki ciągów znaków w
[ExportMetadata("DefaultLocation", "Right")], MEF 2.0
pozwala stworzyć własny atrybut C# oznaczony jako
[MetadataAttribute]. Daje to pełne wsparcie IntelliSense i
kontrolę typów podczas kompilacji:
using System.Composition;
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class PluginMetadataAttribute : Attribute
{
public string Name { get; set; } = string.Empty;
public string DefaultLocation { get; set; } = "Document";
public string Version { get; set; } = "1.0.0";
}
// Użycie na klasie wtyczki:
[Export(typeof(IWidget))]
[PluginMetadata(Name = "Analizator Logów", DefaultLocation = "Bottom", Version = "2.1.0")]
public class LogAnalyzerWidget : IWidget { /* ... */ }
2.
Zastąpienie [InheritedExport] za pomocą
ConventionBuilder
W MEF 1.0 można było oznaczyć interfejs atrybutem
[InheritedExport], dzięki czemu każda klasa go
implementująca była automatycznie eksportowana. W MEF 2.0 zrezygnowano z
[InheritedExport] na rzecz znacznie bardziej elastycznego
mechanizmu konwencji
(System.Composition.Convention), który pozwala
eksportować typy nawet wtedy, gdy w ogóle nie posiadają atrybutów MEF w
swoim kodzie:
using System.Composition.Convention;
using System.Composition.Hosting;
var conventions = new ConventionBuilder();
// Reguła: Każda klasa dziedzicząca/implementująca IWidget ma być automatycznie eksportowana jako IWidget!
conventions
.ForTypesDerivedFrom<IWidget>()
.Export<IWidget>();
// Reguła: Klasa HostServices ma być eksportowana jako IHostContext i współdzielona jako Singleton
conventions
.ForType<HostServices>()
.Export<IHostContext>()
.Shared();
var config = new ContainerConfiguration()
.WithAssembly(typeof(Program).Assembly, conventions);
3.
Zarządzanie Cyklem Życia Części: NonShared,
[Shared], Zakresy ([SharingBoundary]) i
ExportFactory<T>
Cel: Nauczenie się kontroli nad instancjonowaniem
części w MEF 2.0 (odpowiedniki Transient, Singleton i
Scoped) oraz dynamicznego tworzenia i niszczenia izolowanych
zakresów kompozycji za pomocą klasy
ExportFactory<T>.
Kontener MEF 2.0 wspiera trzy polityki cyklu życia eksportowanych obiektów:
| Cecha | NonShared (Niewspółdzielona) | Shared (Współdzielona Globalnie) | Shared w Zakresie (Współdzielona Lokalnie) |
|---|---|---|---|
| Atrybut w MEF 2.0 | Brak (zachowanie domyślne) | [Shared] |
[Shared("NazwaGranicy")]
+ [SharingBoundary("NazwaGranicy")] |
| Odpowiednik w klasycznym DI | Transient | Singleton | Scoped |
| Sposób instancjonowania | Nowa instancja przy każdym żądaniu lub wstrzyknięciu. | Jedna wspólna instancja w całym kontenerze
CompositionHost. |
Jedna instancja współdzielona wyłącznie
wewnątrz zakresu utworzonego przez
ExportFactory<T>. |
| Zastosowanie | Obiekty ze stanem specyficzny dla jednej operacji, widoki okien dialogowych, generatory. | Usługi z centralnym stanem:
IEventAggregator, IHostContext, konfiguracja,
logger. |
Izolowany stan sesji przetwarzania, pojedynczego otwartego dokumentu lub transakcji. |
1. Rola klasy
ExportFactory<T> oraz
Export<T>
W wielu scenariuszach komponent (np. serwis hosta lub wtyczka) nie potrzebuje pojedynczej, stworzonej przy starcie programu instancji danej zależności, lecz chce tworzyć nowe instancje (lub nowe izolowane zakresy kompozycji) na żądanie w trakcie działania programu i samodzielnie decydować o momencie ich zwolnienia z pamięci.
Do tego celu służy wbudowana w MEF 2.0 klasa generyczna
ExportFactory<T> (oraz jej wariant z
metadanymi ExportFactory<T, TMetadata>):
- Zamiast importować bezpośrednio typ
T, klasa importujeExportFactory<T>. - Wywołanie metody
CreateExport()na fabryce konstruuje nowy podgraf obiektów dla typuTi zwraca opakowanie typuExport<T>. - Właściwość
export.Valuedaje dostęp do utworzonej instancjiT. - Klasa
Export<T>implementuje interfejsIDisposable– zakończenie blokuusingna obiekcie zwróconym przezCreateExport()automatycznie niszczy cały utworzony podgraf obiektów (wywołującDispose()na wszystkich częściachNonSharedoraz częściach należących do zdefiniowanego zakresu)!
[ CompositionHost (Główny Kontener) ]
│ (Posiada części globalne: [Shared] IHostContext, [Shared] IEventAggregator)
│
├──► RequestHandlerFactory.CreateExport() ──► [ Zakres 1: Export<RequestHandler> ("RequestScope") ]
│ ├── RequestHandler (NonShared)
│ ├── RequestProcessor (NonShared) ──┐ (Współdzielą tę samą
│ └── AuditLogger (NonShared) ───────┼─► instancję RequestContext
│ │ o ID = A11...)
└──► RequestHandlerFactory.CreateExport() ──► [ Zakres 2: Export<RequestHandler> ("RequestScope") ]
└── ... (Osobna instancja RequestContext o ID = B22...)
2.
Kompletny przykład implementacji zakresu
([Shared("RequestScope")] +
ExportFactory<T>)
using System.Composition;
using System.Composition.Hosting;
// 1. Kontekst współdzielony wyłącznie w obrębie jednej sesji/żądania ("RequestScope")
[Export]
[Shared("RequestScope")] // Określenie nazwy granicy współdzielenia!
public class RequestContext : IDisposable
{
public Guid RequestId { get; } = Guid.NewGuid();
public RequestContext() =>
Console.WriteLine($" [SCOPE START] Utworzono RequestContext: {RequestId}");
public void Dispose() =>
Console.WriteLine($" [SCOPE STOP] Zwolniono RequestContext: {RequestId}");
}
// 2. Pierwsza usługa pomocnicza działająca w ramach żądania
[Export]
public class RequestProcessor
{
[ImportingConstructor]
public RequestProcessor(RequestContext context) => Context = context;
public RequestContext Context { get; }
public void Process() =>
Console.WriteLine($" [PROCESSOR] Przetwarzam w żądaniu: {Context.RequestId}");
}
// 3. Druga usługa pomocnicza w tym samym żądaniu (otrzyma TEN SAM obiekt RequestContext!)
[Export]
public class RequestAuditValidator
{
[ImportingConstructor]
public RequestAuditValidator(RequestContext context) => Context = context;
public RequestContext Context { get; }
public void Validate() =>
Console.WriteLine($" [VALIDATOR] Walidacja w żądaniu: {Context.RequestId}");
}
// 4. Główny obiekt obsługi żądania (domyślnie NonShared - nowa instancja dla każdego zakresu)
[Export]
public class RequestHandler
{
private readonly RequestProcessor _processor;
private readonly RequestAuditValidator _validator;
[ImportingConstructor]
public RequestHandler(RequestProcessor processor, RequestAuditValidator validator)
{
_processor = processor;
_validator = validator;
}
public void Handle()
{
_validator.Validate();
_processor.Process();
}
}
// 5. Zarządca aplikacji importujący fabrykę otwierającą granicę "RequestScope"
[Export]
public class AppManager
{
// KRYTYCZNE W MEF 2.0: Atrybut [SharingBoundary("RequestScope")] informuje fabrykę,
// że każde wywołanie CreateExport() ma utworzyć nową granicę dla części [Shared("RequestScope")].
[Import]
[SharingBoundary("RequestScope")]
public ExportFactory<RequestHandler> RequestHandlerFactory { get; set; } = null!;
public void RunSimulation()
{
Console.WriteLine("=== ZAKRES 1 ===");
using (Export<RequestHandler> scope1 = RequestHandlerFactory.CreateExport())
{
scope1.Value.Handle();
} // Wyjście z 'using' natychmiast wywołuje RequestContext.Dispose() dla Zakresu 1!
Console.WriteLine("\n=== ZAKRES 2 ===");
using (Export<RequestHandler> scope2 = RequestHandlerFactory.CreateExport())
{
scope2.Value.Handle();
}
}
}
4. Interakcja z Aplikacją Hostującą i Izolacja Biblioteki Kontraktów
Cel: Opanowanie zasad projektowania wspólnych
bibliotek kontraktów (.dll) oraz bezpiecznego udostępniania
usług hosta wtyczkom przy zachowaniu pełnej izolacji modułów.
Aby zewnętrzna wtyczka mogła bezpiecznie komunikować się z
aplikacją-hostem (np. publikować komunikaty na pasku stanu, zapisywać
logi czy sprawdzać wersję środowiska) i jednocześnie nie zależeć od
konkretnych klas projektu wykonywalnego .exe, należy
wydzielić osobną, lekką bibliotekę klas Contracts
(.dll). Zarówno aplikacja hosta, jak i projekty
poszczególnych wtyczek dodają referencję wyłącznie do tej biblioteki
kontraktów:
// =========================================================================
// 1. Projekt: ModularStudio.Contracts (Biblioteka wspólna)
// =========================================================================
public interface IHostContext
{
string HostVersion { get; }
void LogMessage(string sender, string message);
}
public interface IPlugin
{
object View { get; } // W aplikacjach WPF będzie to UserControl
void Initialize();
}
// =========================================================================
// 2. Projekt: ModularStudio.Host (Aplikacja główna WPF)
// =========================================================================
using System.Composition;
using ModularStudio.Contracts;
[Export(typeof(IHostContext))]
[Shared] // Singleton - wszystkie wtyczki otrzymają referencję do tej samej instancji hosta
public class HostServices : IHostContext
{
public string HostVersion => "v2.0.0 (.NET 8/9)";
public void LogMessage(string sender, string message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [{sender} @ {HostVersion}]: {message}");
}
}
// =========================================================================
// 3. Projekt: ModularStudio.Plugin.Sample (Zewnętrzna biblioteka .dll)
// =========================================================================
using System.Composition;
using ModularStudio.Contracts;
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "Analizator Tekstu")]
[ExportMetadata("DefaultLocation", "Document")]
public class SamplePlugin : IPlugin
{
private readonly IHostContext _host;
[ImportingConstructor]
public SamplePlugin(IHostContext host)
{
_host = host;
}
public object View { get; } = new object(); // docelowo: new SamplePluginControl()
public void Initialize()
{
_host.LogMessage("SamplePlugin", "Wtyczka została pomyślnie zainicjalizowana.");
}
}
5.
Agregator Zdarzeń (Event Aggregator): Luźno Powiązana
Komunikacja Między Wtyczkami
Cel: Opanowanie wzorca Wydawca–Subskrybent (Publish–Subscribe) w celu zapewnienia odizolowanej komunikacji między niezależnymi wtyczkami za pomocą centralnego, współdzielonego agregatora zdarzeń.
W miarę rozbudowywania aplikacji modularnej pojawia się potrzeba, aby jedna wtyczka reagowała na działanie innej (np. Wtyczka A generuje lub wczytuje zestaw danych pomiarowych, a Wtyczka B powinna natychmiast przeliczyć statystyki i odświeżyć wykres).
Bezpośrednie odwołanie się Wtyczki B do klasy Wtyczki A tworzy ścisłe powiązanie (tight coupling), które niszczy ideę architektury wtyczek – usunięcie lub wymiana Wtyczki A uniemożliwiłoby kompilację i działanie Wtyczki B. Rozwiązaniem jest wzorzec Agregatora Zdarzeń (Event Aggregator / Event Bus):
┌────────────────────────────┐ Publikuje: DataPublishedEvent ┌──────────────────────────────┐
│ Wtyczka A: Generator Danych│ ────────────────────────────────────────────► │ [Shared] Singleton │
│ (Wydawca / Publisher) │ (Nie wie, kto i ile wtyczek nasłuchuje) │ IEventAggregator │
└────────────────────────────┘ │ (Centralna Szyna Zdarzeń) │
└──────────────┬───────────────┘
┌───────────────────────────────────────────────────┴──────┐
▼ Powiadamia subskrybenta 1 ▼ Powiadamia subskrybenta 2
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Wtyczka B: Panel Statystyk │ │ Wtyczka C: Detektor Anomalii │
│ (Subskrybent / Subscriber) │ │ (Subskrybent / Subscriber) │
└──────────────────────────────┘ └──────────────────────────────┘
Porównanie rozwiązań Event Aggregator / Messenger we współczesnym ekosystemie .NET
| Rozwiązanie / Biblioteka | Charakterystyka i mechanizm działania | Zalety i zastosowanie w architekturze wtyczek |
|---|---|---|
Własny
IEventAggregator (MEF [Shared]) |
Lekka klasa oparta na słowniku subskrypcji
Dictionary<Type, List<Delegate>> z
synchronizacją wątków i tokenem IDisposable. |
Zero zewnętrznych
zależności w bibliotece kontraktów, pełna kontrola nad
przekierowaniem na wątek UI (Dispatcher). |
CommunityToolkit.Mvvm.Messaging
(WeakReferenceMessenger) |
Oficjalny standard Microsoftu w pakiecie
CommunityToolkit.Mvvm (interfejs IMessenger
oraz IRecipient<TMessage>). |
Wykorzystuje słabe referencje
(WeakReference), co chroni przed wyciekami pamięci
przy dynamicznym wyładowywaniu wtyczek. |
Prism.Events
(IEventAggregator) |
Klasyczny agregator z frameworka Prism
(PubSubEvent<T>). |
Wbudowana obsługa
ThreadOption.UIThread,
ThreadOption.BackgroundThread oraz filtrowania zdarzeń
predykatami. |
libc.eventbus |
Prosta, zewnętrzna biblioteka szyny zdarzeń. | Minimalistyczne API, jednak w nowoczesnych
projektach .NET 8/9+ wyparta przez
CommunityToolkit.Mvvm lub własny kontrakt. |
1.
Definicja kontraktu zdarzeń i bezpiecznego wielowątkowo
IEventAggregator
W bibliotece kontraktów (Contracts) definiujemy
interfejs IEventAggregator oraz niemutowalne rekordy
wiadomości (zdarzeń):
// W projekcie: ModularStudio.Contracts
public record DataBatchPublishedEvent(
string SourceName,
string RawText,
IReadOnlyList<double> Measurements,
DateTime Timestamp);
public record SystemAlertEvent(string Severity, string Message, DateTime Timestamp);
public interface IEventAggregator
{
void Publish<TEvent>(TEvent eventToPublish) where TEvent : class;
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
}
Implementacja w projekcie hosta (eksportowana jako singleton
[Shared]) z obsługą bezpiecznego wyrejestrowywania
subskrypcji (IDisposable):
// W projekcie: ModularStudio.Host
using System.Composition;
using ModularStudio.Contracts;
[Export(typeof(IEventAggregator))]
[Shared] // Kluczowe! Agregator musi być pojedynczą instancją (Singletonem) w całym kontenerze!
public class EventAggregator : IEventAggregator
{
private readonly Dictionary<Type, List<Delegate>> _subscribers = new();
private readonly object _syncLock = new();
public void Publish<TEvent>(TEvent eventToPublish) where TEvent : class
{
ArgumentNullException.ThrowIfNull(eventToPublish);
List<Action<TEvent>> handlersSnapshot;
lock (_syncLock)
{
if (!_subscribers.TryGetValue(typeof(TEvent), out var list))
return;
// Kopiujemy listę delegatów wewnątrz locka, aby uniknąć wyjątku modyfikacji kolekcji podczas iteracji
handlersSnapshot = list.OfType<Action<TEvent>>().ToList();
}
foreach (var handler in handlersSnapshot)
{
handler(eventToPublish);
}
}
public IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class
{
ArgumentNullException.ThrowIfNull(handler);
lock (_syncLock)
{
var eventType = typeof(TEvent);
if (!_subscribers.TryGetValue(eventType, out var list))
{
list = new List<Delegate>();
_subscribers[eventType] = list;
}
list.Add(handler);
}
// Zwracamy token pozwalający na łatwe wypisanie się ze zdarzenia (np. przy zamykaniu wtyczki)
return new SubscriptionToken(() =>
{
lock (_syncLock)
{
if (_subscribers.TryGetValue(typeof(TEvent), out var list))
list.Remove(handler);
}
});
}
private sealed class SubscriptionToken(Action unsubscribe) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
unsubscribe();
_disposed = true;
}
}
}
2.
Komunikacja między wtyczkami za pomocą
IEventAggregator
// Wtyczka 1 (Wydawca): Publikuje nowe dane pomiarowe po kliknięciu przycisku przez użytkownika
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "Generator Telemetrii")]
[ExportMetadata("DefaultLocation", "Document")]
public class TelemetryGeneratorPlugin : IPlugin
{
private readonly IEventAggregator _eventBus;
[ImportingConstructor]
public TelemetryGeneratorPlugin(IEventAggregator eventBus)
{
_eventBus = eventBus;
}
public object View { get; } = new object();
public void Initialize() { }
public void EmitSampleBatch(string text, double[] values)
{
// Publikacja zdarzenia - wtyczka nie wie, jakie inne moduły są załadowane w aplikacji!
_eventBus.Publish(new DataBatchPublishedEvent(
SourceName: "Generator Telemetrii",
RawText: text,
Measurements: values,
Timestamp: DateTime.Now));
}
}
// Wtyczka 2 (Subskrybent): Odbiera dane i aktualizuje swój wykres / statystyki
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "Statystyki na Żywo")]
[ExportMetadata("DefaultLocation", "Right")]
public class LiveStatsPlugin : IPlugin, IDisposable
{
private readonly IDisposable _subscription;
[ImportingConstructor]
public LiveStatsPlugin(IEventAggregator eventBus)
{
// Subskrypcja wybranego typu zdarzenia w konstruktorze
_subscription = eventBus.Subscribe<DataBatchPublishedEvent>(OnDataBatchReceived);
}
public object View { get; } = new object();
public void Initialize() { }
private void OnDataBatchReceived(DataBatchPublishedEvent e)
{
double avg = e.Measurements.Any() ? e.Measurements.Average() : 0.0;
Console.WriteLine($"[STATYSTYKI] Odebrano paczkę z '{e.SourceName}'. Średnia: {avg:F2}");
}
public void Dispose() => _subscription.Dispose();
}
6.
Modularny Interfejs Użytkownika z Dirkster.AvalonDock
Cel: Integracja dynamicznie załadowanych wtyczek MEF
2.0 z systemem okien dokowanych Dirkster.AvalonDock w WPF
oraz rozmieszczanie widoków (centralne dokumenty vs panele boczne/dolne)
na podstawie metadanych.
Biblioteka Dirkster.AvalonDock pozwala
na stworzenie w WPF elastycznego środowiska przypominającego IDE Visual
Studio, w którym zakładki dokumentów oraz panele narzędziowe mogą być
dowolnie przeciągane, przypinane do krawędzi okna, grupowane lub
odrywane jako niezależne okna pływające (Floating Windows).
Instalacja pakietu w projekcie WPF
(ModularStudio.Host):
dotnet add package Dirkster.AvalonDock
dotnet add package Dirkster.AvalonDock.Themes.VS2013
1. Główne komponenty
układu AvalonDock:
DockingManager— główna kontrolka zarządzająca całym układem dokowania i motywem wizualnym.LayoutRootiLayoutPanel— korzeń struktury układu oraz kontenery dzielące przestrzeń w poziomie (Horizontal) lub w pionie (Vertical).LayoutDocumentPane— obszar centralny przeznaczony na główne widoki robocze opakowane w obiektyLayoutDocument.LayoutAnchorablePane— panele boczne i dolne przeznaczone na pomocnicze okna narzędziowe opakowane w obiektyLayoutAnchorable.
2. Konfiguracja
MainWindow.xaml
<Window x:Class="ModularStudio.Host.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xcad="clr-namespace:AvalonDock;assembly=AvalonDock"
xmlns:layout="clr-namespace:AvalonDock.Layout;assembly=AvalonDock"
Title="ModularStudio - Środowisko Analizy Danych (MEF 2.0)" Height="650" Width="1100">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<!-- Główny menedżer dokowania AvalonDock -->
<xcad:DockingManager x:Name="DockManager" Grid.Row="0">
<layout:LayoutRoot>
<layout:LayoutPanel Orientation="Vertical">
<layout:LayoutPanel Orientation="Horizontal">
<!-- Centralny obszar dokumentów roboczych -->
<layout:LayoutDocumentPane x:Name="DocumentsPane" />
<!-- Prawy panel narzędziowy (np. wykresy, statystyki) -->
<layout:LayoutAnchorablePane x:Name="RightToolsPane" DockWidth="320" />
</layout:LayoutPanel>
<!-- Dolny panel narzędziowy (np. logi, detektor anomalii) -->
<layout:LayoutAnchorablePane x:Name="BottomToolsPane" DockHeight="180" />
</layout:LayoutPanel>
</layout:LayoutRoot>
</xcad:DockingManager>
<!-- Pasek stanu Hosta -->
<StatusBar Grid.Row="1">
<StatusBarItem>
<TextBlock x:Name="TxtStatus" Text="Gotowy" />
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
3. Dynamiczne wstrzykiwanie widoków wtyczek na podstawie metadanych
Host aplikacji nie musi znać konkretnych klas wtyczek – odczytuje
właściwość DefaultLocation z obiektu
PluginMetadata i umieszcza właściwość View
(UserControl) we właściwym panelu
AvalonDock:
using AvalonDock.Layout;
using ModularStudio.Contracts;
private void ComposeAndAttachPlugins()
{
DocumentsPane.Children.Clear();
RightToolsPane.Children.Clear();
BottomToolsPane.Children.Clear();
// Pobranie wszystkich wtyczek wraz z metadanymi z kontenera CompositionHost (MEF 2.0)
var plugins = _container.GetExports<Lazy<IPlugin, PluginMetadata>>();
foreach (var lazyPlugin in plugins)
{
PluginMetadata meta = lazyPlugin.Metadata;
IPlugin pluginInstance = lazyPlugin.Value;
pluginInstance.Initialize();
switch (meta.DefaultLocation)
{
case "Right":
RightToolsPane.Children.Add(new LayoutAnchorable
{
Title = $"{meta.Name} (v{meta.Version})",
Content = pluginInstance.View,
CanClose = false
});
break;
case "Bottom":
BottomToolsPane.Children.Add(new LayoutAnchorable
{
Title = $"{meta.Name} (v{meta.Version})",
Content = pluginInstance.View,
CanClose = false
});
break;
default: // "Document"
DocumentsPane.Children.Add(new LayoutDocument
{
Title = meta.Name,
Content = pluginInstance.View
});
break;
}
}
}
7.
Dynamiczne Skanowanie Katalogu Wtyczek, FileSystemWatcher i
Automatyzacja PostBuild
Cel: Zaimplementowanie mechanizmu automatycznego
wykrywania nowych lub usuniętych bibliotek .dll w trakcie
działania aplikacji przy użyciu klasy FileSystemWatcher z
obsługą wątku UI (Dispatcher) oraz automatyzacja wdrażania
wtyczek w procesie kompilacji MSBuild.
1.
Ładowanie zestawów z folderu Plugins w MEF 2.0 bez
blokowania plików .dll
W przeciwieństwie do klasycznego MEF 1.0, lekki kontener MEF
2.0 (System.Composition) nie posiada klasy
DirectoryCatalog ani metody .Refresh(). Aby
załadować wtyczki z podkatalogu Plugins, skanujemy pliki
*.dll za pomocą Directory.GetFiles i
przekazujemy załadowane obiekty Assembly do
ContainerConfiguration.WithAssemblies(...).
⚠️ Pułapka blokowania plików przez
Assembly.LoadFrom(path):
Standardowe wywołanieAssembly.LoadFrom(dllPath)zakłada w systemie Windows blokadę zapisu na pliku.dllprzez cały czas działania aplikacji! Uniemożliwiłoby to usunięcie wtyczki z folderuPluginslub jej nadpisanie podczas rekompilacji, gdy aplikacja hosta jest uruchomiona.
Aby temu zapobiec, wczytujemy zawartość pliku.dlldo tablicy bajtów (File.ReadAllBytes(dllPath)) i ładujemy zestaw z pamięci za pomocąAssembly.Load(byte[])(lub używamyAssemblyLoadContextz flagąisCollectible: true).
using System.Composition.Hosting;
using System.IO;
using System.Reflection;
private CompositionHost BuildPluginContainer(string pluginFolderPath)
{
Directory.CreateDirectory(pluginFolderPath);
var assemblies = new List<Assembly>
{
typeof(MainWindow).Assembly // Zestaw Hosta (zawiera m.in. HostServices i EventAggregator)
};
foreach (string dllPath in Directory.GetFiles(pluginFolderPath, "*.dll"))
{
try
{
// Wczytanie pliku do pamięci RAM przed załadowaniem Assembly nie blokuje pliku na dysku!
byte[] rawBytes = File.ReadAllBytes(dllPath);
Assembly asm = Assembly.Load(rawBytes);
assemblies.Add(asm);
}
catch (Exception ex)
{
Console.WriteLine($"Pominięto nieprawidłowy plik DLL '{Path.GetFileName(dllPath)}': {ex.Message}");
}
}
var config = new ContainerConfiguration().WithAssemblies(assemblies);
return config.CreateContainer();
}
2.
Monitorowanie folderu Plugins za pomocą
FileSystemWatcher i problem wątków (Thread
Affinity)
Klasa FileSystemWatcher nasłuchuje powiadomień systemu
operacyjnego o utworzeniu (Created), zmodyfikowaniu
(Changed) lub usunięciu (Deleted) pliku
.dll w katalogu rozszerzeń.
Należy przy tym bezwzględnie pamiętać o regule powinowactwa wątków (Thread Affinity) w WPF:
- Wątek tła (Background Thread): Zdarzenia
FileSystemWatchersą wywoływane asynchronicznie na wątku z puli procesora (ThreadPool). - Wątek UI (UI Thread): Kontrolki WPF oraz
kolekcje paneli
AvalonDockmogą być tworzone i modyfikowane wyłącznie z głównego wątku interfejsu użytkownika (STA). - Konflikt: Próba przebudowania kontenera (który
tworzy obiekty
UserControlwtyczek) bezpośrednio w obsłudze zdarzeniaCreatedzakończy się wyjątkiemInvalidOperationException: "The calling thread cannot access this object because a different thread owns it".
Rozwiązaniem jest przekierowanie wykonania do wątku UI za pomocą
Application.Current.Dispatcher.Invoke (lub
InvokeAsync):
private FileSystemWatcher? _watcher;
private void StartWatchingPlugins(string pluginFolder)
{
_watcher = new FileSystemWatcher(pluginFolder, "*.dll")
{
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
};
_watcher.Created += OnPluginFolderChanged;
_watcher.Deleted += OnPluginFolderChanged;
_watcher.Changed += OnPluginFolderChanged;
_watcher.EnableRaisingEvents = true;
}
private void OnPluginFolderChanged(object sender, FileSystemEventArgs e)
{
// Przekierowanie przebudowy kontenera i aktualizacji kontrolek AvalonDock do głównego wątku UI
Application.Current.Dispatcher.Invoke(() =>
{
_container?.Dispose();
_container = BuildPluginContainer(_pluginFolder);
ComposeAndAttachPlugins();
TxtStatus.Text = $"Odświeżono wtyczki ({DateTime.Now:HH:mm:ss}) - zmiana pliku: {e.Name}";
});
}
3.
Publikowanie aplikacji i automatyczne kopiowanie wtyczek
(PostBuildEvent)
Proces publikacji (dotnet publish) wytwarza pliki
binarne w jednym z dwóch modeli wdrożeniowych:
- Aplikacja zależna od frameworka
(framework-dependent): Zawiera wyłącznie kod aplikacji
i jej zależności zewnętrzne; jest mała i wieloplatformowa, ale wymaga
zainstalowanego środowiska wykonawczego
.NET Runtimena maszynie docelowej. - Aplikacja samodzielna (self-contained):
Zawiera kompletne środowisko wykonawcze
.NETwraz z bibliotekami systemowymi (np.dotnet publish -r win-x64 --self-contained true); działa bez instalacji.NET, lecz ma znacznie większy rozmiar i jest przypisana do docelowego systemu operacyjnego (RID).
Aby podczas pracy w Visual Studio lub wywołania
dotnet build nie kopiować ręcznie skompilowanych plików
.dll każdej wtyczki do katalogu Plugins
aplikacji hosta, proces ten automatyzujemy za pomocą zdarzenia
Po kompilacji (PostBuildEvent) w pliku
.csproj projektu wtyczki. Wykorzystujemy przy tym makra
MSBuild ($(SolutionDir), $(TargetDir),
$(TargetFileName)) oraz systemowe polecenie
xcopy z przełącznikami /Y (nadpisywanie bez
pytania) i /D (kopiowanie nowszych plików):
<!-- W pliku .csproj każdej wtyczki (np. ModularStudio.Plugin.Stats.csproj) -->
<Target Name="CopyPluginToHostFolder" AfterTargets="PostBuildEvent">
<Exec Command="if not exist "$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins" mkdir "$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins"" />
<Exec Command="xcopy "$(TargetPath)" "$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins\" /Y /D" />
</Target>
8. Diagnostyka i Rozwiązywanie Najczęstszych Błędów Kompozycji
Cel: Szybka identyfikacja i eliminacja typowych problemów związanych z odkrywaniem części, niezgodnością liczności, metadanymi oraz wielowątkowością w aplikacjach opartych o MEF 2.0.
Większość błędów konfiguracji w MEF 2.0 objawia się rzuceniem wyjątku
CompositionFailedException podczas
wywołania GetExport<T>() lub w momencie odwołania do
właściwości .Value obiektu leniwego
Lazy<T, TMetadata>():
- Błędy liczności (Cardinality Mismatch):
- Brak wymaganego eksportu: Użyto atrybutu
[Import](lub parametru w[ImportingConstructor]), ale w załadowanych zestawach nie ma żadnej klasy z pasującym kontraktem[Export]. - Zbyt wiele eksportów dla pojedynczego
[Import]: W folderzePluginsznajdują się dwie wtyczki eksportujące ten sam kontrakt, a komponent prosi o pojedynczą instancję zamiast kolekcji[ImportMany].
- Brak wymaganego eksportu: Użyto atrybutu
- Błędy mapowania metadanych w
Lazy<T, TMetadata>:- Wyjątek podczas rezolucji
TMetadata: W MEF 2.0 (System.Composition) typTMetadatazostał zdefiniowany jakointerfacezamiast jakoclassz publicznymi właściwościami{ get; set; }. - Brak klucza metadanych: Jeśli klasa metadanych
posiada właściwość
public string DefaultLocation { get; set; }, a któraś z wtyczek nie zadeklaruje[ExportMetadata("DefaultLocation", "...")], MEF 2.0 odrzuci taką część podczas dopasowywania, chyba że użyjesz słownika lub zapewnisz spójny atrybut[MetadataAttribute]na wszystkich wtyczkach.
- Wyjątek podczas rezolucji
- Wyjątki wewnątrz konstruktorów wtyczek
(
[ImportingConstructor]):- W projektach WPF częstym błędem jest zapomnienie o wywołaniu metody
InitializeComponent()w konstruktorze oznaczonym[ImportingConstructor], przez co widokUserControlwtyczki pozostaje pusty, lub odwrotnie – odwołanie się do elementów XAML przed wywołaniemInitializeComponent().
- W projektach WPF częstym błędem jest zapomnienie o wywołaniu metody
- Duplikacja zestawów kontraktu
(
ModularStudio.Contracts.dll) w folderzePlugins:- Jeśli skopiujesz do folderu
Pluginsoprócz pliku wtyczki również plikModularStudio.Contracts.dlli załadujesz go ponownie przezAssembly.Load(byte[]), środowisko .NET uzna interfejsIPluginz pamięci za inny typ niżIPluginzaładowany w aplikacji Hosta! Skopiowane powinny być wyłącznie pliki.dllsamych wtyczek.
- Jeśli skopiujesz do folderu
Zadanie Laboratoryjne do wykonania (100 pkt)
Cel zadania: Stworzenie w technologii WPF (.NET 8/9+) modularnej aplikacji „ModularStudio – Środowisko Analizy Tekstu i Telemetrii” z dokowanym interfejsem użytkownika
Dirkster.AvalonDock, wykorzystującej kontener MEF 2.0 (System.Composition) do dynamicznego ładowania wtyczek z folderuPlugins(z obsługą zmian w locie przezFileSystemWatcher), wzorzec Agregatora Zdarzeń (IEventAggregator) do odseparowanej komunikacji między wtyczkami oraz fabrykę zakresówExportFactory<T>do izolowanego audytu danych.
Część
1: Architektura Rozwiązania i Biblioteka Kontraktów
(ModularStudio.Contracts) (15 pkt)
- Utwórz rozwiązanie składające się z co najmniej 4 osobnych
projektów:
ModularStudio.Contracts(Biblioteka klas.dll),ModularStudio.Host(Aplikacja główna WPF),- Co najmniej dwóch zewnętrznych projektów wtyczek (biblioteki klas
WPF z
UseWPF=true), np.ModularStudio.Plugin.DataGeneratororazModularStudio.Plugin.Analytics.
- W projekcie
ModularStudio.Contractszdefiniuj:- Interfejs wtyczki
IPlugin(zawierający m.in. właściwośćobject View { get; }lubUIElement View { get; }oraz metodę inicjalizującą) i klasę widoku metadanychPluginMetadata(Name,Version,DefaultLocation) lub własny atrybut[MetadataAttribute]. - Interfejs usług hosta
IHostContext(udostępniający wersję aplikacji hosta oraz metodęLogMessage(string pluginName, string message)zapisującą wpisy na pasku stanu lub w logu systemowym hosta). - Interfejs szyny zdarzeń
IEventAggregator(Publish<TEvent>,Subscribe<TEvent>) oraz klasy/rekordy komunikatów (np.DataPayloadPublishedEventprzenoszący tekst oraz kolekcję liczb wyemitowanych przez wtyczkę źródłową).
- Interfejs wtyczki
Część
2: Aplikacja Hosta, CompositionHost (MEF 2.0) oraz Układ
Dirkster.AvalonDock (25 pkt)
- (15 pkt) Konfiguracja kontenera MEF 2.0 i usług
współdzielonych (
[Shared]):- Zaimplementuj w projekcie hosta klasę
HostServices : IHostContextorazEventAggregator : IEventAggregator(lub adapter naWeakReferenceMessengerzCommunityToolkit.Mvvm) i wyeksportuj je w MEF 2.0 jako singletony za pomocą atrybutu[Shared]. - Skonfiguruj kontener
ContainerConfiguration/CompositionHost(System.Composition.Hosting) tak, aby skanował zestaw hosta oraz zewnętrzne pliki.dllznajdujące się w podkataloguPlugins.
- Zaimplementuj w projekcie hosta klasę
- (10 pkt) Integracja z
Dirkster.AvalonDocksterowana metadanymi:- W oknie głównym
MainWindow.xamlzdefiniuj układDockingManagerzawierający centralny obszar dokumentów (LayoutDocumentPane) oraz przynajmniej jeden panel boczny/dolny (LayoutAnchorablePane). - Pobierz wtyczki z kontenera za pomocą
Lazy<IPlugin, PluginMetadata>i na podstawie wartości metadanejDefaultLocation(np."Document"vs"Right"/"Bottom") automatycznie opakuj ich widoki w obiektyLayoutDocumentlubLayoutAnchorablei dodaj do odpowiednich paneli.
- W oknie głównym
Część
3: Wtyczki Funkcjonalne i Komunikacja przez
IEventAggregator (30 pkt)
Zaimplementuj i skonfiguruj za pomocą zdarzeń
PostBuild (automatyczne kopiowanie
.dll do folderu Plugins hosta) przynajmniej
dwie współpracujące ze sobą, lecz całkowicie od siebie niezależne
wtyczki:
- (15 pkt) Wtyczka 1 – Generator / Edytor Strumienia Danych
(
DefaultLocation = "Document"):- Dostarcza widok
UserControlumieszczany w centralnej zakładce dokumentówAvalonDock. - Pozwala użytkownikowi wprowadzić/wkleić tekst analityczny lub wygenerować losową serię pomiarów liczbowych (np. symulacja odczytów czujników / logów) i kliknąć przycisk „Przekaż do analizy”.
- Wstrzykuje przez
[ImportingConstructor]usługiIEventAggregatororazIHostContext. Po kliknięciu przycisku loguje akcję w hoście i publikuje zdarzenieDataPayloadPublishedEventw agregatorze zdarzeń.
- Dostarcza widok
- (15 pkt) Wtyczka 2 – Panel Statystyk, Wizualizacji i Audytu
(
DefaultLocation = "Right"):- Dostarcza widok
UserControldokowany automatycznie w bocznym panelu narzędziowymLayoutAnchorablePane. - Subskrybuje zdarzenie
DataPayloadPublishedEventza pośrednictwemIEventAggregator. - Po odebraniu danych automatycznie wylicza i prezentuje w interfejsie
statystyki (np. liczbę słów/znaków, najczęściej występujące słowa lub
wartość minimalną, maksymalną, średnią i wykres słupkowy
ProgressBar/ItemsControldla serii liczbowej).
- Dostarcza widok
Część
4: Zarządzanie Zakresami ([Shared("...")] +
ExportFactory<T>) (15 pkt)
- Zademonstruj praktyczne użycie cyklu życia Scoped w
MEF 2.0:
- Zdefiniuj klasę kontekstu sesji analizy (np.
AnalysisSessionContextz unikalnym identyfikatoremGuid SessionIdi znacznikiem czasu), oznaczoną atrybutami[Export]oraz[Shared("AnalysisScope")]. - Zdefiniuj klasę procesora/audytora (np.
BatchAuditor/AnomalyDetector), która importujeAnalysisSessionContexti weryfikuje, czy w przesłanej paczce danych występują anomalie (np. wartości przekraczające próg alarmowy lub słowa kluczoweERROR/CRITICAL). - W wybranej wtyczce lub usłudze zaimportuj fabrykę
ExportFactory<BatchAuditor>z atrybutem[SharingBoundary("AnalysisScope")]i przy każdym odbiorze nowej paczki danych twórz izolowany zakres kompozycji w blokuusing (var scope = AuditorFactory.CreateExport()), wyświetlając unikalnySessionIdw interfejsie lub logu.
- Zdefiniuj klasę kontekstu sesji analizy (np.
Część
5: Dynamiczne Monitorowanie Katalogu Wtyczek
(FileSystemWatcher) (15 pkt)
- (10 pkt) Obsługa
FileSystemWatcherw czasie rzeczywistym:- Skonfiguruj w aplikacji hosta instancję
FileSystemWatchermonitorującą katalogPluginspod kątem plików*.dll(zdarzeniaCreatedorazDeleted). - Zadbaj o wczytywanie zestawów w sposób nieblokujący plików na dysku
(np.
File.ReadAllBytes+Assembly.Load), aby możliwe było dorzucenie lub usunięcie pliku.dllw Eksploratorze Windows podczas działania aplikacji.
- Skonfiguruj w aplikacji hosta instancję
- (5 pkt) Bezpieczeństwo wątkowe interfejsu (Thread
Affinity):
- Przekieruj odświeżenie kontenera MEF i przebudowę kolekcji paneli
AvalonDockdo głównego wątku UI za pomocąApplication.Current.Dispatcher.Invoke, tak aby dodanie lub usunięcie pliku.dllz folderuPluginsnatychmiast aktualizowało widoczne panele w oknie bez rzucania wyjątkuInvalidOperationException.
- Przekieruj odświeżenie kontenera MEF i przebudowę kolekcji paneli
Zestawienie Oceny (Suma: 100 pkt)
| Nr | Zakres modułu | Weryfikacja | Punkty |
|---|---|---|---|
| 1 | Biblioteka Kontraktów i Podział Solucji | Odseparowany projekt
Contracts (IPlugin, IHostContext,
IEventAggregator, PluginMetadata), brak
bezpośrednich referencji między wtyczkami. |
15 pkt |
| 2 | Host MEF 2.0
(System.Composition) i
AvalonDock |
Konfiguracja
ContainerConfiguration / CompositionHost,
usługi [Shared], rozmieszczenie widoków w
LayoutDocument i LayoutAnchorable wg
metadanych. |
25 pkt |
| 3 | Wtyczki,
IEventAggregator i Automatyzacja
PostBuild |
Działająca wtyczka dokumentu (wydawca) i
panelu bocznego (subskrybent), komunikacja przez
IEventAggregator, kopiowanie .dll w
PostBuildEvent. |
30 pkt |
| 4 | Cykl życia Scoped i
ExportFactory<T> |
Poprawne użycie
[Shared("NazwaGranicy")] oraz
[SharingBoundary("NazwaGranicy")] z
ExportFactory<T> w bloku using. |
15 pkt |
| 5 | Dynamiczne Przeładowanie
(FileSystemWatcher + Dispatcher) |
Automatyczne wykrywanie nowych/usuniętych
plików .dll w folderze Plugins na żywo z
użyciem Dispatcher.Invoke bez blokowania plików. |
15 pkt |
| SUMA | 100 pkt |