Przemyślenia dotyczące kontrolek Windows Forms
Chciałem się z Wami podzielić moimi przemyśleniami, które są wynikiem
mojej dotychczasowej pracy z aplikacjami Windows Forms. Na początku
krótkie streszczenie:
standardowe kontrolki Windows Forms dostarczane z .Net Framework są do bani!
Rzecz
będzie o dwóch kontrolkach, z którymi miałem najwięcej do czynienia, a
mianowicie DateTimePicker oraz NumericUpDown. Kiedyś rozmawiając z
programistą jednej z firm we Wrocławiu, tworzących oprogramowanie
usłyszałem, że owszem piszą w .Net i wykorzystują standardowe
kontrolki, ale opakowują je własnymi klasami. Wtedy jeszcze nie
widziałem konkretnego zastosowania takiej taktyki. Teraz po zmaganiach
z ww. kontrolkami zauważam potencjalne korzyści. W dużych projektach
mamy mnóstwo wymagań biznesowych, które dokładnie mówią jak ma
przebiegać proces interakcji z kontrolką. Jak się okazało nie zawsze
zachowanie danej kontrolki jest zgodne z naszymi oczekiwaniami. Wtedy
zaczynają się zmagania…
Bitwa no.1. Walczyłem ostatnio z
kontrolką NumericUpDown. Okazało się, że ustawienie jej własności
określającej ilość miejsc po przecinku oraz maksymalnej wartości nie do
końca się sprawdza. Wartości te są aplikowane dopiero w czasie
walidacji zawartości kontrolki, natomiast nie blokują wpisywania.
Dlatego, mimo że ustawiłem 4 miejsca po przecinku, użytkownik może z
powodzeniem wpisać 10 cyfr i zapisać edytowany dokument będąc
przekonanym, że wprowadzona wartość jest poprawna (numeric oczywiście
obetnie te nadmiarowe miejsca podczas walidacji przy zapisywaniu). Jak
się okazało takie zachowanie nie jest akceptowalne i musiałem sobie z
tym sam poradzić. Ręczne wywołanie parsowania wprowadzonego tekstu
okazało się w miarę proste. Problem pojawił się, gdy trzeba było
blokować wpisywanie więcej niż ustalona liczba miejsc po przecinku
zachowując pozycję kursora w odpowiednim miejscu. Uważam, że nie ma
rzeczy niemożliwych dlatego i z tym sobie poradziłem. Wykorzystując
Reflectora obejrzałem sobie jak wygląda numeric od wewnątrz i
dowiedziałem się, że składa się między innymi z TextBoxa, który jest
prywatny. Mechanizm refleksji pozwolił mi dobrać się do tego textboxa i
ustawiać na nim kursor jak mi się podoba. W wyniku moich zmagań
powstała kontrolka, która dziedziczy po standardowym numericu i zmienia
jego zachowanie.
Bitwa no.2. Dostosowałem też
DateTimePickera. Podczas pracy ze standardowym DTP wynikło kilka
problemów. Pierwszym i najbardziej przeszkadzającym jest fakt, że
kontrolka ta zmienia aktualnie wybraną datę i wbija ją do zbindownej
pod spodem encji za każdym razem gdy:
- Zmieni się rok, miesiąc lub dzień wpisując je ręcznie (przy czym wystarczy zmienić tylko jedną z tych części daty).
- Gdy otworzy się kalendarz i zmieni się miesiąc jedną ze strzałek w prawym i lewym górnym rogu.
Takie
zachowanie nie tylko mi przeszkadzało, ale także denerwowało. Przede
wszystkim chodziło o walidację wprowadzonej daty. W moim przypadku
każda zmiana wiązała się z pytaniem użytkownika o to czy zmienić
również tą datę na innym elemencie. Oczywistym jest fakt, że nie mogłem
wykorzystać zdarzenia ValueChanged. Więc dobrze, myślę sobie, zdarzenie
Validated powinno pomóc. Pomogło, ale na krótko. Okazało się, że w
jednym przypadku się nie sprawdza. Gdy nie zmienimy daty i opuścimy
kontrolkę będziemy mieli walidację i ni jak nie będziemy w stanie
sprawdzić czy data się zmieniła czy nie (chyba że o czymś nie wiem).
Rozwiązanie ze zmienną walającą się gdzieś w kodzie i pamiętającą
ostatnio wybraną datę w ogóle mnie nie zadowalało.
Kolejnym
problemem było sprawdzenie czy podana przez użytkownika data mieści się
w zadanym przedziale. Wykorzystywałem do tego właściwości MinDate i
MaxDate. Wszystko byłoby dobrze, gdyby nie pewien przypadek. Mając
przedział czasu, który rozpina się na kilka lat występuje problem z
wprowadzaniem daty ręcznie. Powiedzmy, że mamy przedział od 2006-01-01
do 2007-05-31. Jeżeli będziemy mieli wpisaną datę 2006-06-01 i
zaczniemy wpisywać datę 2007-01-01, która jest poprawna, i zaczniemy od
podania roku to DTP nie przyjmie tej wartości, gdyż po ukończeniu
wpisywania roku waliduje całą datę i stwierdzi, że data 2007-06-01 jest
poza zakresem :/
W końcu po wielu zmaganiach stwierdziłem, że
zdefiniowanie własnego zachowania poprzez dziedziczenie ze
standardowego DTP ułatwi mi życie. Jak pomyślałem tak zrobiłem.
Zdefiniowałem sobie nową właściwość z wybraną datą, do której mogę
sobie bindować i być pewnym, że jeżeli walidacja nie przejdzie to nie
pojawi mi się tam żadna dziwna data. Zdefiniowałem sobie swoje
zdarzenie odpowiadające zmianie daty, które odpala się dopiero po
faktycznym potwierdzeniu przez użytkownika wyboru daty. Dodatkowo
pamiętam sobie datę sprzed edycji co pozwala mi ignorować sytuację, gdy
użytkownik wybrał taką samą datę jaka była. Wszystko łatwo i przyjemnie
i tak jak ja chcę.
Uważam, że pisząc jakikolwiek większy system
powinniśmy korzystać z opakowanych wersji standardowych kontrolek.
Nawet jeżeli to opakowanie nie wnosiłoby nic nowego to jednak w
przyszłości pozwoli na łatwe modyfikacje. Jak by na to nie patrzeć
byłaby to inwestycja na przyszłość, która nie tylko pozwala utrzymać
spójność, ale także ułatwia wprowadzanie zmian.