Gdy buduję formularz w aplikacji WPF, pole tekstowe zwykle wygląda niepozornie, ale to właśnie ono odpowiada za sporą część kontaktu użytkownika z programem. Kontrolka TextBox może obsługiwać pojedynczy wiersz, dłuższy opis, walidację, wiązanie z ViewModelem i reakcję na klawiaturę. Poniżej pokazuję praktyczną konfigurację, typowe błędy oraz sytuacje, w których lepiej użyć innego elementu interfejsu.
TextBox wystarczy do większości formularzy, jeśli dobrze ustawisz trzy rzeczy
- Tryb pracy kontroluje, czy pole przyjmuje jedną linię, czy większy blok tekstu.
- Binding łączy wartość TextBoxa z właściwością w kodzie i ogranicza potrzebę obsługi zdarzeń.
- UpdateSourceTrigger decyduje, kiedy wpisana wartość trafia do źródła danych.
- Walidacja powinna informować użytkownika o błędzie bez kasowania jego wpisanego tekstu.
- PasswordBox i RichTextBox są lepszym wyborem, gdy potrzebujesz odpowiednio ukrywania hasła albo formatowania treści.
Od czego zacząć z kontrolką TextBox w WPF
Najprostsze pole tekstowe dodaje się w XAML jednym elementem. Właściwość Text przechowuje aktualną zawartość, a x:Name pozwala odwołać się do kontrolki z kodu C#.
Wysokość pola nie zawsze zachowuje się tak, jak oczekuje początkujący. Przy pojedynczym wierszu WPF dopasowuje zawartość do wysokości kontrolki, natomiast przy większym obszarze trzeba jawnie włączyć zawijanie tekstu i przewijanie.
AcceptsReturn pozwala wstawiać znaki nowej linii klawiszem Enter, a TextWrapping zawija długie wiersze. Samo zwiększenie wysokości bez tych właściwości nie zamieni pola w wygodny edytor opisu.
W kodzie C# odczyt i zmiana wartości są proste:
string name = NameTextBox.Text.Trim();
NameTextBox.Text = "Nowa wartość";
NameTextBox.Focus();
NameTextBox.SelectAll();Takie odwołania sprawdzają się w małych oknach i prostych prototypach. W większej aplikacji wolę jednak wiązanie danych, ponieważ bezpośredni dostęp do kontrolek szybko miesza logikę interfejsu z logiką biznesową.
Właściwości, które naprawdę zmieniają zachowanie pola
TextBox ma wiele właściwości, ale kilka z nich pojawia się niemal w każdym formularzu. Najczęściej ustawiam je zależnie od rodzaju danych, które użytkownik ma wpisać.
| Właściwość | Zastosowanie |
|---|---|
| MaxLength | Ogranicza liczbę znaków, na przykład do 100 znaków nazwy. |
| IsReadOnly | Pozwala wyświetlać tekst bez umożliwiania jego edycji. |
| TextWrapping | Decyduje, czy długie wiersze mają być zawijane. |
| AcceptsReturn | Włącza wpisywanie wielu linii za pomocą klawisza Enter. |
| CharacterCasing | Zmienia wpisywane litery na wielkie albo małe. |
| VerticalContentAlignment | Ustawia położenie tekstu wewnątrz pola, szczególnie ważne przy większej wysokości. |
Przykładowe pole na krótką nazwę może wyglądać tak:
MaxLength nie zastępuje walidacji. Ogranicza długość wpisu, ale nie sprawdza, czy wartość ma sens, czy nie składa się wyłącznie ze spacji i czy spełnia reguły biznesowe.
Pojedyncza linia czy większy opis
Do nazwy, identyfikatora lub adresu e-mail używam zwykłego pola jednoliniowego. Dla komentarza, opisu błędu albo treści wiadomości ustawiam AcceptsReturn="True" oraz przewijanie pionowe.
Nie ustawiam dużej wysokości dla każdego pola. Puste miejsce sugeruje użytkownikowi, że powinien wpisać dłuższą treść, nawet jeśli aplikacja oczekuje tylko krótkiej wartości. Interfejs powinien podpowiadać przeznaczenie kontrolki samym rozmiarem.
Tekst tylko do odczytu
Jeżeli użytkownik ma skopiować wartość, ale nie może jej zmieniać, lepsze będzie IsReadOnly="True" niż wyłączenie kontrolki przez IsEnabled="False". Wyłączony TextBox zwykle jest wyszarzony i trudniej z niego skopiować tekst.
To drobna różnica, która poprawia użyteczność. Pole pozostaje aktywne z punktu widzenia zaznaczania i kopiowania, ale użytkownik nie zmieni jego wartości.
Wiązanie danych i moment zapisu wartości
W aplikacji opartej na MVVM TextBox najczęściej łączę z właściwością ViewModelu. Dzięki temu interfejs nie musi ręcznie pobierać tekstu z każdej kontrolki przed zapisaniem formularza.
W tym przykładzie zmiana tekstu od razu aktualizuje właściwość UserName. Domyślne zachowanie dla TextBoxa jest inne niż wiele osób zakłada. Wartość źródłowa zwykle aktualizuje się po utracie fokusu, dlatego przy wyszukiwaniu, podpowiedziach lub licznikach znaków jawnie ustawiam UpdateSourceTrigger=PropertyChanged.
Wiązanie można rozszerzyć o wartość początkową, tryb oraz walidację wyjątków:
Mode="TwoWay" oznacza przepływ danych w obu kierunkach. Zmiana ViewModelu odświeża kontrolkę, a wpis użytkownika trafia do ViewModelu. Dla właściwości tekstowych edytowanych przez użytkownika jest to zazwyczaj właściwy wybór.
Dlaczego wpisana wartość nie pojawia się od razu w ViewModelu
Najczęstszą przyczyną jest brak ustawienia UpdateSourceTrigger. Jeżeli logika reaguje na każdą zmianę, użyj wartości PropertyChanged. Jeżeli zapis ma nastąpić dopiero po zakończeniu edycji, domyślne zachowanie związane z utratą fokusu może być wygodniejsze i mniej obciążające.
Przy filtrowaniu listy po każdym znaku trzeba też uważać na wydajność. Dla kilku tysięcy elementów natychmiastowe wyszukiwanie może działać dobrze, ale przy większych zbiorach lepiej zastosować opóźnienie, asynchroniczne filtrowanie albo uruchamianie operacji po naciśnięciu Enter.
Binding a ręczne zdarzenie TextChanged
Zdarzenie TextChanged jest przydatne, gdy reagujesz na zmianę samego interfejsu, na przykład aktualizujesz licznik znaków. Nie używam go jako podstawowego mechanizmu zapisu formularza, gdy dane i tak mogą zostać obsłużone przez binding.
private void DescriptionTextBox_OnTextChanged(
object sender,
TextChangedEventArgs e)
{
CounterTextBlock.Text =
$"{DescriptionTextBox.Text.Length}/500";
}W MVVM licznik można oprzeć na właściwości ViewModelu i uniknąć obsługi zdarzenia w pliku code-behind. Nie traktuję jednak code-behind jako błędu. W małym oknie prosty handler bywa czytelniejszy niż rozbudowana infrastruktura tylko po to, by usunąć kilka linii C# z widoku.
Walidacja danych bez frustracji użytkownika
Samo przyjęcie tekstu nie oznacza, że dane są poprawne. Pole wieku powinno przyjmować liczbę z określonego zakresu, a nazwa użytkownika nie może składać się z pustych znaków. WPF pozwala połączyć TextBox z walidacją na poziomie bindingu.
Dla prostych przypadków można sprawdzać błędy zgłaszane przez właściwość ViewModelu:
Gdy użytkownik wpisze tekst, którego nie da się przekonwertować na typ liczbowy, binding może oznaczyć kontrolkę jako niepoprawną. Domyślny styl WPF często pokazuje czerwone obramowanie, ale w produkcyjnej aplikacji warto dodać czytelny komunikat, na przykład w ToolTipie lub obok pola.
Walidacja zakresu za pomocą ValidationRule
Jeżeli reguła dotyczy konkretnego pola, osobna klasa ValidationRule bywa bardzo przejrzysta. Poniższy przykład akceptuje wiek od 18 do 120 lat.
public sealed class AdultAgeRule : ValidationRule
{
public override ValidationResult Validate(
object value,
CultureInfo cultureInfo)
{
if (!int.TryParse(value?.ToString(), out int age))
{
return new ValidationResult(
false,
"Wpisz liczbę całkowitą.");
}
if (age < 18 || age > 120)
{
return new ValidationResult(
false,
"Wiek musi mieścić się w zakresie od 18 do 120.");
}
return ValidationResult.ValidResult;
}
}Regułę można podłączyć w XAML:
Moim zdaniem najlepszy komunikat walidacyjny odpowiada na dwa pytania naraz: co jest nie tak i jak to poprawić. Samo czerwone obramowanie jest sygnałem wizualnym, ale nie rozwiązuje problemu, zwłaszcza dla użytkownika korzystającego z klawiatury albo czytnika ekranu.
Przeczytaj również: ComboBox w WPF od podstaw - binding, MVVM i walidacja
Ograniczanie znaków w trakcie pisania
Czasami aplikacja powinna przyjmować wyłącznie cyfry albo określony format. Można przechwycić zdarzenie PreviewTextInput, ale nie należy traktować go jako pełnej walidacji. Wklejenie tekstu, operacje IME i niektóre metody wprowadzania danych mogą ominąć prosty filtr klawiatury.
private void DigitsOnly_OnPreviewTextInput(
object sender,
TextCompositionEventArgs e)
{
e.Handled = e.Text.Any(character => !char.IsDigit(character));
}Filtr wejścia poprawia wygodę, ale właściwa kontrola danych powinna nadal znajdować się w ViewModelu lub warstwie domenowej. Użytkownik może wkleić niepoprawną wartość, a kod może otrzymać dane z pliku, schowka albo innego źródła.
Obsługa zdarzeń, poleceń i klawiatury
TextBox dobrze współpracuje z klawiaturą, ale warto świadomie ustawić zachowanie formularza. Przykładowo Enter może zatwierdzać formularz, wstawiać nową linię albo przenosić fokus. Wszystko zależy od tego, czy kontrolka jest jednoliniowa, czy wieloliniowa.
Do reakcji na Enter w jednoliniowym polu można użyć polecenia w przycisku albo obsługi klawisza w oknie. W aplikacji MVVM preferuję polecenia, ponieważ logika zatwierdzania pozostaje poza widokiem.
IsDefault="True" pozwala zatwierdzić okno klawiszem Enter, a IsCancel="True" zwykle wiąże przycisk z klawiszem Escape. W polu wieloliniowym Enter powinien pozostać znakiem nowej linii, więc nie przenosiłbym tam tej samej logiki bez dodatkowego skrótu, na przykład Ctrl+Enter.
Dla wygodnej nawigacji klawiszem Tab ustawiam logiczną kolejność kontrolek przez TabIndex. Dodaję też etykietę opisującą pole, zamiast polegać wyłącznie na tekście zastępczym.
WPF nie ma natywnego placeholdera znanego z aplikacji webowych. Zamiast wkładać instrukcję do właściwości Text i później ją usuwać, stosuję etykietę, ToolTip albo własny styl. Dzięki temu tekst wprowadzony przez użytkownika nie miesza się z podpowiedzią.
Kiedy TextBox nie jest najlepszym wyborem
Nie każde pole związane z tekstem powinno być kontrolką TextBox. Wybór właściwego elementu ogranicza liczbę obejść i sprawia, że kod jest prostszy.
| Potrzeba | Lepsza kontrolka | Powód |
|---|---|---|
| Hasło lub poufny sekret | PasswordBox | Nie pokazuje znaków wprost i ma model przeznaczony do haseł. |
| Formatowany tekst, obrazy i tabele | RichTextBox | Obsługuje dokument z formatowaniem zamiast zwykłego tekstu. |
| Tekst wyłącznie do wyświetlenia | TextBlock | Jest prostszy, gdy użytkownik nie ma nic wpisywać ani edytować. |
| Wybór jednej wartości z listy | ComboBox | Ogranicza dane do dostępnych opcji i zmniejsza liczbę błędów. |
| Liczba, data lub kwota | TextBox z walidacją albo kontrolka dedykowana | Sam tekst nie zapewnia poprawnego typu ani formatu. |
Najczęstszy błąd polega na używaniu TextBoxa do każdego rodzaju danych. Pole tekstowe pozwala wpisać niemal wszystko, ale ta elastyczność oznacza, że aplikacja musi później wykonać konwersję i walidację. Jeżeli zbiór poprawnych wartości jest zamknięty, ComboBox będzie wygodniejszy dla użytkownika i bezpieczniejszy dla programu.
Do haseł wybieram PasswordBox, a nie TextBox z własnym ukrywaniem znaków. Do edycji dokumentów z pogrubieniem, listami czy obrazami używam RichTextBox, bo przechowywanie takiej treści w zwykłym łańcuchu znaków szybko prowadzi do niepotrzebnych komplikacji.
Dobrze skonfigurowane pole tekstowe zaczyna się od danych
Najważniejsza decyzja nie dotyczy koloru obramowania, lecz tego, jaką wartość pole ma przyjmować i kiedy aplikacja ma jej zaufać. Dopiero potem dobieram rozmiar, zawijanie tekstu, sposób walidacji i reakcję na klawiaturę.
W prostym formularzu wystarczy poprawny XAML, binding i jasny komunikat błędu. W większej aplikacji dochodzą ViewModel, reguły domenowe, dostępność oraz obsługa wklejania i nietypowych sposobów wprowadzania. Taki TextBox nie tylko wygląda poprawnie, ale też zachowuje się przewidywalnie wtedy, gdy użytkownik wpisze dokładnie to, czego się od niego nie spodziewasz.
