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 klasieref classpowoduje automatyczną implementację interfejsuIDisposable, a wywołanie operatoradelete 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:
- 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). - 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 obiektApplicationorazWindowmusi działać w trybieApartmentState.STA. Próba utworzenia okna WPF w domyślnym wątkuMTArzuci wyjątekInvalidOperationException: 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:
TextBoxvsRichTextBoxw czystym MVVM:
W WPF kontrolka<RichTextBox>przechowuje treść w obiekcieDocumenttypuFlowDocument, 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 atrybutux:Namei 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
RichTextBoxza pomocą własnej właściwości doczepianejAttachedPropertyw 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ówvoid add(PropertyChangedEventHandler^ value) { }orazvoid 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 deklaracjivirtual event PropertyChangedEventHandler^ PropertyChanged;oraz metody pomocniczejOnPropertyChanged(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.exevs 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:
SyntaxFactory::ParseSyntaxTree(sourceText)❌ (Błąd C2660: funkcja nie przyjmuje 1 argumentu)MetadataReference::CreateFromFile(path)❌ (Błąd C2660: funkcja nie przyjmuje 1 argumentu)gcnew VisualBasicCompilationOptions(OutputKind::DynamicallyLinkedLibrary)❌ (Błąd C2664: konstruktor wymaga wszystkich 18 argumentów!)compilation->Emit(memoryStream)❌ (Błąd C2660: metoda wymaga 11 argumentów!)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):
- Pierwszy parametr (
obj) to referencja do instancji obiektu, na rzecz którego metoda ma zostać wykonana (dla metod statycznych można przekazaćnullptr). - Drugi parametr (
parameters) to tablica obiektówcli::array<Object^>^odpowiadająca argumentom wywoływanej metody.- Jeśli wywoływana metoda nie przyjmuje żadnych
parametrów
(
m->GetParameters()->Length == 0), przekazujemynullptr. - Jeśli metoda przyjmuje parametr (np.
StringlubInteger), sprawdzamy typ parametru za pomocąParameterInfo::ParameterType, konwertujemy ciąg znaków wpisany przez użytkownika w poluParameter:za pomocąConvert::ChangeType(paramText, paramType)i pakujemy go w jednoelementową tablicęgcnew cli::array<Object^> { convertedParam }.
- Jeśli wywoływana metoda nie przyjmuje żadnych
parametrów
(
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.
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).
- Przyczyna: Wywołano dwuargumentową metodę
- Błąd kompilacji Roslyn
BC30002: Type 'System.Object' is not definedlubBC35000: Requested operation is not available because the runtime library function 'Microsoft.VisualBasic...' is not defined:- Przyczyna: Do listy
MetadataReferenceprzekazano zbyt mało zestawów referencyjnych (np. tylkoSystem.IO). - Rozwiązanie: Dodaj do referencji zestawy
Object::typeid->Assembly->Location(System.Private.CoreLib),System.Runtimeoraz obowiązkowoMicrosoft.VisualBasic.Core.
- Przyczyna: Do listy
- Interfejs WPF nie odświeża pól tekstowych
(
Result, typ zmiennej, wartość zmiennej) mimo zmiany wartości w C++/CLI:- Przyczyna: Zdarzenie
PropertyChangedma puste metodyadd {}/remove {}lub w setterze właściwości wDataModelzapomniano wywołaćOnPropertyChanged("NazwaWlasciwosci"). - Rozwiązanie: Użyj automatycznego zdarzenia
virtual event PropertyChangedEventHandler^ PropertyChanged;i wywołujOnPropertyChangedw każdym setterze zmieniającym stan prezentowany w widoku.
- Przyczyna: Zdarzenie
System.Reflection.TargetParameterCountException: Parameter count mismatchpodczas kliknięciaINVOKE:- 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()->Lengthprzed wywołaniemInvoke.
- Przyczyna: Wywołano
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)
- Wersja platformy: Projekt musi wykorzystywać
nowoczesne środowisko
.NET 8.0lub wyższe (minimum.NET 6.0, tryb/clr:netcore). Użycie klasycznego.NET Framework(/clr) oraz mechanizmuCodeDOMjest zabronione. - Czysty wzorzec MVVM (Zakaz
x:Name): W plikuMainWindow.xamlzabronione jest definiowanie atrybutuName/x:Nameoraz bezpośrednie odwoływanie się do kontrolek UI z poziomu kodu C++/CLI (np. przezFindName). Cała komunikacja między widokiem a klasąDataModelmusi odbywać się wyłącznie poprzez wiązanie danych ({Binding ...}), właściwościINotifyPropertyChanged,ObservableCollection<T>oraz komendyICommand. - 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)
- (10 pkt) Konfiguracja rozwiązania i wczytanie
widoku:
- Skonfiguruj projekt biblioteki C++/CLI z obsługą
/clr:netcoreoraz referencją<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF"/>wraz z projektem uruchomieniowym (STA). - Wczytaj okno z pliku
MainWindow.xamlza pomocąXamlReader::Loadi przypisz do jegoDataContextinstancję 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).
- Skonfiguruj projekt biblioteki C++/CLI z obsługą
- (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 wyliczeniaOutputKind, np.DynamicallyLinkedLibrary,ConsoleApplication), trzy listyListBox(Errors:,Methods:,Variables:), pole tekstoweParameter:, przyciskINVOKE, pole wynikuResult:, dwa pola tekstowe dla zaznaczonej zmiennej (typ zmiennej oraz jej bieżąca wartość) i przyciskSET. - Powiąż właściwość
SelectedIndex(lubSelectedItem) listMethodsorazVariablesoraz wszystkie pola tekstowe i przyciski z odpowiednimi właściwościami i komendamiICommandw klasieDataModel.
- Odtwórz układ graficzny okna przedstawiony na schemacie: pole edycji
kodu źródłowego, przycisk
Część 2: Dynamiczna Kompilacja Kodu VB.NET Kompilatorem Roslyn (30 pkt)
- (20 pkt) Budowa drzewa składniowego i kompilacja w
pamięci:
- Po kliknięciu przycisku
Compileutwórz drzewo składniowe z aktualnego tekstu w edytorze za pomocąSyntaxFactory::ParseSyntaxTree(SourceText::From). - Skonfiguruj obiekt
VisualBasicCompilation::Createwraz z kompletem niezbędnych referencjiMetadataReferenceoraz opcjąOutputKindpobraną z kontrolkiComboBox(Kind of output). - Wyemituj wynik kompilacji do strumienia
MemoryStreamza pomocą metodyEmit.
- Po kliknięciu przycisku
- (10 pkt) Obsługa diagnostyki i błędów kompilacji:
- W przypadku niepowodzenia kompilacji
(
!emitResult->Success) wyczyść listy metod i zmiennych, a w liścieErrors: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 listyMethods:orazVariables:.
- W przypadku niepowodzenia kompilacji
(
Część 3: Introspekcja i Wywoływanie Metod przez Refleksję (25 pkt)
- (10 pkt) Prezentacja metod skompilowanej klasy:
- Za pomocą metody
GetMethods(z odpowiednimi flagamiBindingFlags) pobierz metody zadeklarowane w skompilowanej klasie VB.NET i wyświetl ich nazwy w kontrolceListBox(Methods:).
- Za pomocą metody
- (15 pkt) Dynamiczne uruchamianie zaznaczonej metody
(
INVOKE):- Po zaznaczeniu metody na liście
Methods:i kliknięciu przyciskuINVOKE, 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:.
- Po zaznaczeniu metody na liście
Część 4: Inspekcja i Modyfikacja Pól / Właściwości przez Refleksję (20 pkt)
- (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 zSelectedIndexlubSelectedItem), automatycznie odczytaj przez Refleksję i wyświetl w dwóch dolnych polach tekstowych:- Typ zaznaczonego pola/właściwości (np.
System.String,System.Int32), - Aktualną wartość tego pola/właściwości w żywej
instancji obiektu (
GetValue).
- Typ zaznaczonego pola/właściwości (np.
- Wypełnij listę
- (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ą metodySetValue. - Zweryfikuj poprawność działania: zmiana wartości pola (np.
text1) przyciskiemSETpowinna być natychmiast widoczna przy kolejnym wywołaniu metody (INVOKE) korzystającej z tego pola.
- Po wpisaniu nowej wartości w dolnym polu tekstowym i kliknięciu
przycisku
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 |