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:

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 przestrzeni System.ComponentModel.Composition oraz System.Composition w jednym rozwiązaniu! Jeśli kontener budowany jest za pomocą ContainerConfiguration (MEF 2.0), a wtyczka zostanie oznaczona atrybutem [Export] ze starej przestrzeni System.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 NuGet System.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ć:

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 metadanych TMetadata był 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):

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>):

[ 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:

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łanie Assembly.LoadFrom(dllPath) zakłada w systemie Windows blokadę zapisu na pliku .dll przez cały czas działania aplikacji! Uniemożliwiłoby to usunięcie wtyczki z folderu Plugins lub jej nadpisanie podczas rekompilacji, gdy aplikacja hosta jest uruchomiona.
Aby temu zapobiec, wczytujemy zawartość pliku .dll do tablicy bajtów (File.ReadAllBytes(dllPath)) i ładujemy zestaw z pamięci za pomocą Assembly.Load(byte[]) (lub używamy AssemblyLoadContext z 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:

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:

  1. 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 Runtime na maszynie docelowej.
  2. Aplikacja samodzielna (self-contained): Zawiera kompletne środowisko wykonawcze .NET wraz 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 &quot;$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins&quot; mkdir &quot;$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins&quot;" />
  <Exec Command="xcopy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)ModularStudio.Host\bin\$(Configuration)\$(TargetFramework)\Plugins\&quot; /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>():

  1. 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 folderze Plugins znajdują się dwie wtyczki eksportujące ten sam kontrakt, a komponent prosi o pojedynczą instancję zamiast kolekcji [ImportMany].
  2. Błędy mapowania metadanych w Lazy<T, TMetadata>:
    • Wyjątek podczas rezolucji TMetadata: W MEF 2.0 (System.Composition) typ TMetadata został zdefiniowany jako interface zamiast jako class z 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.
  3. 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 widok UserControl wtyczki pozostaje pusty, lub odwrotnie – odwołanie się do elementów XAML przed wywołaniem InitializeComponent().
  4. Duplikacja zestawów kontraktu (ModularStudio.Contracts.dll) w folderze Plugins:
    • Jeśli skopiujesz do folderu Plugins oprócz pliku wtyczki również plik ModularStudio.Contracts.dll i załadujesz go ponownie przez Assembly.Load(byte[]), środowisko .NET uzna interfejs IPlugin z pamięci za inny typ niż IPlugin załadowany w aplikacji Hosta! Skopiowane powinny być wyłącznie pliki .dll samych wtyczek.

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 folderu Plugins (z obsługą zmian w locie przez FileSystemWatcher), wzorzec Agregatora Zdarzeń (IEventAggregator) do odseparowanej komunikacji między wtyczkami oraz fabrykę zakresów ExportFactory<T> do izolowanego audytu danych.


Część 1: Architektura Rozwiązania i Biblioteka Kontraktów (ModularStudio.Contracts) (15 pkt)

  1. 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.DataGenerator oraz ModularStudio.Plugin.Analytics.
  2. W projekcie ModularStudio.Contracts zdefiniuj:
    • Interfejs wtyczki IPlugin (zawierający m.in. właściwość object View { get; } lub UIElement View { get; } oraz metodę inicjalizującą) i klasę widoku metadanych PluginMetadata (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. DataPayloadPublishedEvent przenoszący tekst oraz kolekcję liczb wyemitowanych przez wtyczkę źródłową).

Część 2: Aplikacja Hosta, CompositionHost (MEF 2.0) oraz Układ Dirkster.AvalonDock (25 pkt)

  1. (15 pkt) Konfiguracja kontenera MEF 2.0 i usług współdzielonych ([Shared]):
    • Zaimplementuj w projekcie hosta klasę HostServices : IHostContext oraz EventAggregator : IEventAggregator (lub adapter na WeakReferenceMessenger z CommunityToolkit.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 .dll znajdujące się w podkatalogu Plugins.
  2. (10 pkt) Integracja z Dirkster.AvalonDock sterowana metadanymi:
    • W oknie głównym MainWindow.xaml zdefiniuj układ DockingManager zawierają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 metadanej DefaultLocation (np. "Document" vs "Right" / "Bottom") automatycznie opakuj ich widoki w obiekty LayoutDocument lub LayoutAnchorable i dodaj do odpowiednich paneli.

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:

  1. (15 pkt) Wtyczka 1 – Generator / Edytor Strumienia Danych (DefaultLocation = "Document"):
    • Dostarcza widok UserControl umieszczany w centralnej zakładce dokumentów AvalonDock.
    • 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ługi IEventAggregator oraz IHostContext. Po kliknięciu przycisku loguje akcję w hoście i publikuje zdarzenie DataPayloadPublishedEvent w agregatorze zdarzeń.
  2. (15 pkt) Wtyczka 2 – Panel Statystyk, Wizualizacji i Audytu (DefaultLocation = "Right"):
    • Dostarcza widok UserControl dokowany automatycznie w bocznym panelu narzędziowym LayoutAnchorablePane.
    • Subskrybuje zdarzenie DataPayloadPublishedEvent za pośrednictwem IEventAggregator.
    • 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/ItemsControl dla serii liczbowej).

Część 4: Zarządzanie Zakresami ([Shared("...")] + ExportFactory<T>) (15 pkt)

  1. Zademonstruj praktyczne użycie cyklu życia Scoped w MEF 2.0:
    • Zdefiniuj klasę kontekstu sesji analizy (np. AnalysisSessionContext z unikalnym identyfikatorem Guid SessionId i znacznikiem czasu), oznaczoną atrybutami [Export] oraz [Shared("AnalysisScope")].
    • Zdefiniuj klasę procesora/audytora (np. BatchAuditor / AnomalyDetector), która importuje AnalysisSessionContext i weryfikuje, czy w przesłanej paczce danych występują anomalie (np. wartości przekraczające próg alarmowy lub słowa kluczowe ERROR / 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 bloku using (var scope = AuditorFactory.CreateExport()), wyświetlając unikalny SessionId w interfejsie lub logu.

Część 5: Dynamiczne Monitorowanie Katalogu Wtyczek (FileSystemWatcher) (15 pkt)

  1. (10 pkt) Obsługa FileSystemWatcher w czasie rzeczywistym:
    • Skonfiguruj w aplikacji hosta instancję FileSystemWatcher monitorującą katalog Plugins pod kątem plików *.dll (zdarzenia Created oraz Deleted).
    • 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 .dll w Eksploratorze Windows podczas działania aplikacji.
  2. (5 pkt) Bezpieczeństwo wątkowe interfejsu (Thread Affinity):
    • Przekieruj odświeżenie kontenera MEF i przebudowę kolekcji paneli AvalonDock do głównego wątku UI za pomocą Application.Current.Dispatcher.Invoke, tak aby dodanie lub usunięcie pliku .dll z folderu Plugins natychmiast aktualizowało widoczne panele w oknie bez rzucania wyjątku InvalidOperationException.

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