Programowanie Aplikacji Lokalnych / Programowanie .NET

Rok akademicki: 2026/27

Instrukcja laboratoryjna: Architektura Frameworka WPF i Język XAML (.NET 8/9/10), Drzewo Wizualne, Data Binding, Dispatcher i P/Invoke

Prowadzący: Tomasz Goluch
Wersja: 1.0


1. Wprowadzenie: Architektura WPF, Język XAML i Kompilacja Częściowa

Cel: Zrozumienie architektury podsystemu graficznego Windows Presentation Foundation (WPF), deklaratywnego języka znaczników XAML oraz mechanizmu kompilacji widoków do klas częściowych C#.

Windows Presentation Foundation (WPF) to zaawansowany, wektorowy podsystem prezentacji dla aplikacji desktopowych w systemie Windows, wchodzący w skład platformy .NET (.NET 8 LTS / .NET 9 / .NET 10 LTS). W odróżnieniu od klasycznego systemu Windows Forms (opartego na wiekowym interfejsie GDI/GDI+ oraz uchwytach okien HWND dla każdej kontrolki), WPF renderuje cały interfejs w oparciu o potok DirectX z pełną akceleracją sprzętową karty graficznej (GPU). Oferuje to płynne skalowanie na ekranach o wysokim DPI (4K+), zaawansowane zagnieżdżanie dowolnych kontrolek, bogaty system stylów i szablonów oraz natywną obsługę animacji.

Interfejs użytkownika w WPF definiujemy za pomocą deklaratywnego języka znaczników XAML (Extensible Application Markup Language), co pozwala na czystą separację warstwy wizualnej (widoku) od logiki aplikacyjnej i biznesowej.

Kluczowe zasady języka XAML:

Proces kompilacji XAML i klasy częściowe (*.g.cs oraz *.baml)

Logika okna implementowana jest w plikach code-behind (z rozszerzeniem .xaml.cs), które łączą się z plikiem .xaml za pomocą mechanizmu klas częściowych (partial class).

               [ MainWindow.xaml ]                    [ MainWindow.xaml.cs ]
                        │                                       │
          Kompilator znaczników (Markup Compiler)               │
         ┌──────────────┴──────────────┐                        │
         ▼                             ▼                        ▼
[ MainWindow.baml ]           [ obj/.../MainWindow.g.cs ] ◄─────┘
 (Skompilowany binarny         (Wygenerowana partial class:
  zasób XAML w .dll)            pola kontrolek x:Name + InitializeComponent())
         │                             │
         └──────────────┬──────────────┘
                        ▼
           [ Kompilator Roslyn (C#) ] ──► Zestaw wynikowy (.dll / .exe)

Podczas budowania projektu kompilator XAML wykonuje dwa kroki:

  1. Tłumaczy strukturę XAML do skompilowanego formatu binarnego BAML (Binary Application Markup Language), osadzanego w zasobach pliku wynikowego .dll.
  2. Generuje w katalogu pośrednim obj\<Konfiguracja>\net8.0-windows\ (lub net9.0-windows / net10.0-windows) plik MainWindow.g.cs. Zawiera on drugą część klasy partial class MainWindow z polami dla wszystkich kontrolek oznaczonych atrybutem x:Name oraz definicję metody InitializeComponent(), która w czasie uruchomienia ładuje strumień BAML i buduje drzewo obiektów.

Anatomia podstawowych plików projektu WPF

  1. App.xaml (oraz App.xaml.cs):

    • Głównym znacznikiem jest <Application>, definiujący punkt startowy, cykl życia procesu oraz słownik zasobów globalnych (<Application.Resources>).
    • Atrybut StartupUri="MainWindow.xaml" wskazuje okno otwierane automatycznie po uruchomieniu programu (w zaawansowanych aplikacjach z kontenerem Dependency Injection często zastępujemy go nadpisaniem metody OnStartup w App.xaml.cs).
    • Nowość w .NET 9 / .NET 10: Atrybut ThemeMode="System" (lub "Light" / "Dark") w znaczniku <Application> aktywuje natywny styl Windows 11 Fluent Design.
  2. MainWindow.xaml (oraz MainWindow.xaml.cs):

    • Głównym znacznikiem jest <Window>, reprezentujący okno systemowe.
    • Mapowania przestrzeni nazw XML (xmlns):
      • xmlns="[http://schemas.microsoft.com/winfx/2006/xaml/presentation](http://schemas.microsoft.com/winfx/2006/xaml/presentation)" – domyślna przestrzeń nazw zawierająca wszystkie standardowe kontrolki WPF (dzięki atrybutowi [XmlnsDefinition] na poziomie zestawu łączy wiele przestrzeni CLR pod jednym adresem URI).
      • xmlns:x="[http://schemas.microsoft.com/winfx/2006/xaml](http://schemas.microsoft.com/winfx/2006/xaml)" – przestrzeń konstrukcji języka XAML (np. x:Class, x:Name, x:Key, x:Type, x:Static).
      • xmlns:d="[http://schemas.microsoft.com/expression/blend/2008](http://schemas.microsoft.com/expression/blend/2008)" oraz mc:Ignorable="d" – przestrzeń czasu projektowania (Design-Time). Atrybuty takie jak d:Width="800", d:Height="450" lub d:DataContext działają wyłącznie w podglądzie projektanta Visual Studio i są całkowicie ignorowane podczas kompilacji programu.
      • Mapowanie własnych przestrzeni nazw C# (clr-namespace):
        Aby użyć w XAML własnych klas z projektu (np. konwerterów, kontrolek użytkownika lub ViewModeli), deklarujemy prefiks:
        xmlns:local="clr-namespace:MojaAplikacja.ViewModels;assembly=MojaAplikacja"
        (Jeżeli docelowa przestrzeń nazw znajduje się w tym samym projekcie co plik XAML, człon ;assembly=... można pominąć).

⚠️ Ograniczenie parsera XAML przy klasach zagnieżdżonych:
Analizator XAML nie obsługuje bezpośredniego tworzenia instancji klas zagnieżdżonych wewnątrz innych klas C# ani automatycznego przechodzenia do podprzestrzeni nazw po kropce (ponieważ kropka w składni <local:Klasa.Wlasciwosc> jest zarezerwowana dla właściwości obiektu). Każda podprzestrzeń nazw wymaga osobnej deklaracji xmlns.


2. Kontenery Układu (Layout Panels) i Pozycjonowanie Elementów

Cel: Opanowanie responsywnego rozmieszczania kontrolek przy użyciu wbudowanych paneli układu oraz systemu pomiaru w pikselach niezależnych od urządzenia (DIP).

W przeciwieństwie do sztywnego pozycjonowania pikselowego, WPF wykorzystuje dwuetapowy silnik układu (Measure Pass – pomiar pożądanego rozmiaru dzieci oraz Arrange Pass – ostateczne rozmieszczenie na ekranie). Wszystkie wymiary w WPF podawane są w pikselach niezależnych od urządzenia (DIP – Device-Independent Pixels), gdzie 1 jednostka odpowiada zawsze \(\frac{1}{96}\) cala fizycznego, co gwarantuje automatyczne skalowanie na monitorach o różnym zagęszczeniu pikseli.

Podstawowe panele układu w WPF

Panel układu Charakterystyka i zastosowanie Kluczowe właściwości dołączone / sterujące
Grid Domyślny i najbardziej wszechstronny kontener. Dzieli przestrzeń na siatkę wierszy i kolumn (odpowiednik CSS Grid). Grid.Row, Grid.Column, Grid.RowSpan, Grid.ColumnSpan
StackPanel Układa elementy jeden za drugim w pojedynczej linii (pionowo lub poziomo). Nie zawija elementów po wyczerpaniu miejsca. Orientation="Vertical" (domyślnie) lub "Horizontal"
DockPanel Przyciąga (dokuje) kolejne elementy do krawędzi okna (Top, Bottom, Left, Right). Idealny jako główny szkielet aplikacji (pasek menu, pasek stanu, obszar roboczy). DockPanel.Dock="Top", LastChildFill="True" (domyślnie ostatnie dziecko wypełnia środek)
WrapPanel Układa elementy sekwencyjnie w wierszu lub kolumnie, ale automatycznie zawija je do nowego wiersza/kolumny, gdy brakuje miejsca. Orientation, ItemWidth, ItemHeight
Canvas Płótno o bezwzględnych współrzędnych \((X, Y)\). Nie skaluje zawartości automatycznie – stosowany głównie do wykresów, rysunków CAD i gier 2D. Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom, Panel.ZIndex

Definiowanie wymiarów w panelu Grid (Auto, *, px)

W siatce Grid szerokość kolumn i wysokość wierszy można definiować na trzy sposoby:

<Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/> <!-- Wiersz 0: dopasowany do nagłówka -->
        <RowDefinition Height="*"/>    <!-- Wiersz 1: zajmuje całą pozostałą wysokość -->
        <RowDefinition Height="40"/>   <!-- Wiersz 2: sztywna stopka 40 DIP -->
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*"/> <!-- Kolumna 0: 1/3 szerokości -->
        <ColumnDefinition Width="2*"/> <!-- Kolumna 1: 2/3 szerokości -->
    </Grid.ColumnDefinitions>

    <!-- Nagłówek rozciągnięty na 2 kolumny (ColumnSpan="2") -->
    <TextBlock FontSize="18" FontWeight="Bold" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Margin="0,0,0,10" Text="Panel Sterowania"/>

    <ListBox Grid.Column="0" Grid.Row="1" Margin="0,0,5,0"/>
    <ContentControl Grid.Column="1" Grid.Row="1" Margin="5,0,0,0"/>

    <StatusBar Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"/>
</Grid>

💡 Kolejność nakładania elementów (Panel.ZIndex):
Gdy kilka kontrolek zajmuje tę samą komórkę siatki Grid lub nakłada się na siebie w Canvas, o tym, która z nich jest widoczna na wierzchu, decyduje kolejność deklaracji w pliku XAML (późniejsze elementy przykrywają wcześniejsze). Kolejność tę można jawnie nadpisać właściwością dołączoną Panel.ZIndex="10" – element o wyższej wartości ZIndex jest zawsze rysowany na pierwszym planie.


3. Zasoby Logiczne (Resources): StaticResource vs DynamicResource

Cel: Organizacja reużywalnych obiektów (pędzli, kolorów, stylów, konwerterów) w słownikach zasobów oraz zrozumienie różnicy między wiązaniem statycznym a dynamicznym.

W WPF oprócz zasobów binarnych osadzanych w plikach zestawu (obrazy .png, ikony, czcionki) istnieją Zasoby Logiczne (ResourceDictionary). Pozwalają one zdefiniować w XAML dowolny obiekt .NET pod unikalnym kluczem x:Key i wielokrotnie używać go w różnych częściach interfejsu.

Każdy element dziedziczący po FrameworkElement (np. Window, UserControl, Grid, Button) oraz klasa Application posiadają własną właściwość Resources. Wyszukiwanie zasobu o podanym kluczu odbywa się hierarchicznie w górę drzewa logicznego: Kontrolka → Panel nadrzędny (np. Grid) → Okno (Window.Resources) → Aplikacja (Application.Resources) → Motyw systemowy.

Porównanie rozszerzeń znaczników StaticResource i DynamicResource

Cecha {StaticResource Klucz} {DynamicResource Klucz}
Moment rozwiązania Jednorazowo podczas ładowania i parsowania strumienia XAML (przed wyświetleniem okna). W locie (w czasie wykonania), za każdym razem gdy obiekt pod danym kluczem w słowniku zostanie podmieniony.
Reakcja na podmianę obiektu w słowniku Brak – przypisanie nowego obiektu Resources["Klucz"] = nowyPedzel; zostanie zignorowane. Automatyczna aktualizacja UI (np. przy zmianie motywu Jasny/Ciemny lub zmianie kolorów w systemie Windows).
Wymagania kolejności w XAML Zasób musi być zadeklarowany w XAML wcześniej (wyżej) niż miejsce jego użycia. Zasób może zostać załadowany lub dodany do słownika później (np. w kodzie C#).
Wydajność Wysoka (brak narzutu na śledzenie zmian w słowniku). Nieco niższa (utrzymuje wyrażenie nasłuchujące zmian w ResourceDictionary).

Przykład praktyczny: Reakcja na zmianę koloru systemowego w czasie rzeczywistym

<Window.Resources>
    <!-- Własny zasób logiczny pędzla -->
    <SolidColorBrush Color="#05E0E9" x:Key="AccentBrush"/>
</Window.Resources>

<StackPanel Margin="20">
    <!-- 1. StaticResource: Pobierze kolor pulpitu TYLKO raz przy starcie okna -->
    <Button Margin="0,0,0,10" Padding="10">
        <Button.Background>
            <SolidColorBrush Color="{StaticResource {x:Static SystemColors.DesktopColorKey}}"/>
        </Button.Background>
        Przycisk StaticResource (stały po uruchomieniu)
    </Button>

    <!-- 2. DynamicResource: Odświeży kolor natychmiast po zmianie tła w Ustawieniach Windows! -->
    <Button Padding="10">
        <Button.Background>
            <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.DesktopColorKey}}"/>
        </Button.Background>
        Przycisk DynamicResource (reaguje na żywo)
    </Button>
</StackPanel>

(Aby zaobserwować różnicę podczas działania programu, otwórz w systemie Windows: Ustawienia → Personalizacja → Tło, ustaw Jednolity kolor i zmień barwę – przycisk z DynamicResource natychmiast zmieni swój wygląd).


4. Kaskadowe Style (Style) i Dziedziczenie Wyglądu (BasedOn)

Cel: Ujednolicenie wyglądu kontrolek w aplikacji przy użyciu stylów niejawnych, jawnych, dziedziczonych oraz programowych.

Obiekt Style pozwala zgrupować zestaw wartości właściwości (<Setter>) oraz wyzwalaczy (<Trigger>) i zaaplikować je do wielu kontrolek jednocześnie – analogicznie do arkuszy CSS w technologiach webowych.

1. Style niejawne (Implicit Styles – bez atrybutu x:Key)

Gdy w definicji stylu podamy wyłącznie atrybut TargetType="TextBox" (pomijając x:Key), styl zostanie automatycznie nałożony na wszystkie kontrolki typu TextBox znajdujące się w zasięgu danego słownika zasobów:

<Window.Resources>
    <Style TargetType="TextBox">
        <Setter Property="HorizontalAlignment" Value="Center"/>
        <Setter Property="FontFamily" Value="Segoe UI"/>
        <Setter Property="FontSize" Value="14"/>
        <Setter Property="Padding" Value="6,4"/>
    </Style>
</Window.Resources>

2. Style jawne (Explicit Styles – z atrybutem x:Key)

Dodanie atrybutu x:Key="TitleText" sprawia, że styl jest stosowany wyłącznie do kontrolek, które jawnie się do niego odwołają:

<Style TargetType="TextBox" x:Key="TitleText">
    <Setter Property="FontSize" Value="18"/>
    <Setter Property="FontWeight" Value="Bold"/>
</Style>

<!-- Użycie w kodzie XAML: -->
<TextBox Style="{StaticResource TitleText}" Text="Nagłówek formularza"/>

3. Dziedziczenie stylów (BasedOn)

Style mogą rozszerzać inne style (zarówno jawne, jak i domyślny styl bazowy kontrolki) za pomocą atrybutu BasedOn:

<!-- Rozszerzenie domyślnego (niejawnego) stylu dla typu TextBox: -->
<Style BasedOn="{StaticResource {x:Type TextBox}}" TargetType="TextBox" x:Key="HighlightedTextBox">
    <Setter Property="BorderBrush" Value="DodgerBlue"/>
    <Setter Property="BorderThickness" Value="2"/>
</Style>

4. Programowe przypisanie stylu w C#

Aby przypisać styl z poziomu pliku code-behind, kontrolka musi posiadać nazwę x:Name:

InputNumberTextBox.Style = (Style)FindResource("TitleText");

5. Szablony Kontrolek (ControlTemplate), TemplateBinding i Wyzwalacze (Triggers)

Cel: Całkowita przebudowa struktury wizualnej istniejących kontrolek (koncepcja Lookless Controls) z zachowaniem ich wbudowanej funkcjonalności i logiki.

Kontrolki w WPF są tzw. kontrolkami bezpostaciowymi (Lookless Controls) – klasa Button definiuje wyłącznie logikę zachowania (obsługę kliknięcia, fokusu klawiatury, stanów IsPressed / IsMouseOver), natomiast jej fizyczny wygląd pochodzi w 100% z wymiennego szablonu ControlTemplate.

Budowa własnego szablonu ControlTemplate

Definiując nowy szablon dla przycisku, musimy pamiętać o trzech kluczowych elementach:

  1. ContentPresenter: Specjalny znacznik wewnątrz szablonu określający miejsce, w którym ma zostać wyrenderowana zawartość (Content) przekazana przez użytkownika przycisku (np. tekst "STOP" lub ikona).
  2. {TemplateBinding Wlasciwosc}: Lekkie wiązanie pobierające wartość właściwości bezpośrednio z szablonowanej kontrolki (np. Background, Foreground, Padding), dzięki czemu nasz szablon pozostaje elastyczny.
  3. <ControlTemplate.Triggers>: Ponieważ nadpisanie szablonu usuwa domyślną animację najechania kursorem i wciśnięcia, interakcje wizualne definiujemy za pomocą wyzwalaczy (Trigger) reagujących na właściwości takie jak IsMouseOver czy IsPressed.
<Window.Resources>
    <ControlTemplate TargetType="Button" x:Key="RoundedButtonTemplate">
        <Grid>
            <!-- Element tła narysowany wektorowo za pomocą Path i EllipseGeometry -->
            <Path Fill="{TemplateBinding Background}" Stretch="Uniform" Stroke="{TemplateBinding Foreground}" StrokeThickness="2" UseLayoutRounding="False" x:Name="backgroundElement">
                <Path.Data>
                    <EllipseGeometry RadiusX="1" RadiusY="1"/>
                </Path.Data>
            </Path>

            <!-- Miejsce wstrzyknięcia zawartości przycisku (Content) -->
            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>

        <!-- Definicja zachowania wizualnego przy interakcji z myszą -->
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Fill" TargetName="backgroundElement" Value="AliceBlue"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Fill" TargetName="backgroundElement" Value="LightSteelBlue"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</Window.Resources>

<!-- Zastosowanie szablonu na przycisku: -->
<Button Background="Crimson" FontWeight="Bold" Foreground="White" Height="100" Template="{StaticResource RoundedButtonTemplate}" Width="100">
    STOP
</Button>

6. Drzewo Logiczne vs Drzewo Wizualne oraz Zdarzenia Trasowane (Routed Events)

Cel: Zrozumienie dwupoziomowej struktury drzewiastej interfejsu WPF oraz mechanizmu propagacji zdarzeń (bąbelkowania i tunelowania).

1. Drzewo Logiczne (Logical Tree) a Drzewo Wizualne (Visual Tree)

W każdej aplikacji WPF współistnieją dwa drzewa obiektów:

DRZEWO LOGICZNE (XAML)                 DRZEWO WIZUALNE (Po rozwinięciu ControlTemplate)
      [ Window ]                                        [ Window ]
          │                                                 │
       [ Grid ]                                         [ Border ]
          │                                                 │
      [ Button ] ──────(Szablon przycisku)──────►        [ Grid ]
                                                            │
                                                     [ ButtonChrome ]
                                                            │
                                                   [ ContentPresenter ]
                                                            │
                                                      [ TextBlock ]

🔍 Diagnostyka w czasie rzeczywistym (Live Visual Tree):
Podczas debugowania aplikacji w Visual Studio nad oknem programu pojawia się czarny pasek narzędziowy. Kliknięcie ikony „Przejdź do dynamicznego drzewa wizualnego” (Go to Live Visual Tree) pozwala na żywo badać rozwinięte drzewo wizualne oraz sprawdzać w oknie Live Property Explorer, skąd pochodzi aktualna wartość każdej właściwości (ze stylu, wiązania czy wartości lokalnej).

2. Zdarzenia Trasowane (Routed Events)

Wyobraźmy sobie przycisk <Button>, wewnątrz którego umieszczono złożony układ: <StackPanel> z obrazkiem <Image> i tekstem <TextBlock>. Gdy użytkownik kliknie myszą dokładnie na piksele obrazka, w klasycznych systemach okienkowych zdarzenie trafiłoby tylko do kontrolki Image, która nie wie, że leży wewnątrz przycisku.

WPF rozwiązuje ten problem za pomocą Zdarzeń Trasowanych (Routed Events), które wędrują wzdłuż drzewa wizualnego według jednej z trzech strategii:

  1. Zdarzenia bąbelkujące (Bubbling Events): Zostają wywołane na elemencie źródłowym (np. Image), a następnie wędrują w górę drzewa przez kolejnych rodziców (StackPanel → Button → Grid → Window). Przykłady: MouseDown, KeyDown, Button.Click, Drop.
  2. Zdarzenia tunelujące (Tunneling Events – z prefiksem Preview*): Każde zdarzenie bąbelkujące posiada swój odpowiednik tunelujący, który wyrusza z korzenia okna (Window) w dół drzewa aż do elementu źródłowego, zanim jeszcze wykona się właściwe zdarzenie bąbelkujące. Przykłady: PreviewMouseDown, PreviewKeyDown (idealne do przechwytywania i blokowania klawiszy przed wpisaniem ich do TextBox).
  3. Zdarzenia bezpośrednie (Direct Events): Wykonywane wyłącznie na samym elemencie źródłowym (jak klasyczne zdarzenia CLR). Przykład: MouseEnter, MouseLeave.
// Obsługa zdarzenia Drag & Drop (wymaga ustawienia AllowDrop="True" w XAML):
private void Button_Drop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop))
    {
        string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
        // Przetworzenie upuszczonych plików...
    }

    // Zatrzymanie dalszej propagacji zdarzenia dla pozostałych rodziców w drzewie:
    e.Handled = true;
}

Rejestracja procedury obsługi w kodzie C#:

// Wariant 1: Standardowy operator +=
b1.Drop += Button_Drop;

// Wariant 2: Metoda AddHandler (pozwala przechwycić zdarzenie nawet wtedy,
// gdy wcześniejszy element w drzewie ustawił już flagę e.Handled = true!):
b1.AddHandler(UIElement.DropEvent, new DragEventHandler(Button_Drop), handledEventsToo: true);

7. System Właściwości: DependencyProperty, DependencyObject i AttachedProperty

Cel: Zrozumienie architektury właściwości zależności oraz implementacja własnych DependencyProperty i AttachedProperty.

Zwykła właściwość języka C# (public string Text { get; set; }) przechowuje wartość w prywatnym polu obiektu w pamięci RAM. Gdyby każda kontrolka WPF (posiadająca ponad 100 właściwości: czcionki, marginesy, pędzle, efekty) alokowała w każdej instancji pamięć na 100 pól – z których 95% ma wartość domyślną – aplikacja zużywałaby setki megabajtów RAM-u.

Dlatego kontrolki WPF dziedziczą po klasie DependencyObject, a ich właściwości budowane są jako Właściwości Zależności (DependencyProperty):

1. Implementacja własnej właściwości zależności (DependencyProperty)

Tworząc własną właściwość zależności, musimy ściśle przestrzegać konwencji nazewniczej: statyczne pole readonly typu DependencyProperty musi nazywać się <NazwaWlasciwosci>Property (z sufiksem Property), a właściwość C# pełni jedynie rolę cienkiej powłoki (wrappera) wywołującej GetValue i SetValue:

using System.Windows;

namespace MyApp.Controls;

public class ExampleDependencyObject : DependencyObject
{
    // 1. Rejestracja statyczna (konwencja nazwy pola: Nazwa + "Property"):
    public static readonly DependencyProperty ReadyForBindingProperty =
        DependencyProperty.Register(
            name: nameof(ReadyForBinding),
            propertyType: typeof(string),
            ownerType: typeof(ExampleDependencyObject),
            typeMetadata: new PropertyMetadata(defaultValue: "Stan początkowy"));

    // 2. Wrapper CLR (UWAGA: Wewnątrz get/set NIE WOLNO umieszczać żadnej dodatkowej logiki,
    //    ponieważ silnik XAML wywołuje bezpośrednio metody GetValue/SetValue!):
    public string ReadyForBinding
    {
        get => (string)GetValue(ReadyForBindingProperty);
        set => SetValue(ReadyForBindingProperty, value);
    }
}

(W Visual Studio wpisanie skrótu propdp i dwukrotne naciśnięcie klawisza Tab automatycznie wygeneruje powyższy szablon).

2. Implementacja właściwości dołączonej (AttachedProperty)

Właściwość dołączona (Attached Property) to specjalny rodzaj DependencyProperty, który jest definiowany w jednej klasie (np. DockPanel lub Grid), ale przypisywany dowolnym innym kontrolkom (np. <CheckBox DockPanel.Dock="Top" />).

Rejestrujemy ją za pomocą metody DependencyProperty.RegisterAttached oraz pary publicznych metod statycznych Get<Nazwa> i Set<Nazwa>:

using System.Windows;

namespace MyApp.Helpers;

public static class SecurityBadgeHelper
{
    public static readonly DependencyProperty AccessLevelProperty =
        DependencyProperty.RegisterAttached(
            name: "AccessLevel",
            propertyType: typeof(int),
            ownerType: typeof(SecurityBadgeHelper),
            defaultMetadata: new PropertyMetadata(0));

    // Wymagana statyczna metoda odczytu dla analizatora XAML:
    public static int GetAccessLevel(DependencyObject obj) =>
        (int)obj.GetValue(AccessLevelProperty);

    // Wymagana statyczna metoda zapisu dla analizatora XAML:
    public static void SetAccessLevel(DependencyObject obj, int value) =>
        obj.SetValue(AccessLevelProperty, value);
}

Użycie w XAML na dowolnej kontrolce:

<Button Content="Panel Admina" local:SecurityBadgeHelper.AccessLevel="5"/>

(Skrót snippeta w Visual Studio: propa + Tab Tab).


8. Mechanizm Wiązania Danych (Data Binding), INotifyPropertyChanged i Kolekcje

Cel: Opanowanie deklaratywnego łączenia kontrolek interfejsu z obiektami danych za pomocą DataContext, trybów przepływu Binding.Mode, interfejsu INotifyPropertyChanged oraz kolekcji ObservableCollection<T>.

Wiązanie danych (Data Binding) tworzy automatyczny most komunikacyjny między Celem wiązania (Binding Target) – którym musi być DependencyProperty kontrolki UI – a Źródłem wiązania (Binding Source), którym może być dowolny obiekt C# (najczęściej ViewModel przypisany do właściwości DataContext) lub inna kontrolka XAML (ElementName).

  [ Cel (Binding Target) ]                                  [ Źródło (Binding Source) ]
┌──────────────────────────┐          Obiekt Binding      ┌─────────────────────────────┐
│ Kontrolka UI (np. TextBox│ ◄────────── OneWay ───────── │ Obiekt C# w DataContext     │
│ DependencyProperty: Text)│ ◄────────── TwoWay ────────► │ Właściwość +                │
│                          │ ────── OneWayToSource ─────► │ INotifyPropertyChanged /    │
└──────────────────────────┘             OneTime          │ ObservableCollection<T>     │
                                                          └─────────────────────────────┘

1. Cztery tryby kierunku przepływu danych (BindingMode)

🧠 Kiedy TextBox aktualizuje właściwość w C#? (UpdateSourceTrigger)
Domyślnie kontrolka TextBox w trybie TwoWay ma ustawiony wyzwalacz UpdateSourceTrigger="LostFocus" – zmiana trafia do obiektu C# dopiero wtedy, gdy użytkownik kliknie poza pole tekstowe lub naciśnie Tab. Aby właściwość aktualizowała się na żywo po każdym wciśniętym klawiszu, ustawiamy:
<TextBox Text="{Binding InputNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />.

2. Powiadamianie widoku o zmianach: Interfejs INotifyPropertyChanged

Aby zmiana wartości właściwości w kodzie C# w trybach OneWay oraz TwoWay natychmiast odświeżyła kontrolkę na ekranie, klasa źródłowa (np. ViewModel lub okno) musi implementować interfejs INotifyPropertyChanged:

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MyApp.ViewModels;

public class Person : INotifyPropertyChanged
{
    private string _name = string.Empty;
    private int _age;

    public string Name
    {
        get => _name;
        set { if (_name != value) { _name = value; OnPropertyChanged(); } }
    }

    public int Age
    {
        get => _age;
        set { if (_age != value) { _age = value; OnPropertyChanged(); } }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    // Atrybut [CallerMemberName] automatycznie podstawia nazwę wywołującej właściwości!
    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

3. Wiązanie między kontrolkami (ElementName oraz RelativeSource)

Źródłem wiązania może być również inny element XAML (wskazany przez ElementName) lub sam bieżący obiekt (RelativeSource Self):

<Window DataContext="{Binding RelativeSource={RelativeSource Self}}" x:Class="MyApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel Margin="20">
        <TextBox Text="{Binding InputNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="InputNumberTextBox"/>

        <Slider Maximum="100" Minimum="0" Value="{Binding Text, ElementName=InputNumberTextBox, Mode=TwoWay}"/>
    </StackPanel>
</Window>

4. Wiązanie kolekcji (ObservableCollection<T>), DataTemplate oraz ICollectionView

Zwykła lista List<T> nie informuje interfejsu użytkownika o dodaniu lub usunięciu elementu. W WPF do bindowania list (ListBox, ComboBox, ListView, DataGrid) zawsze wykorzystujemy kolekcję ObservableCollection<T>, która implementuje interfejs INotifyCollectionChanged:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;

namespace MyApp.ViewModels;

public class MainViewModel
{
    public ObservableCollection<Person> People { get; } = new()
    {
        new Person { Name = "Jan", Age = 25 },
        new Person { Name = "Anna", Age = 30 },
        new Person { Name = "Tomasz", Age = 40 }
    };

    public void ApplySortAndFilter()
    {
        // Widok kolekcji pozwala sortować i filtrować dane w UI
        // bez modyfikowania oryginalnej kolekcji ObservableCollection!
        ICollectionView view = CollectionViewSource.GetDefaultView(People);
        view.SortDescriptions.Clear();
        view.SortDescriptions.Add(new SortDescription(nameof(Person.Age), ListSortDirection.Ascending));
        view.Filter = obj => obj is Person p && p.Age >= 25;
    }
}

Wizualizację pojedynczego obiektu na liście definiujemy za pomocą szablonu danych DataTemplate:

<ListBox ItemsSource="{Binding People}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Margin="2" Orientation="Horizontal">
                <TextBlock FontWeight="Bold" Text="{Binding Name}"/>
                <TextBlock Foreground="Gray" Text=" – wiek: "/>
                <TextBlock Text="{Binding Age}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

9. Konwertery Wartości (IValueConverter oraz IMultiValueConverter)

Cel: Transformacja danych w locie pomiędzy typem źródłowym w modelu a typem oczekiwanym przez właściwość kontrolki UI.

Silnik XAML posiada wbudowane konwertery typów (TypeConverter), które potrafią automatycznie zamienić np. tekst "Yellow" na pędzel Brushes.Yellow. Gdy jednak potrzebujemy niestandardowej logiki przeliczania wartości podczas wiązania (np. skali logarytmicznej dla suwaka, zamiany wartości bool na widoczność Visibility.Visible/Collapsed, czy kolorowania wiersza w zależności od progu liczbowego), tworzymy klasę implementującą interfejs IValueConverter:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace MyApp.Converters;

public class LogScaleConverter : IValueConverter
{
    // Konwersja od Źródła (TextBox.Text) do Celu (Slider.Value):
    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is null) return DependencyProperty.UnsetValue;

        if (double.TryParse(value.ToString(), NumberStyles.Any, culture, out double numericValue)
            && numericValue > 0)
        {
            return Math.Log(numericValue);
        }

        // Binding.DoNothing instruuje WPF, aby nie aktualizował kontrolki docelowej przy błędnym wejściu
        return Binding.DoNothing;
    }

    // Konwersja powrotna od Celu (Slider.Value) do Źródła (TextBox.Text) w trybie TwoWay:
    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is double sliderValue)
        {
            return ((int)Math.Round(Math.Exp(sliderValue))).ToString(culture);
        }
        return Binding.DoNothing;
    }
}

Rejestracja konwertera w zasobach okna i użycie w wiązaniu:

<Window.Resources>
    <local:LogScaleConverter x:Key="LogScaleConverterInstance" />
</Window.Resources>

<Slider Maximum="10" Minimum="0" Value="{Binding Text, ElementName=InputNumberTextBox, Mode=TwoWay,
                        Converter={StaticResource LogScaleConverterInstance}}"/>

(Gdy wartość docelowa musi zostać wyliczona jednocześnie z kilku różnych właściwości źródłowych, stosujemy znacznik <MultiBinding> wraz z interfejsem IMultiValueConverter).


10. Wielowątkowość i Model STA: Dispatcher

Cel: Bezpieczna aktualizacja interfejsu użytkownika z wątków roboczych tła przy zachowaniu jednowątkowego modelu apartamentowego (Single-Threaded Apartment).

Wszystkie kontrolki WPF dziedziczą na samym dole swojej hierarchii po klasie DispatcherObject (Object → DispatcherObject → DependencyObject → Visual → UIElement → FrameworkElement → Control). Narzuca to architekturę Thread Affinity (Powiązania Wątkowego) w modelu STA:

Kolejkowanie zadań do wątku UI przez Dispatcher

Aby zaktualizować interfejs z wątku tła, zlecamy wykonanie delegata do kolejki komunikatów obiektu Dispatcher:

Uniwersalna klasa rozszerzająca DispatcherExtensions

using System;
using System.Windows.Threading;

namespace MyApp.Helpers;

public static class DispatcherExtensions
{
    // Bezpieczne wywołanie akcji bezparametrowej
    public static void InvokeIfRequired(this DispatcherObject obj, Action action)
    {
        if (!obj.Dispatcher.CheckAccess())
        {
            obj.Dispatcher.Invoke(action);
        }
        else
        {
            action();
        }
    }

    // Bezpieczne wywołanie akcji z parametrem T
    public static void InvokeIfRequired<T>(this DispatcherObject obj, Action<T> action, T parameter)
    {
        if (!obj.Dispatcher.CheckAccess())
        {
            obj.Dispatcher.Invoke(action, parameter);
        }
        else
        {
            action(parameter);
        }
    }
}

Użycie wewnątrz metody okna uruchamianej w tle:

Task.Run(() =>
{
    // Ciężkie obliczenia w wątku puli wątków...
    this.InvokeIfRequired(val => progressBar.Value = val, 50.0);
});

11. Tworzenie Własnych Kontrolek: UserControl vs Custom Control

Cel: Wybór właściwej strategii tworzenia reużywalnych komponentów wizualnych w WPF.

WPF oferuje trzy poziomy tworzenia własnych kontrolek, różniące się elastycznością i nakładem pracy:

Klasa bazowa Charakterystyka Kiedy stosować?
UserControl Kompozycja istniejących kontrolek WPF (plik .xaml + .xaml.cs). Dziedziczy po ContentControl. Gdy chcemy wydzielić powtarzalny fragment widoku (np. kartę pacjenta, pasek wyszukiwania) do wielokrotnego użycia lub osobnej biblioteki DLL. Nie wspiera wymiany wyglądu przez ControlTemplate.
Control (Custom Control) Kontrolka bezpostaciowa (Lookless – sam plik .cs + domyślny szablon w Themes/Generic.xaml). Gdy budujemy uniwersalną kontrolkę biblioteczną, której strukturę wizualną inni programiści będą mogli całkowicie podmienić własnym ControlTemplate i motywami.
FrameworkElement Niskopoziomowe renderowanie bezpośrednie poprzez nadpisanie metody OnRender(DrawingContext). Gdy potrzebujemy maksymalnej wydajności rysowania tysięcy prymitywów geometrycznych (wykresy czasu rzeczywistego, oscyloskopy).

12. Konsola Diagnostyczna w Procesie WPF, Mechanizm P/Invoke i Wzorzec IDisposable

Cel: Dynamiczne dołączanie systemowego okna konsoli Win32 do aplikacji okienkowej WPF za pomocą wywołań natywnych P/Invoke (kernel32.dll, user32.dll) oraz bezpieczne zwalnianie zasobów niezarządzanych.

Projekt WPF kompilowany jest jako aplikacja podsystemu okienkowego (<OutputType>WinExe</OutputType>), przez co domyślnie nie posiada przypisanego okna konsoli, a wywołania Console.WriteLine trafiają w próżnię. Aby w celach diagnostycznych otworzyć i kontrolować prawdziwe okno konsoli z poziomu działającej aplikacji WPF, wykorzystujemy mechanizm P/Invoke (Platform Invocation Services) pozwalający wywoływać funkcje C/C++ bezpośrednio z bibliotek systemowych Windows.

Wykorzystywane funkcje Win32 API:

  1. Biblioteka kernel32.dll (Zarządzanie konsolą procesu):
    • AllocConsole() – tworzy i przydziela nowe okno konsoli dla bieżącego procesu.
    • FreeConsole() – odłącza proces od konsoli i zamyka jej okno.
    • GetConsoleWindow() – zwraca niezarządzany uchwyt okna (IntPtr / HWND) przypisanej konsoli.
    • SetConsoleTitle(string) – ustawia tekst na pasku tytułu konsoli.
  2. Biblioteka user32.dll (Manipulacja oknem i menu systemowym):
    • ShowWindow(IntPtr hWnd, int nCmdShow) – pozwala ukrywać (SW_HIDE = 0) i pokazywać (SW_SHOW = 5) okno konsoli.
    • GetSystemMenu(IntPtr hWnd, bool bRevert) oraz DeleteMenu(...) – Kluczowe zabezpieczenie: domyślnie kliknięcie przez użytkownika przycisku X (Zamknij) na oknie konsoli wysyła sygnał CTRL_CLOSE_EVENT, który natychmiast zabija cały proces aplikacji WPF (bez wywołania zdarzeń Closing!). Usunięcie pozycji SC_CLOSE (0xF060) z menu systemowego konsoli dezaktywuje przycisk X konsoli, chroniąc aplikację przed przypadkowym zamknięciem.

Kompletna implementacja klasy ConsoleManager z wzorcem IDisposable

Ponieważ konsola systemowa jest zasobem niezarządzanym (spoza pamięci nadzorowanej przez Garbage Collector), klasa zarządzająca nią musi implementować pełny wzorzec IDisposable wraz z metodą GC.SuppressFinalize(this):

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace MyApp.Diagnostics;

public class ConsoleManager : IDisposable
{
    // Stałe Win32 API (User32.dll)
    private const int SW_HIDE = 0;
    private const int SW_SHOW = 5;
    private const int MF_BYCOMMAND = 0x00000000;
    private const int SC_CLOSE = 0xF060;

    // Importy z biblioteki Kernel32.dll
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool AllocConsole();

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool FreeConsole();

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetConsoleWindow();

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool SetConsoleTitle(string lpConsoleTitle);

    // Importy z biblioteki User32.dll
    [DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

    [DllImport("user32.dll")]
    private static extern bool DeleteMenu(IntPtr hMenu, int nPosition, int wFlags);

    private bool _isDisposed;

    public ConsoleManager(string title = "Debug Console")
    {
        // 1. Przydzielenie nowej konsoli dla procesu WPF
        if (AllocConsole())
        {
            SetConsoleTitle(title);

            // Odświeżenie standardowych strumieni wejścia/wyjścia klasy System.Console
            var stdOut = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
            Console.SetOut(stdOut);
            Console.SetError(stdOut);
            Console.SetIn(new StreamReader(Console.OpenStandardInput()));

            IntPtr handle = GetConsoleWindow();
            if (handle != IntPtr.Zero)
            {
                // 2. Dezaktywacja przycisku "X" konsoli, aby nie zabić procesu WPF
                IntPtr sysMenu = GetSystemMenu(handle, false);
                if (sysMenu != IntPtr.Zero)
                {
                    DeleteMenu(sysMenu, SC_CLOSE, MF_BYCOMMAND);
                }

                // 3. Wyświetlenie okna konsoli
                ShowWindow(handle, SW_SHOW);
            }
        }
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        // Informujemy Garbage Collector, że zasób został już zwolniony deterministycznie
        // i nie trzeba wywoływać finalizatora (~ConsoleManager)
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            FreeConsole();
            _isDisposed = true;
        }
    }

    ~ConsoleManager()
    {
        Dispose(disposing: false);
    }
}

Integracja z cyklem życia głównego okna (MainWindow.xaml.cs)

Zwolnienie konsoli umieszczamy w obsłudze zdarzenia Closing (lub Closed) głównego okna aplikacji:

using System;
using System.ComponentModel;
using System.Windows;
using MyApp.Diagnostics;

namespace MyApp;

public partial class MainWindow : Window
{
    private readonly ConsoleManager _console;

    public MainWindow()
    {
        InitializeComponent();
        _console = new ConsoleManager("Konsola Diagnostyczna WPF");

        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Konsola została pomyślnie zainicjalizowana.");
    }

    // Zdarzenie podpięte w XAML: <Window ... Closing="Window_Closing">
    private void Window_Closing(object? sender, CancelEventArgs e)
    {
        _console.Dispose();
    }
}