Gdy aplikacja desktopowa ma pokazać listę klientów, zamówień albo wyników raportu, zwykły ListBox szybko przestaje wystarczać. WPF DataGrid daje gotową tabelę z kolumnami, sortowaniem, edycją i walidacją, ale dopiero właściwe powiązanie danych decyduje o tym, czy rozwiązanie będzie wygodne i stabilne. Pokażę, jak zacząć, które właściwości ustawić oraz gdzie pojawiają się typowe problemy z wydajnością i edycją.
DataGrid w WPF najlepiej działa wtedy, gdy tabela jest świadomie związana z modelem danych
- ItemsSource łączy tabelę z kolekcją obiektów.
- ObservableCollection odświeża widok po dodaniu lub usunięciu elementu.
- INotifyPropertyChanged informuje interfejs o zmianie właściwości wiersza.
- AutoGenerateColumns="False" daje kontrolę nad kolejnością, formatem i edycją kolumn.
- ICollectionView pozwala dodać filtrowanie, sortowanie i grupowanie bez mieszania logiki z widokiem.
Do czego służy DataGrid w aplikacji WPF
DataGrid to kontrolka przeznaczona do prezentowania danych w układzie wierszy i kolumn. Każdy wiersz reprezentuje obiekt, a każda kolumna zwykle wskazuje jego właściwość, na przykład Name, Price albo IsActive. Dzięki temu jedna kontrolka może wyświetlać dane z kolekcji C#, zapytania LINQ, bazy SQL albo usługi sieciowej.
W praktyce dostajemy od razu kilka ważnych mechanizmów. Użytkownik może zaznaczać wiersze, zmieniać szerokość kolumn, sortować dane kliknięciem nagłówka, a w razie potrzeby również edytować wartości bez otwierania osobnego formularza.
To jednak nie jest kontrolka wyłącznie do wyświetlania tabeli. Można w niej umieścić pola wyboru, listy rozwijane, przyciski, obrazy czy własne szablony komórek. Ja traktuję ją jako warstwę prezentacji danych, a nie miejsce na reguły biznesowe. Walidację, zapis do bazy i operacje na rekordach lepiej trzymać w modelu lub ViewModelu.
| Element | Rola |
|---|---|
DataGrid |
Kontener tabeli i obsługa interakcji użytkownika |
DataGridTextColumn |
Wyświetlanie tekstu, liczb i dat |
DataGridCheckBoxColumn |
Wartości typu bool
|
DataGridComboBoxColumn |
Wybór wartości z listy |
DataGridTemplateColumn |
Własny wygląd i tryb edycji komórki |
Najprostsze powiązanie tabeli z kolekcją
Najkrótsza droga do działającej tabeli prowadzi przez właściwość ItemsSource. W przykładzie użyję listy produktów, ponieważ dobrze pokazuje zarówno tekst, liczbę, jak i wartość logiczną.
Po stronie C# potrzebujemy właściwości zawierającej dane. Do dynamicznych zmian najlepiej użyć ObservableCollection
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public bool IsAvailable { get; set; }
}
public class ProductsViewModel
{
public ObservableCollection Products { get; } = new()
{
new Product { Id = 1, Name = "Klawiatura", Price = 249.99m, IsAvailable = true },
new Product { Id = 2, Name = "Mysz", Price = 129.90m, IsAvailable = true }
};
} Jeżeli ustawiam DataContext okna na ViewModel, binding zaczyna działać bez ręcznego przypisywania danych w kodzie widoku.
public MainWindow()
{
InitializeComponent();
DataContext = new ProductsViewModel();
}Automatyczne kolumny są świetne do prototypu i szybkiego podglądu danych. W aplikacji produkcyjnej zwykle wyłączam je jednak przez AutoGenerateColumns="False". Dzięki temu przypadkowo dodana właściwość modelu nie pojawi się użytkownikowi jako nowa kolumna, a kolejność i format pozostają pod kontrolą.
Jak skonfigurować kolumny i edycję
Jawne kolumny dają przewidywalny interfejs
Ręczne zdefiniowanie kolumn wymaga kilku dodatkowych wierszy XAML, ale szybko się zwraca. Możemy ustawić nagłówki po polsku, szerokości, format liczb oraz to, które pola są tylko do odczytu.
Warto rozróżnić szerokość stałą, taką jak 120, od proporcjonalnej, zapisanej jako 2*. Ta druga pozwala wykorzystać wolne miejsce w oknie i zwykle lepiej sprawdza się przy kolumnach z dłuższym tekstem.
Edycja nie kończy się na zmianie wartości w komórce
DataGrid pozwala edytować komórki domyślnie, ale zapis zmiany do obiektu wymaga poprawnego bindingu. Jeżeli właściwość ma być zmieniana od razu po wpisaniu tekstu, przydaje się UpdateSourceTrigger=PropertyChanged. Przy bardziej złożonych danych bezpieczniej pozostawić aktualizację po opuszczeniu komórki i zatwierdzać cały wiersz.
Gdy użytkownik może wpisywać dane, model powinien implementować INotifyPropertyChanged. Bez tego tabela może zmienić obiekt, ale nie odświeżyć innych kontrolek, które korzystają z tej samej właściwości.
Do pól wyboru używam DataGridComboBoxColumn, a do niestandardowej prezentacji DataGridTemplateColumn. Ten drugi wariant pozwala mieć na przykład TextBlock w trybie wyświetlania i DatePicker po wejściu w edycję.
Przeczytaj również: var w JavaScripcie - zakres, hoisting i pułapki starego kodu
Walidację trzeba pokazać użytkownikowi
Najczęstszy błąd polega na przyjęciu, że typ decimal sam rozwiąże walidację ceny. Nie rozwiąże. Trzeba jeszcze obsłużyć pustą wartość, zakres liczby i komunikat, który wyjaśni, co użytkownik ma poprawić.
W prostych ekranach wystarczy binding z ValidatesOnExceptions. Przy większych formularzach lepiej użyć ValidationRule, IDataErrorInfo albo INotifyDataErrorInfo. Walidacja komórki sprawdza pojedynczą wartość, natomiast walidacja wiersza nadaje się do reguł zależnych od kilku pól, na przykład daty rozpoczęcia i zakończenia.
Sortowanie, filtrowanie i zaznaczanie rekordów
Kliknięcie nagłówka kolumny uruchamia domyślne sortowanie. To wystarcza dla prostych tabel, ale filtrowanie nie pojawia się automatycznie jako gotowy panel. Do takich operacji najwygodniej wykorzystać ICollectionView, czyli widok kolekcji, który potrafi nakładać sortowanie, filtrowanie i grupowanie bez zmieniania źródłowych danych.
public ICollectionView ProductView { get; }
public ProductsViewModel()
{
Products = new ObservableCollection();
ProductView = CollectionViewSource.GetDefaultView(Products);
}
public void FilterByName(string phrase)
{
ProductView.Filter = item =>
{
var product = (Product)item;
return string.IsNullOrWhiteSpace(phrase)
|| product.Name.Contains(phrase, StringComparison.OrdinalIgnoreCase);
};
} W XAML wystarczy wtedy podać jako źródło widok, a nie bezpośrednio kolekcję.
Jeżeli filtr reaguje na każdy znak wpisywany w pole tekstowe, przy dużej liczbie rekordów warto zastosować opóźnienie rzędu 200-300 ms. Bez niego każde naciśnięcie klawisza może uruchomić kosztowne filtrowanie i sprawić wrażenie, że aplikacja się zawiesza.
Do obsługi zaznaczonego rekordu najczęściej wystarczy SelectedItem. Gdy użytkownik ma wybrać wiele pozycji, ustawiam SelectionMode="Extended" i korzystam z SelectedItems. Ta właściwość nie jest klasyczną właściwością zależną WPF, dlatego w architekturze MVVM wymaga dodatkowego zachowania albo dołączonej właściwości.
Wydajność przy większej liczbie wierszy
Dla kilkudziesięciu lub kilkuset rekordów standardowa konfiguracja zwykle wystarcza. Problemy zaczynają się wtedy, gdy widok próbuje jednocześnie wyświetlić tysiące obiektów, rozbudowane szablony i obrazy. Samo włączenie wirtualizacji nie naprawi sytuacji, jeśli aplikacja wcześniej pobiera całą bazę do pamięci i wykonuje ciężkie operacje na wątku interfejsu.
Warto pozostawić włączoną wirtualizację wierszy i unikać ustawień, które wymuszają utworzenie wizualnych elementów dla wszystkich rekordów. Pomaga też ograniczenie liczby kolumn z własnymi kontrolkami oraz ładowanie danych partiami, na przykład po 100-500 elementów.
- Do małych zbiorów użyj zwykłej kolekcji i prostego bindingu.
- Przy dynamicznych zmianach wybierz ObservableCollection.
- Przy dużych zbiorach filtruj i sortuj po stronie zapytania do bazy.
- Nie twórz ciężkich kontrolek w każdej komórce, jeśli wystarczy tekst lub ikona.
- Nie blokuj wątku UI podczas pobierania danych z SQL albo API.
Moja praktyczna zasada jest prosta. Jeśli użytkownik potrzebuje przeglądać 50 tysięcy rekordów, nie próbuję na siłę załadować wszystkich do jednego DataGridu. Wybieram paginację lub filtrowanie po stronie serwera, bo poprawa architektury źródła danych daje znacznie więcej niż kolejne sztuczki w XAML.
Typowe problemy i rozsądne rozwiązania
| Problem | Najczęstsza przyczyna | Rozwiązanie |
|---|---|---|
| Puste komórki | Niepoprawna nazwa właściwości w bindingu | Sprawdź ścieżkę, typ obiektu i komunikaty bindingu w Output |
| Brak odświeżenia po dodaniu rekordu | Użycie List zamiast kolekcji obserwowalnej |
Zastosuj ObservableCollection
|
| Zmiana modelu niewidoczna w UI | Brak INotifyPropertyChanged
|
Dodaj powiadomienia w setterach |
| Niepożądane kolumny | Automatyczne generowanie | Ustaw AutoGenerateColumns="False"
|
| Powolne filtrowanie | Filtrowanie całej kolekcji przy każdym znaku | Dodaj opóźnienie albo przenieś operację do bazy |
Warto też pamiętać o różnicy między DataContext a ItemsSource. Pierwszy wskazuje obiekt, z którego binding pobiera właściwości, drugi określa konkretną kolekcję wierszy. Gdy tabela jest pusta, najpierw sprawdzam właśnie te dwa miejsca, a dopiero później wygląd kontrolki.
W aplikacjach korzystających z MVVM nie podpinam zdarzeń do kodu okna tylko po to, żeby wykonać operację na zaznaczonym wierszu. Lepszy jest command w ViewModelu, szczególnie gdy przycisk znajduje się w DataGridTemplateColumn. Dzięki temu logikę można łatwiej przetestować i ponownie wykorzystać.
Jaką konfigurację wybrać w konkretnej aplikacji
Do prototypu wystarczy ItemsSource i automatyczne kolumny. To dobry sposób, aby sprawdzić, czy model danych jest poprawny. Gdy ekran trafia do użytkowników, przechodzę na jawne kolumny, wyłączam niepotrzebną edycję i dodaję formatowanie, walidację oraz jasne komunikaty błędów.
Jeśli tabela ma być głównie raportem, ustawiam IsReadOnly="True" i skupiam się na sortowaniu, filtrowaniu oraz eksporcie. Jeżeli ma pełnić funkcję edytora, projektuję ją bardziej jak formularz rozłożony na wiersze, z ograniczoną liczbą edytowalnych pól i walidacją działającą przed zapisem.
Nie każda tabela powinna też być DataGridem. Gdy użytkownik ma edytować jeden rekord z wieloma zależnościami, klasyczny formularz będzie czytelniejszy. DataGrid wygrywa wtedy, gdy najważniejsza jest praca na wielu podobnych rekordach jednocześnie.
Od czego zacząć, żeby tabela nie urosła w problem
Najpierw przygotuj prosty model i sprawdź binding na kilku rekordach. Dopiero potem dodawaj edycję, filtry, grupowanie i własne szablony. Taka kolejność pozwala szybko odróżnić problem danych od problemu wyglądu.
W większości aplikacji desktopowych wystarczy połączenie ObservableCollection, jawnych kolumn, ICollectionView i walidacji. Najwięcej czasu tracimy nie na samą kontrolkę, lecz na niejasny model danych, brak powiadomień i próbę upchnięcia całej logiki biznesowej w XAML.
