Języki programowania na platformie .NET / Programowanie .NET

Rok akademicki: 2026/27

Instrukcja laboratoryjna nr 5: Język C++/CLI w .NET 8/9+ (/clr:netcore), Interfejs WPF (MVVM), Kompilator Roslyn (Microsoft.CodeAnalysis) oraz Refleksja (System.Reflection)

Prowadzący: Tomasz Goluch
Wersja: 2.0


1. Wprowadzenie: Rola Języka C++/CLI w Ekosystemie .NET oraz Architektura Projektu

Cel: Zrozumienie miejsca języka C++/CLI na platformie .NET, różnic między trybami kompilacji /clr a /clr:netcore oraz poznanie składni zarządzanego C++/CLI poprzez budowę nietypowej aplikacji łączącej WPF (MVVM), kompilator Roslyn oraz mechanizm Refleksji.

C++/CLI (ang. Common Language Infrastructure) to opracowane przez Microsoft rozszerzenie standardowego języka C++ (ustandaryzowane jako ECMA-372), które pozwala w jednym pliku źródłowym łączyć kod natywny (niezarządzany) z kodem zarządzanym przez środowisko CLR (.NET).

Prawdziwe zastosowanie przemysłowe vs cel dydaktyczny laboratorium

W projektach komercyjnych podstawowym zastosowaniem C++/CLI jest budowa tzw. warstw pośrednich (ang. Wrappers / Interop Bridges) w technologii IJW (It Just Works). Pozwala ona wystawiać klasy zarządzane (ref class), które pod spodem wywołują wysokowydajne biblioteki natywne C/C++ (np. silniki fizyczne, biblioteki kryptograficzne, OpenCV czy sterowniki sprzętowe) bez konieczności ręcznego definiowania dziesiątek deklaracji P/Invoke ([DllImport]).

W ramach niniejszego laboratorium wykorzystamy jednak C++/CLI w celowo nietypowej roli: zbudujemy w nim kompletną warstwę aplikacyjną WPF z wzorcem MVVM, która dynamicznie w pamięci RAM kompiluje kod źródłowy Visual Basic .NET za pomocą kompilatora Roslyn (Microsoft.CodeAnalysis), a następnie bada i uruchamia skompilowany kod przy użyciu Refleksji (System.Reflection). Takie podejście stanowi przekrojowy poligon doświadczalny, pozwalający w praktyce opanować wszystkie kluczowe konstrukcje zarządzanego C++/CLI (ref class, uchwyty ^, gcnew, właściwości property, zdarzenia event, delegaty, tablice cli::array oraz jawną implementację interfejsów).

 ┌────────────────────────────────────────────────────────────────────────────────────────┐
 │ 1. Projekt Startowy C# (CppCliWpfRoslyn.Launcher.exe | net8.0-windows)                 │
 │    • Ustawia jednowątkowy apartament COM (ApartmentState.STA) wymagany przez WPF       │
 │    • Wywołuje metodę wejściową: CppCliEngine::Start::WinMain()                         │
 └───────────────────────────────────────────┬────────────────────────────────────────────┘
                                             │ Referencja do projektu (.dll)
                                             ▼
 ┌────────────────────────────────────────────────────────────────────────────────────────┐
 │ 2. Biblioteka Główna C++/CLI (CppCliWpfRoslyn.Engine.dll | /clr:netcore)               │
 │  ┌────────────────────────────────────────┐  ┌──────────────────────────────────────┐  │
 │  │ Warstwa Prezentacji WPF (MVVM)         │  │ Silnik Kompilacji i Introspekcji     │  │
 │  │ • Widok ładowany dynamicznie w locie   │  │ • Kompilator Roslyn (VB.NET -> IL):  │  │
 │  │   przez XamlReader::Load("...xaml")    │  │   SyntaxFactory -> VisualBasicComp.  │  │
 │  │ • ViewModel: ref class DataModel       │─►│ • Emisja do RAM: MemoryStream        │  │
 │  │   (INotifyPropertyChanged + ICommand)  │  │ • Assembly::Load(ms->ToArray())      │  │
 │  │ • Zero dostępu przez x:Name (100% Bind)│  │ • System.Reflection (Invoke/Get/Set) │  │
 │  └────────────────────────────────────────┘  └──────────────────────────────────────┘  │
 └────────────────────────────────────────────────────────────────────────────────────────┘

Porównanie konstrukcji składniowych: C# vs Klasyczny C++ vs Zarządzany C++/CLI

W C++/CLI standardowe typy natywne C++ (alokowane na stosie lub stercie natywnej za pomocą new/delete) mogą współistnieć z typami zarządzanymi (alokowanymi na stercie odśmiecania pamięci Garbage Collector za pomocą operatora gcnew). Do rozróżnienia wskaźnika natywnego (*) od śledzonej przez GC referencji zarządzanej służy symbol uchwytu (^, ang. handle to object on managed heap).

Koncepcja Język C# Natywny C++ (ISO) Zarządzany C++/CLI (.NET)
Deklaracja klasy referencyjnej public class MyClass class MyClass public ref class MyClass
Deklaracja struktury (wartościowej) public struct MyStruct struct MyStruct public value class MyStruct (lub value struct)
Referencja / Wskaźnik do obiektu MyClass obj MyClass* ptr MyClass^ handle (uchwyt śledzony przez GC)
Alokacja na stercie new MyClass() new MyClass() (wymaga delete) gcnew MyClass() (zwalniane przez GC)
Wartość pusta (brak obiektu) null nullptr nullptr
Dostęp do składowych obiektu obj.Method() ptr->Method() handle->Method()
Dostęp do składowych statycznych MyClass.StaticMethod() MyClass::StaticMethod() MyClass::StaticMethod()
Obiekt metadanych typu (System.Type) typeof(string) typeid(std::string) (type_info) String::typeid
Tablice jednowymiarowe string[] arr = new string[5]; std::string* arr = new std::string[5]; cli::array<String^>^ arr = gcnew cli::array<String^>(5);
Deterministyczne zwalnianie (IDisposable) using (var s = ...) lub s.Dispose() Destruktor ~MyClass() (RAII) delete handle; (wywołuje metodę Dispose()!)

🧠 Destruktory (~Klasa) a Finalizatory (!Klasa) w C++/CLI:
W C++/CLI zdefiniowanie destruktora ~MyClass() w klasie ref class powoduje automatyczną implementację interfejsu IDisposable, a wywołanie operatora delete uchwyt; na obiekcie zarządzanym nie zwalnia bezpośrednio pamięci RAM, lecz wywołuje metodę Dispose()! Z kolei odpowiednikiem finalizatora wywoływanego niedeterministycznie przez Garbage Collector jest składnia !MyClass().


2. Konfiguracja Projektu C++/CLI (/clr:netcore) z Obsługą WPF w .NET 8/9+

Cel: Skonfigurowanie projektu biblioteki C++/CLI dla nowoczesnego środowiska .NET 8.0 / 9.0 (/clr:netcore), podłączenie frameworka graficznego WPF oraz utworzenie aplikacji uruchomieniowej (Launchera).

Domyślne szablony projektów C++/CLI w Visual Studio nie zawierają kreatorów dla interfejsu graficznego WPF (a od wersji VS 2012 wycofano również szablony Windows Forms). Choć w typowych systemach warstwę GUI tworzy się w C#, a kod C++/CLI dołącza jako bibliotekę pomocniczą, platforma .NET pozwala na pełną obsługę okien XAML bezpośrednio z poziomu kodu C++/CLI.

1. Wybór środowiska uruchomieniowego CLR w Visual Studio

W oknie Właściwości projektu → Właściwości konfiguracji → Zaawansowane → Obsługa środowiska uruchomieniowego języka wspólnego (CLR Support) dostępne są dwa główne tryby:

  1. Obsługa środowiska uruchomieniowego platformy .NET (/clr:netcore) — przeznaczona dla nowoczesnych aplikacji wieloplatformowych i desktopowych .NET 6.0 / 8.0 / 9.0+ (wykorzystywana w laboratorium).
  2. Obsługa środowiska uruchomieniowego .NET Framework (/clr) — przeznaczona wyłącznie dla starszych aplikacji utrzymaniowych opartych o klasyczny .NET Framework 4.8.1.

2. Modyfikacja pliku projektu C++/CLI (*.vcxproj)

W celu włączenia obsługi kontrolek WPF w projekcie C++/CLI otwieramy plik *.vcxproj w edytorze tekstu. Upewniamy się, że jako docelowy framework ustawiono wariant z obsługą systemu Windows (net8.0-windows lub net9.0-windows) oraz dodajemy grupę <FrameworkReference> wskazującą na Microsoft.WindowsDesktop.App.WPF:

<PropertyGroup Label="Globals">
  <VCProjectVersion>17.0</VCProjectVersion>
  <Keyword>NetCoreCProj</Keyword>
  <ProjectGuid>{964536fa-6356-4390-b24f-a90306c24256}</ProjectGuid>
  <RootNamespace>CppCLIWPFCodeDOM</RootNamespace>
  <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
  <!-- KRYTYCZNE: Dla WPF w .NET 8/9 wymagany jest sufiks -windows -->
  <TargetFramework>net8.0-windows</TargetFramework>
  <WindowsTargetPlatformMinVersion>7.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>

<!-- Dodanie referencji do bibliotek WPF (PresentationFramework, PresentationCore, WindowsBase) -->
<ItemGroup>
  <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>

3. Dlaczego potrzebujemy projektu startowego (Launchera) w C#?

W trybie nowoczesnego .NET (/clr:netcore) kompilator Microsoft C++ wspiera wytwarzanie wyłącznie bibliotek dynamicznych (.dll) — próba zmiany typu wyjściowego projektu C++/CLI na .exe zakończy się błędem kompilacji MSB8047.

Aby uruchomić naszą bibliotekę C++/CLI zawierającą logikę WPF, w rozwiązaniu umieszczamy mikro-projekt startowy w języku C# (np. Launcher.csproj z <TargetFramework>net8.0-windows</TargetFramework>), który posiada referencję do projektu C++/CLI.

⚠️ Wymóg wątku STA (Single-Threaded Apartment) w WPF:
Wszystkie kontrolki WPF bazują wewnętrznie na architekturze komunikatów powłoki Windows i modelu apartamentów COM. Wątek tworzący obiekt Application oraz Window musi działać w trybie ApartmentState.STA. Próba utworzenia okna WPF w domyślnym wątku MTA rzuci wyjątek InvalidOperationException: The calling thread must be STA, because many UI components require this.

Kod uruchomieniowy w projekcie C# (Program.cs) ustawiający tryb STA i przekazujący sterowanie do biblioteki C++/CLI wygląda następująco:

using System.Threading;

internal static class Program
{
    private static void Main()
    {
        // Jawne przełączenie bieżącego wątku w tryb Single-Threaded Apartment (STA) przed inicjalizacją WPF
        Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown);
        Thread.CurrentThread.SetApartmentState(ApartmentState.STA);

        // Wywołanie statycznej metody startowej z biblioteki C++/CLI
        Start.WinMain();
    }
}

4. Dynamiczne ładowanie widoku XAML za pomocą XamlReader::Load w Main.cpp

W projekcie C++/CLI nie korzystamy z prekompilatorów plików BAML generujących klasy częściowe (partial class), lecz wczytujemy plik MainWindow.xaml bezpośrednio ze strumienia dyskowego w czasie uruchomienia przy użyciu klasy System::Windows::Markup::XamlReader.

Tworzymy plik Main.cpp inicjalizujący aplikację WPF i spinający widok (Window^) z modelem widoku (DataModel^):

#include <windows.h>
#include "DataModel.h"

using namespace System;
using namespace System::IO;
using namespace System::Windows;
using namespace System::Windows::Markup;

public ref class MyApplication : public Application
{
    DataModel^ h_dm;
    String^ pathToVBCode;
    String^ code;

public:
    MyApplication(Window^ win)
    {
        // Wczytanie domyślnego kodu źródłowego VB.NET z pliku tekstowego
        pathToVBCode = gcnew String("..\\..\\..\\..\\VBCode\\Class1.vb");
        code = File::Exists(pathToVBCode) ? File::ReadAllText(pathToVBCode) : "";

        // Utworzenie instancji ViewModelu i przypisanie jej do DataContext okna głównego
        h_dm = gcnew DataModel(code);
        win->DataContext = h_dm;
    }
};

public ref class Start abstract sealed // Odpowiednik 'static class' w C++/CLI
{
public:
    static void WinMain()
    {
        // Otwarcie strumienia z definicją interfejsu XAML
        Stream^ st = File::OpenRead("MainWindow.xaml");

        // UWAGA: Wywołujemy jednoargumentowe przeciążenie XamlReader::Load(Stream^)!
        // Przekazanie nullptr jako drugiego argumentu (ParserContext^) rzuci ArgumentNullException!
        Window^ win = safe_cast<Window^>(XamlReader::Load(st));
        delete st; // Deterministyczne zamknięcie strumienia (odpowiednik st.Dispose())

        Application^ app = gcnew MyApplication(win);
        app->Run(win);
    }
};

3. Implementacja Wzorca MVVM w C++/CLI: INotifyPropertyChanged, Właściwości i ICommand

Cel: Zaimplementowanie w C++/CLI kompletnego modelu widoku (ViewModel) wspierającego dwukierunkowe wiązanie danych (Two-Way Data Binding), kolekcje obserwowalne (ObservableCollection<T>) oraz komendy przycisków (ICommand) bez odwoływania się do kontrolek po nazwie (x:Name).

1. Deklaracja szkieletu okna w MainWindow.xaml

Plik MainWindow.xaml (kopiowany do katalogu wyjściowego projektu) stanowi czysty opis widoku, w którym korzeniem jest element <Window> bez atrybutu x:Class:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow - C++/CLI Roslyn Dynamic Compiler"
        Height="585" Width="620">
    <Grid Margin="10">
        <!-- Kontrolki interfejsu powiązane z ViewModelem poprzez {Binding ...} -->
    </Grid>
</Window>

🧠 Pułapka bindowania kodu źródłowego: TextBox vs RichTextBox w czystym MVVM:
W WPF kontrolka <RichTextBox> przechowuje treść w obiekcie Document typu FlowDocument, który nie jest właściwością zależności (DependencyProperty) i nie pozwala na bezpośredni zapis <RichTextBox Text="{Binding CodeText}"/>.
Ponieważ w zadaniu zabronione jest używanie atrybutu x:Name i pobieranie referencji do kontrolek w kodzie, najczystszym rozwiązaniem w XAML jest użycie wielowierszowej kontrolki <TextBox>:

<TextBox Text="{Binding CodeText, UpdateSourceTrigger=PropertyChanged}"
         AcceptsReturn="True"
         AcceptsTab="True"
         VerticalScrollBarVisibility="Auto"
         HorizontalScrollBarVisibility="Auto"
         FontFamily="Consolas" FontSize="13" />

(Sposób podpięcia RichTextBox za pomocą własnej właściwości doczepianej AttachedProperty w C++/CLI opisano w sekcji rozszerzonej na końcu rozdziału).

2. Poprawna implementacja INotifyPropertyChanged w C++/CLI (plik DataModel.h)

W C++/CLI słowo kluczowe event automatycznie generuje delegat przechowujący listę subskrybentów oraz trzy metody dostępowe: add, remove oraz raise.

⚠️ Najczęstszy błąd w C++/CLI przy INotifyPropertyChanged:
Ręczne zadeklarowanie pustych bloków void add(PropertyChangedEventHandler^ value) { } oraz void remove(...) { } powoduje, że silnik wiązania danych WPF rejestruje subskrypcję „w próżnię” i nigdy nie otrzymuje powiadomień o zmianie wartości właściwości w kodzie C++/CLI!
Zamiast pustych akcesorów używamy domyślnej deklaracji virtual event PropertyChangedEventHandler^ PropertyChanged; oraz metody pomocniczej OnPropertyChanged(String^ propertyName).

// Plik: DataModel.h
#pragma once
#include <windows.h>

using namespace System;
using namespace System::Collections::Generic;
using namespace System::Collections::ObjectModel;
using namespace System::ComponentModel;
using namespace System::Windows::Input;

public ref class DataModel : public INotifyPropertyChanged
{
#pragma region Private Fields
    String^ _codeText;
    ICollection<String^>^ _errorsList = gcnew ObservableCollection<String^>();
    ICommand^ _addCodeCommand;
#pragma endregion

protected:
    // Metoda pomocnicza wyzwalająca zdarzenie PropertyChanged (odświeża powiązane kontrolki w GUI)
    void OnPropertyChanged(String^ propertyName)
    {
        PropertyChanged(this, gcnew PropertyChangedEventArgs(propertyName));
    }

public:
#pragma region Public Constructors
    DataModel(String^ codeText);
#pragma endregion

#pragma region Public Events
    // Kompilator C++/CLI automatycznie generuje pole delegatu oraz metody add, remove i raise!
    virtual event PropertyChangedEventHandler^ PropertyChanged;
#pragma endregion

#pragma region Public Properties
    property String^ CodeText
    {
        String^ get()
        {
            return _codeText;
        }
        void set(String^ value)
        {
            if (_codeText != value)
            {
                _codeText = value;
                OnPropertyChanged("CodeText");
            }
        }
    }

    property ICollection<String^>^ ErrorsList
    {
        ICollection<String^>^ get()
        {
            return _errorsList;
        }
        void set(ICollection<String^>^ value)
        {
            _errorsList = value;
            OnPropertyChanged("ErrorsList");
        }
    }

    property ICommand^ AddCodeCommand
    {
        ICommand^ get()
        {
            if (_addCodeCommand == nullptr)
            {
                _addCodeCommand = gcnew NavigateToAddCodeCommand(this);
            }
            return _addCodeCommand;
        }
        void set(ICommand^ value)
        {
            _addCodeCommand = value;
        }
    }
#pragma endregion

#pragma region Public Nested Classes (Commands)
    ref class NavigateToAddCodeCommand : public ICommand
    {
        DataModel^ _viewModel;

    public:
        NavigateToAddCodeCommand(DataModel^ viewModel);

        virtual bool CanExecute(Object^ parameter) = ICommand::CanExecute;
        virtual void Execute(Object^ parameter) = ICommand::Execute;

        virtual event EventHandler^ CanExecuteChanged;
    };
#pragma endregion
};

3. Definicja konstruktora i komendy w pliku DataModel.cpp

W pliku implementacyjnym DataModel.cpp definiujemy ciała metod zadeklarowanych w pliku nagłówkowym:

// Plik: DataModel.cpp
#include "DataModel.h"

DataModel::DataModel(String^ codeText)
{
    // Przypisanie przekazanego kodu startowego do pola prywatnego
    _codeText = codeText;
}

DataModel::NavigateToAddCodeCommand::NavigateToAddCodeCommand(DataModel^ viewModel)
{
    _viewModel = viewModel;
}

bool DataModel::NavigateToAddCodeCommand::CanExecute(Object^ parameter)
{
    return true;
}

void DataModel::NavigateToAddCodeCommand::Execute(Object^ parameter)
{
    _viewModel->ErrorsList->Clear();
    _viewModel->ErrorsList->Add("Not implemented yet");
}

Powiązanie przycisku oraz listy błędów w pliku MainWindow.xaml:

<Button Command="{Binding AddCodeCommand}" Content="Compile" Width="120" Height="28" />
<ListBox ItemsSource="{Binding ErrorsList}" Height="120" />
💡 Zaawansowane: Uniwersalna klasa RelayCommand w C++/CLI oraz obsługa RichTextBox bez x:Name (Kliknij, aby rozwinąć/zwinąć)

1. Uniwersalna klasa RelayCommand oparta na delegatach Action<Object^>^

Aby nie tworzyć osobnej klasy zagnieżdżonej dla każdego przycisku w interfejsie (Compile, INVOKE, SET), można stworzyć jedną reużywalną klasę RelayCommand przyjmującą wskaźnik na metodę modelu widoku za pomocą delegatu Action<Object^>^:

public ref class RelayCommand : public ICommand
{
    Action<Object^>^ _execute;
    Predicate<Object^>^ _canExecute;

public:
    RelayCommand(Action<Object^>^ execute, Predicate<Object^>^ canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    RelayCommand(Action<Object^>^ execute) : RelayCommand(execute, nullptr) {}

    virtual bool CanExecute(Object^ parameter)
    {
        return _canExecute == nullptr ? true : _canExecute(parameter);
    }

    virtual void Execute(Object^ parameter)
    {
        _execute(parameter);
    }

    virtual event EventHandler^ CanExecuteChanged
    {
        void add(EventHandler^ handler) { CommandManager::RequerySuggested += handler; }
        void remove(EventHandler^ handler) { CommandManager::RequerySuggested -= handler; }
    }
};

Inicjalizacja komendy w konstruktorze DataModel staje się wówczas jednolinijkowa:

_compileCommand = gcnew RelayCommand(gcnew Action<Object^>(this, &DataModel::CompileCode));

2. Przekazanie dokumentu RichTextBox przez CommandParameter w XAML

Jeśli zamiast <TextBox AcceptsReturn="True"> chcesz użyć w XAML kontrolki <RichTextBox> bez nadawania jej atrybutu x:Name, możesz przekazać jej właściwość Document bezpośrednio jako parametr komendy CommandParameter przy użyciu wiązania RelativeSource:

<RichTextBox>
    <FlowDocument>
        <Paragraph>
            <Run Text="{Binding CodeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </Paragraph>
    </FlowDocument>
</RichTextBox>

Umieszczenie znacznika <Run Text="{Binding CodeText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> wewnątrz <Paragraph> dokumentu RichTextBox w .NET 8/9 pozwala na natywne dwukierunkowe wiązanie tekstu w RichTextBox bez ani jednej linijki kodu pomocniczego!


4. Kompilacja Kodu w Locie: Od Przestarzałego CodeDOM do Kompilatora Roslyn (Microsoft.CodeAnalysis)

Cel: Zrozumienie ewolucji dynamicznej kompilacji kodu w .NET oraz opanowanie specyfiki wywoływania kompilatora Roslyn (VisualBasicCompilation) z poziomu języka C++/CLI.

1. Dlaczego CodeDOM nie działa w nowoczesnym .NET 8/9+?

W klasycznym .NET Framework 1.0–4.8 standardowym mechanizmem generowania i kompilowania kodu w czasie działania programu był CodeDOM (System.CodeDom.Compiler). Tworzono w nim obiekt CodeSnippetCompileUnit, ustawiano CompilerParameters (GenerateInMemory = true) i wywoływano metodę CodeDomProvider::CreateProvider("VisualBasic")->CompileAssemblyFromDom(param, csu) (która w rzeczywistości uruchamiała w tle zewnętrzny proces vbc.exe na plikach tymczasowych).

W nowoczesnym środowisku .NET Core / .NET 6/8/9+ wywołanie metody CompileAssemblyFromDom zawsze rzuca wyjątek PlatformNotSupportedException. Kompilatory języków C# i Visual Basic zostały całkowicie przepisane w ramach projektu Roslyn (Microsoft.CodeAnalysis) zgodnie z paradygmatem Compiler as a Service — udostępniając pełne API analizy składniowej, semantycznej i emisji kodu IL bezpośrednio w pamięci procesu.

Cecha CodeDOM (.NET Framework – Legacy) Kompilator Roslyn (.NET 8/9+ – używany w lab.)
Główna przestrzeń nazw System.CodeDom.Compiler Microsoft.CodeAnalysis / Microsoft.CodeAnalysis.VisualBasic
Sposób działania Uruchamia zewnętrzny proces csc.exe / vbc.exe na dysku. Działa w 100% wewnątrz pamięci RAM procesu (niemutowalne drzewa).
Wsparcie w .NET 6.0 / 8.0 / 9.0+ Brak (CompileAssemblyFromDom rzuca PlatformNotSupportedException). Pełne wsparcie (oficjalny kompilator platformy .NET).
Wspierane języki C#, Visual Basic, JScript (w .NET Framework). C# (CSharpCompilation) oraz Visual Basic (VisualBasicCompilation). (F# posiada osobny kompilator FSharp.Compiler.Service).
Wynik kompilacji w pamięci CompilerResults->CompiledAssembly compilation->Emit(memoryStream) ➔ EmitResult (Success, Diagnostics).

2. Instalacja pakietu NuGet Microsoft.CodeAnalysis.VisualBasic

Aby korzystać z kompilatora Roslyn dla języka VB.NET, instalujemy pakiet NuGet Microsoft.CodeAnalysis.VisualBasic zarówno w projekcie biblioteki C++/CLI (.vcxproj), jak i w projekcie startowym C# (.csproj), aby wszystkie zależności uruchomieniowe Roslyna zostały skopiowane do folderu wyjściowego bin:

<!-- W pliku .vcxproj oraz .csproj -->
<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.11.0" />
</ItemGroup>

3. Pułapki wywoływania API Roslyn w C++/CLI i jak je rozwiązać!

API kompilatora Roslyn zostało zaprojektowane w języku C# z bardzo szerokim wykorzystaniem parametrów opcjonalnych (domyślnych), w tym domyślnych wartości struktur (np. CancellationToken cancellationToken = default, MetadataReferenceProperties properties = default).

⚠️ KRYTYCZNA PUŁAPKA C++/CLI (cl.exe vs parametry domyślne z C#):
Kompilator C++/CLI nie obsługuje automatycznego podstawiania wartości domyślnych dla parametrów opcjonalnych będących strukturami (value class) zdefiniowanych w bibliotekach C#!
Oznacza to, że wywołania znane z przykładów C#, takie jak:

nie skompilują się w C++/CLI, jeśli nie przekażemy jawnie wymaganych argumentów lub nie użyjemy odpowiednich metod pomocniczych!

Poniżej znajduje się kompletny, w 100% poprawny i kompilujący się w C++/CLI (.NET 8/9+) proces kompilacji kodu VB.NET krok po kroku:

using namespace System::Text;
using namespace System::Threading;
using namespace System::Reflection;
using namespace Microsoft::CodeAnalysis;
using namespace Microsoft::CodeAnalysis::Emit;
using namespace Microsoft::CodeAnalysis::Text;
using namespace Microsoft::CodeAnalysis::VisualBasic;
Krok 1: Utworzenie obiektu SourceText oraz drzewa składniowego SyntaxTree

Metoda SourceText::From oraz SyntaxFactory::ParseSyntaxTree w C++/CLI wymagają jawnego przekazania parametrów domyślnych (w tym SourceHashAlgorithm::Sha1 oraz CancellationToken::None):

// 1. Opakowanie łańcucha znaków z kodem VB w obiekt SourceText
SourceText^ sourceText = SourceText::From(
    _codeText,
    Encoding::UTF8,
    SourceHashAlgorithm::Sha1
);

// 2. Parsowanie kodu źródłowego do drzewa składniowego (SyntaxTree)
SyntaxTree^ syntaxTree = SyntaxFactory::ParseSyntaxTree(
    sourceText,
    nullptr,                 // ParseOptions^ (nullptr = domyślne opcje VB)
    "",                      // ścieżka pliku źródłowego (opcjonalna nazwa do diagnostyki)
    CancellationToken::None  // jawne przekazanie struktury CancellationToken!
);
Krok 2: Zbudowanie listy referencji metadanych (MetadataReference) dla .NET 8/9+

W nowoczesnym .NET 8/9+ podstawowe typy systemowe oraz funkcje uruchomieniowe języka Visual Basic są rozdzielone pomiędzy kilka kluczowych bibliotek systemowych (System.Private.CoreLib, System.Runtime, Microsoft.VisualBasic.Core, System.Console). W C++/CLI metoda MetadataReference::CreateFromFile wymaga 3 argumentów:

// 3. Przygotowanie referencji do bibliotek bazowych .NET 8/9 oraz środowiska Visual Basic
List<MetadataReference^>^ references = gcnew List<MetadataReference^>();

cli::array<String^>^ requiredAssemblies = gcnew cli::array<String^> {
    Object::typeid->Assembly->Location,                        // System.Private.CoreLib.dll
    Assembly::Load("System.Runtime")->Location,                // System.Runtime.dll
    Assembly::Load("Microsoft.VisualBasic.Core")->Location,    // Wymagane przez kompilator VB.NET!
    Assembly::Load("System.Console")->Location,                // System.Console.dll
    Assembly::Load("System.Collections")->Location             // System.Collections.dll
};

for each (String^ asmPath in requiredAssemblies)
{
    references->Add(MetadataReference::CreateFromFile(
        asmPath,
        MetadataReferenceProperties(), // Jawne utworzenie domyślnej struktury właściwości
        nullptr                        // DocumentationProvider^
    ));
}
Krok 3: Utworzenie obiektu VisualBasicCompilation i ustawienie OutputKind

Aby ominąć problem 18-parametrowego konstruktora klasy VisualBasicCompilationOptions w C++/CLI, tworzymy kompilację z domyślnymi opcjami (nullptr), a następnie modyfikujemy wybrany przez użytkownika w ComboBoxie rodzaj wyjścia (OutputKind) za pomocą jednoargumentowej metody niemutowalnej WithOutputKind(...):

// 4. Utworzenie obiektu kompilacji Visual Basic
cli::array<SyntaxTree^>^ syntaxTrees = gcnew cli::array<SyntaxTree^> { syntaxTree };

VisualBasicCompilation^ compilation = VisualBasicCompilation::Create(
    "DynamicVBModule", // Nazwa generowanego zestawu (Assembly Name)
    syntaxTrees,       // Tablica drzew składniowych
    references,        // Kolekcja referencji MetadataReference
    nullptr            // Przekazujemy nullptr, aby Roslyn sam utworzył domyślne opcje w C#
);

// 5. Ustawienie wybranego w GUI typu wyjścia (np. DynamicallyLinkedLibrary) bez wywoływania 18-arg konstruktora!
OutputKind selectedKind = OutputKind::DynamicallyLinkedLibrary; // docelowo pobrane z właściwości zbindowanej z ComboBoxem
compilation = compilation->WithOptions(
    compilation->Options->WithOutputKind(selectedKind)
);
Krok 4: Emisja kodu pośredniego CIL do strumienia MemoryStream i obsługa błędów

Wywołujemy metodę Emit, przekazując strumień MemoryStream^ oraz wymagane przez C++/CLI argumenty nullptr wraz z CancellationToken::None na końcu:

// 6. Emisja skompilowanego kodu IL do pamięci RAM (MemoryStream)
MemoryStream^ ms = gcnew MemoryStream();

EmitResult^ emitResult = compilation->Emit(
    ms,                      // peStream - główny strumień wyjściowy dla kodu IL
    nullptr,                 // pdbStream
    nullptr,                 // xmlDocumentationStream
    nullptr,                 // win32Resources
    nullptr,                 // manifestResources
    nullptr,                 // EmitOptions^
    nullptr,                 // debugEntryPoint
    nullptr,                 // sourceLinkStream
    nullptr,                 // embeddedTexts
    nullptr,                 // metadataPEStream
    CancellationToken::None  // CancellationToken
);

if (!emitResult->Success)
{
    // Kompilacja nie powiodła się - odczytujemy kolekcję Diagnostics i wypisujemy błędy do ErrorsList
    for each (Diagnostic^ diag in emitResult->Diagnostics)
    {
        if (diag->Severity == DiagnosticSeverity::Error)
        {
            _errorsList->Add(diag->ToString());
        }
    }
}
else
{
    // Sukces! Strumień 'ms' zawiera gotowy obraz binarny skompilowanego zestawu .NET
    ms->Seek(0, SeekOrigin::Begin);
    Assembly^ compiledAssembly = Assembly::Load(ms->ToArray());
    // ... przejście do inspekcji za pomocą Refleksji ...
}
delete ms;

5. Mechanizm Refleksji (System.Reflection): Introspekcja Typów, Wywoływanie Metod i Manipulacja Polami

Cel: Opanowanie ładowania skompilowanego w pamięci zestawu Assembly, odkrywania jego klas, metod (MethodInfo), właściwości (PropertyInfo) i pól (FieldInfo), tworzenia instancji (Activator::CreateInstance) oraz dynamicznego wywoływania metod i modyfikacji stanu obiektu.

Gdy kompilator Roslyn pomyślnie wyemituje kod pośredni CIL do obiektu MemoryStream^, ładujemy go do bieżącej domeny aplikacji za pomocą Assembly::Load(ms->ToArray()). Od tego momentu możemy badać jego zawartość i komunikować się ze skompilowanym obiektem za pomocą przestrzeni nazw System::Reflection.

 [ MemoryStream (Binarny obraz PE/CIL w pamięci RAM) ]
                         │
                         ▼  Assembly::Load(ms->ToArray())
            ┌─────────────────────────┐
            │    System::Reflection   │
            │        Assembly^        │
            └────────────┬────────────┘
                         │  compiledAssembly->GetTypes()[0] (lub GetType("NazwaKlasy"))
                         ▼
            ┌─────────────────────────┐          Activator::CreateInstance(type)
            │      System::Type^      │ ─────────────────────────────────────────────► [ Object^ _instance ]
            └────────────┬────────────┘                                                  (Żywa instancja
                         │                                                                klasy VB w RAM)
       ┌─────────────────┼──────────────────────┐
       ▼                 ▼                      ▼
 [ MethodInfo^ ]   [ FieldInfo^ ]       [ PropertyInfo^ ]
  • GetMethods()    • GetFields()        • GetProperties()
  • Invoke(inst,    • GetValue(inst)     • GetValue(inst)
    paramsArray)    • SetValue(inst, v)  • SetValue(inst, v)

1. Pobieranie typu klasy i tworzenie żywej instancji obiektu (Activator::CreateInstance)

Aby nie uzależniać naszego programu od sztywno zakodowanej nazwy przestrzeni nazw w kompilowanym pliku VB.NET, możemy pobrać pierwszy publiczny lub zdefiniowany w zestawie typ za pomocą GetTypes(), a następnie powołać jego instancję za pomocą statycznej metody Activator::CreateInstance:

// Pobranie pierwszego typu (klasy) ze skompilowanego zestawu
cli::array<Type^>^ types = compiledAssembly->GetTypes();
if (types->Length > 0)
{
    Type^ targetType = types[0];

    // Powołanie żywej instancji skompilowanej klasy VB.NET na stercie zarządzanej
    Object^ instance = Activator::CreateInstance(targetType);
}

2. Odkrywanie metod (GetMethods), pól (GetFields) oraz właściwości (GetProperties)

Obiekt klasy Type^ udostępnia metody pozwalające pobrać tablice metadanych opisujących składowe klasy. Aby pominąć standardowe metody odziedziczone z klasy bazowej System.Object (takie jak ToString, Equals, GetHashCode, GetType) lub uwzględnić wyłącznie składowe zadeklarowane bezpośrednio w kompilowanej klasie, stosujemy flagi BindingFlags:

BindingFlags flags = BindingFlags::Public | BindingFlags::NonPublic | BindingFlags::Instance | BindingFlags::DeclaredOnly;

// 1. Pobranie metod zadeklarowanych w klasie VB
cli::array<MethodInfo^>^ methods = targetType->GetMethods(flags);
for each (MethodInfo^ m in methods)
{
    // Pominięcie ukrytych metod dostępowych właściwości (get_... / set_...)
    if (!m->IsSpecialName)
    {
        _methodsList->Add(m->Name);
    }
}

// 2. Pobranie pól (Fields) zadeklarowanych w klasie VB
cli::array<FieldInfo^>^ fields = targetType->GetFields(flags);
for each (FieldInfo^ f in fields)
{
    _variablesList->Add(f->Name);
}

// 3. Pobranie właściwości (Properties) zadeklarowanych w klasie VB
cli::array<PropertyInfo^>^ props = targetType->GetProperties(flags);
for each (PropertyInfo^ p in props)
{
    _variablesList->Add(p->Name);
}

3. Dynamiczne wywoływanie wybranej metody (MethodInfo::Invoke)

Metodę reprezentowaną przez obiekt MethodInfo^ uruchamiamy za pomocą metody Invoke(Object^ obj, cli::array<Object^>^ parameters):

MethodInfo^ selectedMethod = targetType->GetMethod(selectedMethodName, flags);
if (selectedMethod != nullptr)
{
    cli::array<ParameterInfo^>^ paramInfos = selectedMethod->GetParameters();
    cli::array<Object^>^ callArgs = nullptr;

    if (paramInfos->Length == 1)
    {
        Type^ expectedType = paramInfos[0]->ParameterType;
        Object^ convertedArg = Convert::ChangeType(_parameterText, expectedType);
        callArgs = gcnew cli::array<Object^> { convertedArg };
    }

    // Wywołanie metody i przechwycenie zwróconego wyniku
    Object^ returnValue = selectedMethod->Invoke(_compiledInstance, callArgs);
    ResultText = (returnValue != nullptr) ? returnValue->ToString() : "void / brak wyniku";
}

4. Odczyt i zapis wartości pola (FieldInfo) lub właściwości (PropertyInfo)

Do pobrania oraz ustawienia wartości pola lub właściwości w obiekcie _compiledInstance służą metody GetValue oraz SetValue:

// Odczyt typu oraz bieżącej wartości pola przy zaznaczeniu elementu na liście Variables:
FieldInfo^ field = targetType->GetField(selectedVarName, flags);
if (field != nullptr)
{
    // 1. Odczyt typu zmiennej (np. System.String, System.Int32)
    VariableTypeText = field->FieldType->FullName;

    // 2. Odczyt bieżącej wartości zmiennej w obiekcie
    Object^ currentVal = field->GetValue(_compiledInstance);
    VariableValueText = (currentVal != nullptr) ? currentVal->ToString() : "";
}

// Zapis nowej wartości po kliknięciu przycisku SET:
if (field != nullptr)
{
    // Konwersja tekstu z TextBoxa na docelowy typ pola (np. z "42" na Int32)
    Object^ newVal = Convert::ChangeType(VariableValueText, field->FieldType);
    field->SetValue(_compiledInstance, newVal);
}

(Analogicznie dla właściwości obiektu klasy PropertyInfo^ wywołujemy prop->PropertyType, prop->GetValue(_compiledInstance) oraz prop->SetValue(_compiledInstance, newVal)).


6. Diagnostyka Najczęstszych Błędów w Projektach C++/CLI + WPF + Roslyn

Cel: Szybkie rozpoznawanie i eliminacja błędów kompilacji i uruchomienia specyficznych dla połączenia /clr:netcore, XAML oraz Roslyn.

  1. System.ArgumentNullException: Value cannot be null. (Parameter 'parserContext') podczas startu:
    • Przyczyna: Wywołano dwuargumentową metodę XamlReader::Load(st, nullptr).
    • Rozwiązanie: Użyj jednoargumentowego przeciążenia: XamlReader::Load(st).
  2. Błąd kompilacji Roslyn BC30002: Type 'System.Object' is not defined lub BC35000: Requested operation is not available because the runtime library function 'Microsoft.VisualBasic...' is not defined:
    • Przyczyna: Do listy MetadataReference przekazano zbyt mało zestawów referencyjnych (np. tylko System.IO).
    • Rozwiązanie: Dodaj do referencji zestawy Object::typeid->Assembly->Location (System.Private.CoreLib), System.Runtime oraz obowiązkowo Microsoft.VisualBasic.Core.
  3. Interfejs WPF nie odświeża pól tekstowych (Result, typ zmiennej, wartość zmiennej) mimo zmiany wartości w C++/CLI:
    • Przyczyna: Zdarzenie PropertyChanged ma puste metody add {} / remove {} lub w setterze właściwości w DataModel zapomniano wywołać OnPropertyChanged("NazwaWlasciwosci").
    • Rozwiązanie: Użyj automatycznego zdarzenia virtual event PropertyChangedEventHandler^ PropertyChanged; i wywołuj OnPropertyChanged w każdym setterze zmieniającym stan prezentowany w widoku.
  4. System.Reflection.TargetParameterCountException: Parameter count mismatch podczas kliknięcia INVOKE:
    • Przyczyna: Wywołano method->Invoke(instance, nullptr) dla metody oczekującej parametru lub odwrotnie — przekazano tablicę z parametrem do metody bezparametrowej.
    • Rozwiązanie: Sprawdź liczbę parametrów metody za pomocą method->GetParameters()->Length przed wywołaniem Invoke.

Zadanie Laboratoryjne do wykonania (100 pkt)

Cel zadania: Napisanie w języku C++/CLI (.NET 8.0 / 9.0+, /clr:netcore) programu z interfejsem graficznym WPF zrealizowanym ściśle według wzorca MVVM, który kompiluje w pamięci RAM kod źródłowy Visual Basic .NET przy użyciu kompilatora Roslyn (Microsoft.CodeAnalysis), a następnie za pomocą mechanizmu Refleksji (System.Reflection) umożliwia przeglądanie i wywoływanie jego metod oraz odczyt i modyfikację jego pól/właściwości.

┌───────────────────────────────────────────────────────────────────────────────┐
│ [] MainWindow                                                       ─   □   × │
├───────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐  ┌──────────────┐ │
│ │ Public Class Class1                                     │  │   Compile    │ │
│ │     Private text1 As String = "Test1"                   │  └──────────────┘ │
│ │     Public text2 As String = "Test2"                    │  Kind of output:  │
│ │     Public licznik As Integer = 10                      │  ┌──────────────┐ │
│ │     Public Function Method1(p As String) As String      │  │ Dynamically▼ │ │
│ │         Return text1 & " " & p                          │  └──────────────┘ │
│ │     End Function                                        │                   │
│ └─────────────────────────────────────────────────────────┘                   │
│                                                                               │
│           ┌───────────────────────────────────────────────┐                   │
│   Errors: │                                               │  Parameter:       │
│           │                                               │  ┌──────────────┐ │
│           └───────────────────────────────────────────────┘  │              │ │
│           ┌───────────────────────────────────────────────┐  └──────────────┘ │
│  Methods: │ Method1                                       │  ┌──────────────┐ │
│           │ Method2                                       │  │    INVOKE    │ │
│           └───────────────────────────────────────────────┘  └──────────────┘ │
│           ┌───────────────────────────────────────────────┐  Result:          │
│ Variables:│ text1                                         │  ┌──────────────┐ │
│           │ text2                                         │  │              │ │
│           │ licznik                                       │  └──────────────┘ │
│           └───────────────────────────────────────────────┘  ┌──────────────┐ │
│                                                              │ System.String│ │
│                                                              └──────────────┘ │
│                                                              ┌──────────────┐ │
│                                                              │ Test1        │ │
│                                                              └──────────────┘ │
│                                                              ┌──────────────┐ │
│                                                              │     SET      │ │
│                                                              └──────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘

Wymagania Techniczne i Ograniczenia Architektoniczne (Obowiązkowe)

  1. Wersja platformy: Projekt musi wykorzystywać nowoczesne środowisko .NET 8.0 lub wyższe (minimum .NET 6.0, tryb /clr:netcore). Użycie klasycznego .NET Framework (/clr) oraz mechanizmu CodeDOM jest zabronione.
  2. Czysty wzorzec MVVM (Zakaz x:Name): W pliku MainWindow.xaml zabronione jest definiowanie atrybutu Name / x:Name oraz bezpośrednie odwoływanie się do kontrolek UI z poziomu kodu C++/CLI (np. przez FindName). Cała komunikacja między widokiem a klasą DataModel musi odbywać się wyłącznie poprzez wiązanie danych ({Binding ...}), właściwości INotifyPropertyChanged, ObservableCollection<T> oraz komendy ICommand.
  3. Kompilator Roslyn: Kompilacja kodu VB.NET wprowadzonego w oknie edytora musi odbywać się dynamicznie w pamięci RAM przy użyciu klas z pakietu Microsoft.CodeAnalysis.VisualBasic.

Część 1: Konfiguracja Projektu C++/CLI, Widok XAML i Architektura MVVM (25 pkt)

  1. (10 pkt) Konfiguracja rozwiązania i wczytanie widoku:
    • Skonfiguruj projekt biblioteki C++/CLI z obsługą /clr:netcore oraz referencją <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF"/> wraz z projektem uruchomieniowym (STA).
    • Wczytaj okno z pliku MainWindow.xaml za pomocą XamlReader::Load i przypisz do jego DataContext instancję klasy modelu widoku (DataModel^), ładując na starcie przykładowy kod klasy VB.NET (zawierającej co najmniej 2 metody i 3 pola/właściwości).
  2. (15 pkt) Implementacja ViewModelu i wiązań danych w XAML:
    • Odtwórz układ graficzny okna przedstawiony na schemacie: pole edycji kodu źródłowego, przycisk Compile, listę rozwijaną ComboBox (Kind of output: z wartościami wyliczenia OutputKind, np. DynamicallyLinkedLibrary, ConsoleApplication), trzy listy ListBox (Errors:, Methods:, Variables:), pole tekstowe Parameter:, przycisk INVOKE, pole wyniku Result:, dwa pola tekstowe dla zaznaczonej zmiennej (typ zmiennej oraz jej bieżąca wartość) i przycisk SET.
    • Powiąż właściwość SelectedIndex (lub SelectedItem) list Methods oraz Variables oraz wszystkie pola tekstowe i przyciski z odpowiednimi właściwościami i komendami ICommand w klasie DataModel.

Część 2: Dynamiczna Kompilacja Kodu VB.NET Kompilatorem Roslyn (30 pkt)

  1. (20 pkt) Budowa drzewa składniowego i kompilacja w pamięci:
    • Po kliknięciu przycisku Compile utwórz drzewo składniowe z aktualnego tekstu w edytorze za pomocą SyntaxFactory::ParseSyntaxTree (SourceText::From).
    • Skonfiguruj obiekt VisualBasicCompilation::Create wraz z kompletem niezbędnych referencji MetadataReference oraz opcją OutputKind pobraną z kontrolki ComboBox (Kind of output).
    • Wyemituj wynik kompilacji do strumienia MemoryStream za pomocą metody Emit.
  2. (10 pkt) Obsługa diagnostyki i błędów kompilacji:
    • W przypadku niepowodzenia kompilacji (!emitResult->Success) wyczyść listy metod i zmiennych, a w liście Errors: wyświetl komunikaty diagnostyczne błędów (emitResult->Diagnostics).
    • W przypadku powodzenia kompilacji wyczyść listę Errors:, załaduj skompilowany zestaw z pamięci (Assembly::Load(ms->ToArray())), utwórz instancję skompilowanej klasy (Activator::CreateInstance) i wypełnij listy Methods: oraz Variables:.

Część 3: Introspekcja i Wywoływanie Metod przez Refleksję (25 pkt)

  1. (10 pkt) Prezentacja metod skompilowanej klasy:
    • Za pomocą metody GetMethods (z odpowiednimi flagami BindingFlags) pobierz metody zadeklarowane w skompilowanej klasie VB.NET i wyświetl ich nazwy w kontrolce ListBox (Methods:).
  2. (15 pkt) Dynamiczne uruchamianie zaznaczonej metody (INVOKE):
    • Po zaznaczeniu metody na liście Methods: i kliknięciu przycisku INVOKE, wywołaj tę metodę na utworzonej instancji obiektu za pomocą MethodInfo::Invoke.
    • Obsłuż zarówno metody bezparametrowe, jak i metody przyjmujące parametr (pobierając wartość z pola tekstowego Parameter: i konwertując ją do wymaganego typu parametru).
    • Wyświetl wartość zwróconą przez metodę w polu Result:.

Część 4: Inspekcja i Modyfikacja Pól / Właściwości przez Refleksję (20 pkt)

  1. (10 pkt) Odczyt typu i wartości po zaznaczeniu zmiennej:
    • Wypełnij listę Variables: nazwami pól (GetFields, w tym prywatnych i publicznych) oraz właściwości (GetProperties) skompilowanej klasy.
    • W momencie zmiany zaznaczonego elementu na liście Variables: (reakcja w setterze właściwości zbindowanej z SelectedIndex lub SelectedItem), automatycznie odczytaj przez Refleksję i wyświetl w dwóch dolnych polach tekstowych:
      1. Typ zaznaczonego pola/właściwości (np. System.String, System.Int32),
      2. Aktualną wartość tego pola/właściwości w żywej instancji obiektu (GetValue).
  2. (10 pkt) Modyfikacja wartości zmiennej w obiekcie (SET):
    • Po wpisaniu nowej wartości w dolnym polu tekstowym i kliknięciu przycisku SET, skonwertuj wprowadzoną wartość do typu docelowego pola/właściwości i zapisz ją w instancji obiektu za pomocą metody SetValue.
    • Zweryfikuj poprawność działania: zmiana wartości pola (np. text1) przyciskiem SET powinna być natychmiast widoczna przy kolejnym wywołaniu metody (INVOKE) korzystającej z tego pola.

Zestawienie Oceny (Suma: 100 pkt)

Nr Zakres modułu Weryfikacja Punkty
1 Projekt C++/CLI (/clr:netcore), WPF i MVVM Działające okno ładowane przez XamlReader::Load, poprawny INotifyPropertyChanged, ICommand, brak x:Name w XAML. 25 pkt
2 Kompilacja w locie kompilatorem Roslyn Poprawne użycie ParseSyntaxTree, VisualBasicCompilation, OutputKind z ComboBox, emisja do MemoryStream i raportowanie błędów w Errors. 30 pkt
3 Refleksja – Odkrywanie i wywoływanie metod (INVOKE) Wypisanie metod klasy w Methods, poprawne wywołanie MethodInfo::Invoke (z parametrem z pola Parameter lub bez) i prezentacja wyniku w Result. 25 pkt
4 Refleksja – Odczyt typu/wartości i zapis zmiennych (SET) Automatyczna prezentacja typu i wartości po kliknięciu zmiennej na liście Variables (GetValue) oraz trwała zmiana wartości w instancji po kliknięciu SET (SetValue). 20 pkt
SUMA 100 pkt