2/2014 (21)
www•programistamag•pl
Cena 12.90 zł (w tym VAT 23%)
Index: 285358
WPF:STYLOWANIEKONTROLEK •POCZĄTKIZFORTRAN •CONCEPTSLITE:ROZSZERZENIE STANDARDUC++14
Własny debugger
dla Windows
Badanie jakości
kodu C ++
Interface a
implementacja
Przedstwiamy szcze-
góły interfejsu
Windows Debug API
O narzędziach i metry-
kach do analizy
jakości oprogramowania
Separowanie interfejsu
użytkownika od
logiki aplikacji
Javarewolucyjne
zmiany
4 / 2 . 2014 . (21) /
REDAKCJA/EDYTORIAL
W tym wydaniu Programisty absolutna gratka dla progra-
mistów Java – obszerny artykuł Tomasza Nurkiewicza „Java 8
– najbardziej rewolucyjna wersja w historii” opisujący przełomo-
we zmiany w nowej wersji tego języka.
Współautor cyklu „Bliżej silikonu”, Mateusz Jurczyk, nadal
pozostaje przy temacie niskopoziomowego programowania
i, tym razem indywidualnie, przybliży nam, jak napisać własny
debugger w systemie Windows. Dziś pierwsza część tego arty-
kułu, a na drugą odsłonę zapraszamy już w kolejnym numerze.
Jako że język C++ z pewnością nie należy do najprostszych,
ciekawą propozycją dla jego użytkowników jest artykuł Sła-
womira Zborowskiego o badaniu jakości kodu stworzonego w
tymże języku. Tekst opisuje różne rodzaje metod badania jako-
ści i narzędzia do tego przeznaczone – poczynając od detekcji
copy-paste, aż po analizę statyczną.
Programistów C# powinien zainteresować natomiast ar-
tykuł Wojciecha Sury „Interface a implementacja” o rozwiązy-
waniu problemów dotyczących łączenia GUI z implementacją.
Prezentuje on złe i dobre podejścia do tego tematu i pokazuje,
jak stworzyć aplikację, w której widoki można zmieniać niczym
rękawiczki.
To jednak nie wszystko, co można znaleźć w najnowszym
wydaniu Programisty, więc jak zwykle zachęcamy do znalezie-
nia wygodnego miejsca i dokładnego zapoznania się z najnow-
szym wydaniem, w którym każdy, mamy nadzieję, znajdzie dla
siebie interesujący temat.
Z wyrazami szacunku
Redakcja
Wydawca/ Redaktor naczelny:
Anna Adamczyk
annaadamczyk@programistamag.pl
Redaktor prowadzący:
Łukasz Łopuszański
lukaszlopuszanski@programistamag.pl
Korekta:
Tomasz Łopuszański
Kierownik produkcji:
Krzysztof Kopciowski
bok@keylight.com.pl
DTP:
Krzysztof Kopciowski
Dział reklamy:
reklama@programistamag.pl
tel. +48 663 220 102
tel. +48 604 312 716
Prenumerata:
prenumerata@programistamag.pl
Współpraca:
Michał Bartyzel
Mariusz Sieraczkiewicz
Michał Leszczyński
Marek Sawerwain
Łukasz Mazur
Rafał Kułaga
Sławomir Sobótka
Michał Mac
Gynvael Coldwind
Bartosz Chrabski
Adres wydawcy:
Dereniowa 4/47
02-776 Warszawa
Druk:
ArtDruk – www.artdruk.com
ul. Napoleona 4
05-230 – Kobyłka
Nakład: 5000 egz.
Redakcja zastrzega sobie prawo do skrótów
i opracowań tekstów oraz do zmiany planów
wydawniczych, tj. zmian w zapowiadanych tematach
artykułów i terminach publikacji, a także nakładzie
i objętości czasopisma.
O ile nie zaznaczono inaczej, wszelkie prawa do
materiałów i znaków towarowych/firmowych
zamieszczanych na łamach magazynu Programista są
zastrzeżone. Kopiowanie i rozpowszechnianie ich bez
zezwolenia jest Zabronione.
Redakcja magazynu Programista nie ponosi
odpowiedzialności za szkody bezpośrednie
i pośrednie, jak również za inne straty i wydatki
poniesione w związku z wykorzystaniem informacji
prezentowanych na łamach magazynu Programista.
Magazyn Programista wydawany jest
przez Dom Wydawniczy Anna Adamczyk
Zamów prenumeratę magazynu Programista przez formularz na stronie http://programistamag.pl/typy-prenumeraty/
lub zrealizuj ją na podstawie faktury Pro-forma. W spawie faktur Pro-Forma prosimy kontktować się z nami drogą
mailową redakcja@programistamag.pl.
Prenumerata realizowana jest także przez RUCH S.A. Zamówienia można składać bezpośrednio na stronie
www.prenumerata.ruch.com.pl Pytania prosimy kierować na adres e-mail: prenumerata@ruch.com.pl lub kontaktując
się telefonicznie z numerem: 801 800 803 lub 22 717 59 59 (godz.: 7:00 – 18:00 (koszt połączenia wg taryfy operatora).
5/ www.programistamag.pl /
SPIS TREŚCI
BIBLIOTEKI I NARZĘDZIA
Interface a implementacja.......................................................................................................................
Wojciech Sura
6
Wstęp do WPF – część 2: Stylowanie kontrolek w WPF.....................................................................
Wojciech Sura
12
ASP.NET SignalR – czyli aplikacje czasu bardzo rzeczywistego. Część 2..........................................
Karol Rogowski
16
JĘZYKI PROGRAMOWANIA
Java 8 – najbardziej rewolucyjna wersja w historii...............................................................................
Tomasz Nurkiewicz
22
PoczątkizjęzykiemFortran......................................................................................................................
Radosław Suduł
30
Concepts Lite. Rozszerzenie standardu C++14...................................................................................
Robert Matusewicz
36
PROGRAMOWANIE SYSTEMOWE
Jak napisać własny debugger w systemie Windows – część 1..........................................................
Mateusz “j00ru” Jurczyk
38
TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ
Badanie jakości kodu C++........................................................................................................................
Sławomir Zborowski
46
LABORATORIUM BOTTEGA
Refaktoryzacja testów legacy w kierunku wykonywalnych specyfikacji. Część II: Techniki uła-
twiające utrzymanie testów....................................................................................................................
Rafał Jamróz
54
Brakujący element Agile. Część 1: Feedback........................................................................................
Paweł Badeński
60
PLANETA IT
Szczyt za szczytem.....................................................................................................................................
Łukasz Sobótka
64
STREFA CTF
Ghost in the Shellcode 2014 – Pwn Adventure 2................................................................................
Michał "Redford" Kowalczyk
66
KLUB LIDERA IT
Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 6).......................
Mariusz Sieraczkiewicz
70
KLUB DOBREJ KSIĄŻKI
Scala od podszewki...................................................................................................................................
Marek Sawerwain
72
6 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Wojciech Sura
NIE METODA, A AKCJA
Spróbujmy spojrzeć na interface użytkownika z nieco innej perspektywy. W
momencie, gdy użytkownik klika przycisk lub wybiera opcję z menu, jego
intencją jest wykonanie pewnej akcji: otwarcie nowego dokumentu, skopio-
wanie fragmentu tekstu do schowka albo zamknięcie programu. Przez akcję
rozumiemy zatem pojedynczą operację, którą można wywołać z poziomu in-
terface'u użytkownika naszej aplikacji.
Najprostszym sposobem zaimplementowania takiej akcji jest wywołanie
określonej metody w momencie, gdy użytkownik wybierze odpowiedni ele-
ment interface’u. Sytuacja skomplikuje się jednak nieco, gdy okaże się, że ta
sama akcja może zostać wywołana z poziomu kilku różnych miejsc albo – co
gorsza – że jej wywołanie uwarunkowane jest zaistnieniem pewnych szcze-
gólnych okoliczności.
Żaden z opisanych problemów nie jest oczywiście nie do rozwiązania.
Tyle tylko, że oprogramowanie odpowiednich mechanizmów zajmuje czas,
wymaga rozszerzenia architektury aplikacji i panowania nad wszystkim tak,
aby na koniec nie utonąć w gąszczu zależności, warunków i flag.
Odpowiedzią na powyższe potrzeby jest mechanizm, który spełnia nastę-
pujące kryteria. Po pierwsze, pozwala na zdefiniowanie akcji jako osobnego
bytu, stanowiącego pomost pomiędzy interfacem użytkownika a kodem źró-
dłowym. Po drugie, umożliwia określenie zależności pomiędzy elementami
interface’u, a później automatycznie pilnuje ich w trakcie pracy programu.
Dzięki temu programista nie musi skupiać się na niepotrzebnej pracy, bo
integralności interface’u pilnuje jeden spójny mechanizm. Na koniec dobrze
byłoby, gdyby był on zaprojektowany w sposób, który ułatwia rozwój zarów-
no jego samego, jak i aplikacji, którą zarządza.
Chciałbym zaproponować dwa rozwiązania. Pierwszym z nich – z uwagi
na brak takowego w standardowych bibliotekach .NET dla Windows Forms
– jest otwarty mechanizm o nazwie Visual State Manager. Postaram się wyja-
śnić, w jaki sposób można z niego skorzystać i go rozwijać, a także na jakich
pomysłach opiera się jego działanie, co pozwoli w łatwy sposób przygotować
w razie potrzeby jego odpowiednik dla innego języka programowania lub
frameworka. Drugim z nich jest mechanizm przygotowany przez Microsoft
działający w środowiskuWPF, którego można użyć zarówno w aplikacjach de-
sktopowych, Windows 8, jak i dla Windows Store Apps lub Windows Phone.
VISUAL STATE MANAGER
Visual State Manager dla Windows Forms opiera się na istnieniu i współpracy
trzech rodzajów obiektów: akcji, warunków i operatorów kontrolek (Action,
Condition, Control operator). Zadaniem programisty jest zdefiniowanie zależ-
ności pomiędzy tymi trzema warstwami, a następnie dostarczanie informacji
o zajściu zdarzeń mogących mieć wpływ na warunki, a w efekcie na akcje.
Warunek
Warunek jest prostym obiektem przechowującym pojedynczą wartość typu
bool mówiącą o tym, czy jest on w tym momencie spełniony, czy też nie.
Mechanizm managera wykorzystuje warunki do regulowania dostępności,
widoczności i zaznaczenia elementów interface'u odpowiedzialnych za wy-
woływanie akcji.
public interface ICondition
{
bool Value
{
get;
}
event ValueChangedHandler ValueChanged;
}
Warunek dostępny jest w postaci bardzo prostego w implementacji in-
terface’u Icondition: wystarczy dostarczyć tylko własność informującą
o obecnym stanie warunku oraz zdarzenie powiadamiające o tym, iż stan
ów właśnie się zmienił. Dla wygody przygotowane jest kilka klas implemen-
tujących ten interface. Pierwszą z nich jest Condition – najprostsza imple-
mentacja. Ponadto możemy skorzystać z klasy CompositeCondition, któ-
ra pozwala na połączenie kilku warunków przy pomocy operacji logicznych
„i” oraz „lub”, a także NegateCondition, która neguje wskazania innego
warunku.
Akcja
Akcja stanowi abstrakcję dla pewnego działania, które może podjąć użytkow-
nik. Każdą akcję – jak już wcześniej wspomniałem – możemy scharakteryzo-
wać trzema cechami: dostępnością, widocznością oraz zaznaczeniem. Do-
stępność określa, czy użytkownik może w danym momencie akcję wywołać,
widoczność informuje, czy kontrolki odpowiedzialne za tę akcję powinny być
w ogóle wyświetlane, zaś zaznaczenie przydaje się w przypadku akcji odpo-
wiadających za przełączenie jakiegoś stanu (na przykład zawijanie linii lub
widoczność paska narzędzi).
Zadaniem akcji jest również automatyczne przywiązanie się do wska-
zanych kontrolek w interface użytkownika. Dzięki temu programista zwol-
niony jest z obowiązku ustawienia tym kontrolkom odpowiednich handle-
rów zdarzeń, a także dbania o synchronizację stanów – wszystko dzieje się
automatycznie.
Skorzystanie z akcji jest stosunkowo proste, ponieważ wystarczy tylko ją
zainstancjonować, przekazując w locie parametry definiujące, w jaki sposób
powinna działać:
»» Metodę,któramazostaćwywołanapowybraniuakcjiprzezużytkownika;
»» (opcjonalnie) warunek regulujący dostępność akcji;
»» (opcjonalnie) warunek regulujący zaznaczenie elementów interface'u po-
wiązanych z akcją;
»» (opcjonalnie) warunek regulujący widoczność elementów interface'u po-
wiązanych z akcją;
»» (opcjonalnie) kontrolki, które będą odpowiadały za wywołanie akcji.
Interface a implementacja
W każdym programie okienkowym prędzej czy później programista staje przed
zadaniem powiązania wizualnych kontrolek z kodem źródłowym, który realizuje za
ich sprawą różne zadania. Najprostszym i często stosowanym sposobem jest napi-
sanie kodu bezpośrednio w handlerach zdarzeń. Sposób ten oczywiście zadziała,
ale to jest chyba wszystko dobre, co można o nim powiedzieć. W niniejszym artyku-
le chciałbym zaproponować nieco inne podejście do rozwiązywania tego problemu.
8 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Operator kontrolek
Ostatnią z opisywanych warstw stanowi operator kontrolek, który pozwala
ujednolicić sposób współpracy akcji z komponentami wizualnymi. Domyśl-
nie dostarczona jest implementacja dla większości standardowych kontrolek,
więc jeśli skorzystamy z nich, nie trzeba robić już nic więcej. W przypadku
mniej standardowych, Visual State Manager spróbuje ustawić odpowiednie
właściwości przy pomocy refleksji, a jeśli i to rozwiązanie okaże się niesatys-
fakcjonujące, programista może rozszerzyć mechanizmy VSM poprzez do-
starczenie pośrednika, który umożliwi współpracę Visual State Managera z
dowolnym zestawem niestandardowych komponentów.
JAK TO DZIAŁA?
Spróbujmy napisać prosty przykład – standardowe operacje na liście elemen-
tów: dodawanie, edytowanie i usuwanie. Zaczniemy od zdefiniowania logiki
zależności pomiędzy kontrolkami i akcjami.
»» Dodać element możemy w każdym momencie;
»» Edycja dostępna jest tylko wówczas, gdy zaznaczony jest dokładnie jeden
element;
»» Usuwanie dostępne jest wtedy, gdy zaznaczony jest przynajmniej jeden
element (jeden lub więcej).
Teraz możemy zaprojektować odpowiednią formatkę.
Rysunek 1. Główne okno programu
Kolejną czynnością będzie zaimplementowanie zdefiniowanych wcześniej
zależności. Dla czytelności kodu proponuję umieścić odpowiednie pola wraz
z ustawiającą je metodą w osobnym pliku – na przykład DataEditForm.Logic.
cs. Zaczynamy od dodania skrótów do namespace’u i skrótu do typu:
Listing 1. Wymagane odniesienia do namespace'ów
using VisualStateManager;
using Action = VisualStateManager.Action;
Kolejnym krokiem będzie przygotowanie pól przechowujących odpowiednie
warunki i akcje:
Listing 2. Deklaracje warunków i akcji
public partial class DataEditForm
{
private Condition singleItemSelected;
private Condition itemsSelected;
private Action addElementAction;
private Action editElementAction;
private Action removeElementsAction;
}
Definiowanie zależności jest stosunkowo proste, ponieważ odbywa się w kon-
struktorach odpowiednich klas. W naszym przypadku możemy więc napisać:
Listing 3. Metoda inicjalizująca warunki i akcje
public void InitializeActions()
{
singleItemSelected = new Condition(false);
itemsSelected = new Condition(false);
addElementAction = new Action(DoAdd, bAdd);
editElementAction = new Action(DoEdit, singleItemSelected,
bEdit);
removeElementsAction = new Action(DoRemove, itemsSelected,
bRemove);
}
Jak wcześniej wspomniałem, konstruktory akcji są dosyć elastyczne i po-
zwalają określić metodę, która ma zostać wykonana, gdy użytkownik wywoła
akcję, warunki dla dostępności, zaznaczenia i widoczności kontrolek, a także
listę komponentów, które będą tę akcję wyzwalały.
Kontrolowanie stanów kontrolek będzie odbywało się automatycznie, ale
musimy najpierw nauczyć VSM, w jakich sytuacjach warunki się zmieniają.
Zrobimy to, implementując zdarzenie reagujące na zmianę zaznaczenia listy:
Listing 4. Zmiana wartości warunku
private void lbData_SelectedValueChanged(object sender, EventArgs e)
{
singleItemSelected.Value = lbData.SelectedItems.Count == 1;
itemsSelected.Value = lbData.SelectedItems.Count > 0;
}
Na koniec warto wspomnieć o jednej rzeczy: ponieważ kontrolki wizualne
muszą być już zainstancjonowane, gdy przygotowujemy logikę akcji, meto-
da InitializeActions powinna zostać wywołana po wywołaniu metody
InitializeComponent. Wygodnym miejscem jest więc konstruktor okna,
w którym obie metody możemy wywołać we właściwej kolejności:
Listing 5. Inicjalizowanie okna
public DataEditForm()
{
InitializeComponent();
InitializeActions();
}
I to wszystko – mechanizm jest już przygotowany do pracy. Naturalnie do za-
implementowania pozostają jeszcze same akcje – przykładowy projekt moż-
na odnaleźć w materiałach do artykułu dostępnych do ściągnięcia ze strony
magazynu Programista.
Jakie zalety ma zastosowane powyżej rozwiązanie? Po pierwsze, me-
chanizm będzie pilnował za nas, aby użytkownik nie wywołał akcji, gdy nie
będzie takiej możliwości: ogranicza to liczbę błędów mogących powstać w
trakcie pracy programu. Zauważmy też, że każda kontrolka, która jest odpo-
wiedzialna za wywołanie akcji, automatycznie będzie kontrolowana przez
mechanizm (przynajmniej dopóki nie spróbujemy ręcznie ustawiać handle-
rów zdarzeń na metody realizujące akcje, ale przed tym Visual State Manager
ani inny gotowy mechanizm nie jest już w stanie zabezpieczyć).
Drugą zaletą jest stosunkowa łatwość rozbudowy możliwości formatki.
Jeśli zmienią się okoliczności wywołania niektórych akcji, wystarczy dodać
odpowiednie warunki lub zbudować warunki kompozytowe, a wszystko
wciąż będzie działało prawidłowo. Bardziej wymagającym programistom
pozostanie oczywiście opcja implementacji interface’u ICondition w
celu wprowadzenia bardziej skomplikowanych zależności. Jeśli zaistnie-
je potrzeba dodania jeszcze jednego miejsca, z poziomu którego będzie
można wywołać akcję, sprowadzi się to tylko do dodania odpowiedniej
kontrolki do konstruktora klasy Action. Poza tym wprowadzenie do-
datkowej warstwy pomiędzy interfacem użytkownika i implementację
pozwala na bardzo łatwą wymianę tego pierwszego. Sprowadzi się to
bowiem tylko do wprowadzenia odpowiednich modyfikacji w metodzie
InitializeActions.
Trzecią zaletą jest fakt, iż podczas korzystania zVisual State Managera pro-
gramista może zdefiniować zależności rządzące formatką w bardzo naturalny
sposób. Jest to znacznie wygodniejsze od ręcznego implementowania me-
chanizmów zabezpieczających.
9/ www.programistamag.pl /
INTERFACE A IMPLEMENTACJA
WINDOWS PRESENTATION
FOUNDATION
Biblioteki Windows Presentation Foundation, w przeciwieństwie do Windows
Forms, dostarczają gotowe rozwiązanie dla zadanego problemu.
ICommand
Kluczową rolę odgrywa tu interface ICommand, który jest odpowiednikiem
akcji z zaprezentowanego wcześniej mechanizmu, i wygląda następująco:
Listing 6. Interface ICommand
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Przeznaczenia składowych ICommandmożna się łatwo domyślić: Executema
wykonać komendę, CanExecute powinno stwierdzić, czy w danym momen-
cie może ona zostać wykonana, zaś zdarzenie CanExecuteChanged infor-
muje o sytuacji, w której dostępność komendy uległa zmianie. CanExecute
jest zazwyczaj wywoływana w reakcji na zdarzenie CanExecuteChanged.
Najprostszym sposobem wykorzystania komend jest po prostu zaimple-
mentowanie interface'u ICommand, a następnie przypisanie instancji takiej
klasy do własności Command kontrolek wizualnych. Przykładowa implemen-
tacja dostępna jest na Listingu 7.
Listing 7. Przykładowa implementacja ICommand
internal class SimpleCommand : ICommand
{
private bool canExecute;
private Action
10 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
RoutedCommand
Na koniec przyjrzymy się nieco bardziej zaawansowanym technikom progra-
mowania z pomocą komend w WPFie. Biblioteki te dostarczają bowiem kilka
klas i mechanizmów, które pozwalają znacznie rozszerzyć funkcjonalność
komend.
Koncepcja komend wWPFie składa się z czterech kluczowych elementów:
»» Komendy (Command), reprezentującej akcję, która ma zostać wykonana;
»» Źródła komendy (Command source) – obiektu, który wywołuje komendę;
»» Celu komendy (Command target) – obiektu, na którym jest wykonywana
komenda
»» Powiązania komendy (Command binding), który wiąże komendę z jej lo-
giką w aplikacji.
Aby zrozumieć, w jaki sposób powyższe elementy ze sobą współpracują,
prześledźmy, jaką ścieżkę musimy przejść, by komenda została wykonana.
Wszystko zaczyna się od źródła komendy (Command Source), które
wyzwala komendę. Źródłem takim może być na przykład element menu,
przycisk albo skrót klawiaturowy. Następnie mechanizmy WPF określają cel
komendy. Może on być zdefiniowany jawnie w deklaracji kontrolki będącej
źródłem komendy; jeśli tak nie jest, to jako cel komendy przyjmowany jest
obiekt, który w danym momencie ma fokus.
Gdy cel komendy zostanie określony, do głosu dochodzi routing, który
stara się odnaleźć kontrolkę posiadającą odpowiednie powiązanie (com-
mand binding) pozwalające na wykonanie wybranej komendy. Routing dzia-
ła identycznie, jak w przypadku routed events, to jest najpierw wykonywany
jest tunneling, a później bubbling w poszukiwaniu handlerów zdarzeń – od-
powiednio PreviewExecute lub Execute.
Po odnalezieniu odpowiedniego handlera jest on wykonywany i na tym
cała operacja się kończy.
Choć proces ten może wydawać się skomplikowany, w rzeczywistości
umożliwia zaimplementowanie komend przy pomocy mniejszej ilości kodu
źródłowego, a ponadto pozwala na bardziej precyzyjne rozplanowanie logiki
w zależności od źródła i celu komendy.
Implementacja
Na wstępie zauważmy, że w przypadku klas udostępnianych przez WPF
komenda sama w sobie nie wykonuje żadnego kodu związanego z logiką
aplikacji – jej zadaniem jest tylko wyzwolenie mechanizmu mającego za
zadanie odnalezienie celu komendy i wykonania routingu. Oznacza to, że
komendy same w sobie są całkowicie odizolowane od aplikacji, dlatego też
w bibliotekach WPF odnajdziemy pewien zestaw standardowych komend, z
których możemy od razu skorzystać, na przykład otwórz, zapisz, kopiuj, wklej
czy odtwórz. Odpowiednie instancje odnajdziemy w namespace System.
Windows.Input (klasy MediaCommands, ApplicationCommands, Navi-
gationCommands i ComponentCommands) oraz System.Windows.Doc-
uments (EditingCommands). Jeśli wśród nich nie odnajdziemy potrzebnej
nam komendy, możemy również skorzystać z klasy RoutedCommand, co ma
miejsce w poniższym przykładzie.
Zaczynamy od zdefiniowania komend. W naszym (uproszczonym) przy-
padku umieścimy je w zasobach okna; jeśli programujemy zgodnie z zasada-
mi modelu MVVM, wówczas bardziej właściwym miejscem będzie viewmodel.
Listing 12. Osadzenie zdarzeń w zasobach oknaTeraz możemy określić, które kontrolki mają wyzwalać zdarzenia:
Listing 13. Powiązanie elementów interface'u z komendamiKolejnym krokiem jest przygotowanie command bindings, które pozwolą na
powiązanie komend z logiką aplikacji. Przykładowy program jest bardzo pro-
sty, więc powiązania te można umieścić praktycznie w dowolnym miejscu; w
naszym przypadku podwiesimy je również pod klasę okna:
Listing 14. Definiowanie akcji i warunków wykonania dla komendJakwidać,prawiecałykododpowiedzialnyzalogikękomendudałosięupchnąć
w XAMLu. W code-behind musimy umieścić tylko to, czego w XAMLu zrobić się
już nie dało, czyli obsłużyć zdarzenia CanExecute oraz Executed. I voila!
Listing 15. Reakcja na zmianę warunków
private void editCommandBinding_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = lbData.SelectedItems.Count == 1;
}
private void removeCommandBinding_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = lbData.SelectedItems.Count > 0;
}
Zaprezentowane mechanizmy są obecne w większości frameworków. Zdecy-
dowałem się opisać tylko dwa z nich, bo w większości przypadków rządzące
nimi reguły są podobne i łatwo jest przestawić się z jednego modelu na inny.
Warto jednak z nich korzystać, wprowadzając dodatkową warstwę pomiędzy
interfacem użytkownika i logikę programu – w znacznym stopniu ułatwia to
potem jego rozwój i konserwację.
Kody źródłowe wszystkich trzech aplikacji są dostępne do ściągnięcia ze
strony internetowej magazynu Programista w zakładce„download”.
Wojciech Sura wojciechsura@gmail.com
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
11/ www.programistamag.pl /
MATERIAŁ PROMOCYJNY
J
est to ogólnopolski konkurs programistyczny. Dla każdego lubiącego
intelektualny pot. Pierwsze zagadnienie zostało już opublikowane 25
lutego. Przez kolejne tygodnie uczestnicy zmagać się będą z zadaniami
związanymi z szeroko pojętym tworzeniem oprogramowania: algorytmiką,
architekturą, refaktoryzacją itp. MasterCodere’m zostanie ten, który suma-
rycznie uzyska największą ilość punktów.
W tym momencie na stronie www.mastercoder.pl zarejestrowało się już
prawie 200 osób. Rejestracja jest wciąż otwarta, więc liczba uczestników nie-
ustannie rośnie. Spośród nich 5 najlepszych osób zostanie zaproszonych na
finał w oddziale organizatora konkursu.
Dlakogojestprzeznaczonykonkurs?Iczegomogą
spodziewaćsięuczestnicywkolejnychzadaniach?
Dla wszystkich, którzy programując, dobrze się bawią. Fakt, czy pracujesz jako
programista zawodowo, nie ma tutaj znaczenia. To jest konkurs dla pasjona-
tów, zarówno profesjonalistów, jak i studentów oraz amatorów – twierdzi jeden
z autorów pytań, Przemysław Różycki – Solution Architect w firmie Cybercom
Poland – i dodaje, że uczestnik MasterCodera powinien być gotowy na wszystko!
A najbardziej na to, iż oceniając, mamy na uwadze istotny fakt – oprogramowania
używająludzie.Inawetjeśliprogramykonkursowemająograniczonąużyteczność,
to będą oceniane tak, jakby były częścią realnych projektów. Co oznacza nie tylko
wysoką jakość, ale również czytelność kodu i łatwość rozbudowy. Jak wiadomo,
prawdziwy klient z krwi i kości często zmienia zdanie, oczekując naszej reaktywno-
ści, więc niewykluczone, że nam również w którymś zadaniu się odmieni.
Obecniejestwielekonkursówprogramistycznych,
które są organizowane dla ludzi z branży IT
Nie twierdzimy, że nasz konkurs jest wyjątkowy. Możemy jednak zapewnić, że nasz
konkurs jest przekrojowy. Nie dotyczy jedynie algorytmiki czy danego języka progra-
mowania. U nas sprawdzić może się każdy, niezależnie od ulubionej technologii, w
wielu aspektach związanych z tworzeniem oprogramowania: algorytmy, refaktory-
zacja,bazydanychitp.Dodatkowąwartościąjestbezpośrednikontaktzorganizato-
rami, którzy liczą na kreatywność uczestników i wierzą, że uczestnicy zaproponują
nietypoweiinnowacyjnerozwiązaniaproblemów,anietylkote,którepasujądoszta-
mywyznaczonejprzezautorów – podkreśla inny pasjonat i współorganizator kon-
kursu – JarosławTrepczyński, Senior Software Consultant w Cybercom Poland.
Zachęcamy więc wszystkich do zapoznania się z większą ilością szczegó-
łów na stronie konkursu: www.mastercodera.pl. Jesteśmy dopiero po pierw-
szym zadaniu, więc wciąż liczymy jeszcze na zgłoszenia pasjonatów progra-
mowania. Głęboko wierzymy, że konkurs MasterCoder na stałe wpisze się
w kalendarz kluczowych wydarzeń w branży IT.
MasterCoder by Cybercom Poland
Olimpiada w Soczi zakończona? Czas rozpocząć
MasterCodera!
Kolejny rok bez powołania na Igrzyska Olimpijskie? Nic straconego. Mamy dla Ciebie
coś o wiele lepszego. Nie musisz skakać na nartach jak nasz złoty medalista. Nikt
nie będzie kazał biegać Ci z pękniętą stopą. Curling na lodzie może być dla Ciebie
niezrozumiały, a przekaz płynący z łyżwiarstwa figurowego totalnie niejasny! Ruszył
MasterCoder - gdzie jedyne znaczenie ma zdolność analitycznego myślenia, kreatyw-
ność w rozwiązywaniu problemów oraz wszechstronność technologiczna.
O Cybercom Poland Sp. z o.o.
Firma Cybercom została założona w 1995 roku w Szwecji. Od momentu
powstania firma dynamicznie się rozwija, zarówno poprzez wzrost orga-
niczny, jak i przejęcia firm w strategicznych miejscach na świecie. Obec-
nie Grupa Cybercom ma biura w dziewięciu krajach i zatrudnia 1 600 osób.
W Polsce Cybercom posiada dwa oddziały: w Warszawie i Łodzi, w których
jest obecnie zatrudnionych ponad 140 kompetentnych i doświadczonych
specjalistów. Główne obszary działań to telekomunikacja, przemysł, me-
dia, bankowość i finanse, handel detaliczny oraz sektor publiczny. Firma
specjalizuje się w rozwiązaniach internetowych, bezpieczeństwa, usługach
mobilnych oraz telekomunikacyjnych. Świadczy też pełen zakres usług
consultingowych i outsourcingowych, testowania oraz R&D dla dużych
i średnich firm. Globalne zaplecze i zweryfikowane narzędzia sprawiają, że
w sposób więcej niż satysfakcjonujący firma może realizować zarówno lo-
kalne projekty o zasięgu ogólnopolskim, jak i międzynarodowym.
Dowiedz się więcej o firmie na: http:www.cybercom.pl
12 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Wojciech Sura
T
ematyka, którą chcę poruszyć, dotyka zaawansowanych zagadnień
frameworka WPF – zakładam więc, że czytelnik zaznajomiony jest z
jego podstawowymi koncepcjami, takimi jak dependency properties,
bindings czy routed events. W razie potrzeby zachęcam do sięgnięcia po jeden
z poprzednich numerów Programisty, w którym tematy te zostały omówione.
KOSZTOWNE ZACHCIANKI
Jakiś czas temu zachciało mi się wprowadzić do mojego programu este-
tycznie wyglądające pole tekstowe służące do szybkiego filtrowania danych
– takie, jakie obecnie ma Visual Studio.
Rysunek 1. Estetyczne pole wyszukiwania
Napisanie własnego komponentu wizualnego (nawet od zera) to dla mnie
żadna nowość, ale mój zapał do pisania takiego pola tekstowego szybko
ostygł, gdy uświadomiłem sobie, ile pracy będzie kosztowało mnie oprogra-
mowanie całej jego funkcjonalności znanej ze współczesnegoWindowsa – od
wprowadzania tekstu, poprzez ustawianie kursora w dowolnym miejscu, za-
znaczenie (klawiaturą i myszką) oraz przenoszenie zaznaczonego tekstu, aż
do obsługi schowka i standardowego menu kontekstowego.
Próbowaliście kiedyś napisać pole tekstowe od zera? Ja tak – było to w cza-
sach liceum, gdy uparłem się zaimplementować w DOS-owym trybie graficznym
framework podobny do VCLa znanego z Delphi. Znalazłem w sobie dostatecznie
dużo samozaparcia, by oprogramować wprowadzanie znaków przy pomocy kla-
wiatury i przesuwanie kursora klawiszami strzałek, ale poddałem się już wówczas,
gdy doszło do implementacji mechanizmu pozwalającego użytkownikowi na
wpisanie do pola więcej tekstu niż mieściło się w obszarze widocznym na ekranie.
Rysunek 2. Pole tekstowe pisane od zera
Jak zapewne się domyślacie, WPF pozwala na zaprojektowanie takiego kom-
ponentu przy znacznie mniejszym nakładzie pracy. Spróbuję pokazać, w jaki
sposób można to osiągnąć.
MODYFIKOWANIE ISTNIEJĄCYCH
KONTROLEK
Windows Presentation Foundation daje użytkownikowi możliwość wprowa-
dzania zmian do istniejących kontrolek przy pomocy mechanizmu stylów.
Style w WPF działają w podobny sposób, jak w CSS w HTMLu – umożliwiają
automatyczne ustawianie wartości własnościom określonych kontrolek.
Działanie stylów WPF najłatwiej jest wyjaśnić na przykładzie, więc zapro-
jektujmyprosteoknowXAMLu,naktórymbędziemymoglieksperymentować.
Listing 1. Proste okno w XAMLuZacznijmy od czegoś prostego; spróbujmy przygotować własny styl dla etykie-
ty, przy pomocy którego zmienimy jej kolor na czerwony. Oto odpowiedni kod:
Listing 2. Styl dla etykietyPierwszym, co rzuca się w oczy, jest atrybut TargetType. WPF wymaga bo-
wiem, by jednoznacznie określić, jakiego typu obiekty będziemy chcieli mo-
dyfikować naszym stylem. Wewnątrz stylu definiujemy natomiast listę sette-
rów, z których każdy opisuje, którą własność obiektu chcemy zmienić i jaka
ma być jej nowa wartość. Styl jest gotowy, ale teraz trzeba go zaaplikować do
etykiety znajdującej się w oknie.
APLIKOWANIE STYLU
Najprostszym sposobem zaaplikowania stylu jest po prostu jego bezpośred-
nie przypisanie do kontrolki, którą chcemy zmodyfikować, i wygląda to mniej
więcej następująco:
Wstęp do WPF – część 2:
Stylowanie kontrolek w WPF
Po jakimś czasie pracy z dowolnym frameworkiem graficznym programista zaczyna
odkrywać, że standardowy zestaw kontrolek to zbyt mało, aby napisać wygodną, dyna-
miczną i estetyczną aplikację. Na ratunek przychodzą wówczas różne darmowe i płatne
internetowe repozytoria udostępniające multum dodatkowych komponentów. W przy-
padku WPF nie zawsze konieczne jest jednak korzystanie z zewnętrznych bibliotek, po-
nieważ we framework ten wbudowany jest bardzo elastyczny mechanizm pozwalający
na stylowanie kontrolek – umożliwiając tym samym łatwą zmianę ich wyglądu, a także
– w pewnym zakresie – również zachowania.
13/ www.programistamag.pl /
WSTĘP DO WPF – CZĘŚĆ 2: STYLOWANIE KONTROLEK W WPF
Listing 3. Bezpośrednie zaaplikowanie styluRozwiązanie to w praktyce pojawia się stosunkowo rzadko, ponieważ jest
mało praktyczne – po pierwsze, równie dobrze możemy przypisać wartości
do odpowiednich własności modyfikowanej kontrolki bez zastosowania sty-
lu, a po drugie przypisanie takiego stylu do wielu kontrolek skończy się ko-
piowaniem dużych ilości kodu. Znacznie lepszym rozwiązaniem jest umiesz-
czenie stylu w zasobach i odwołanie się do niego przy pomocy rozszerzeń
StaticResource lub DynamicResource.
Listing 4. Zaaplikowanie stylu osadzonego w zasobach(...)Teraz jest znacznie lepiej – kod stał się znacznie bardziej czytelny i elastyczny,
a efekt końcowy pozostał niezmieniony.
Rysunek 3. Style zaaplikowane do etykiet
ZAKRES DZIAŁANIA
WPF traktuje style znajdujące się w zasobach w różny sposób, w zależno-
ści od tego, czy jest do nich przypisany jakiś klucz (x:Key). W poprzednim
przykładzie styl został umieszczony w zasobach pod kluczem SimpleStyle,
w efekcie czego zostanie on zastosowany tylko do tych kontrolek, które jaw-
nie sobie tego zażyczą (poprzez ustawienie stylu na {StaticResource
SimpleStyle} ). Jeżeli natomiast zapiszemy w zasobach styl, nie podając
żadnego klucza, będzie on automatycznie aplikowany do wszystkich kontro-
lek, które dziedziczą z klasy określonej jako TargetType.
Aby zrozumieć dokładnie, jak działa ten mechanizm, trzeba wiedzieć, w
jaki sposób WPF poszukuje zasobów. Kiedy kontrolka próbuje dostać się do
zasobów (na przykład przez StaticResource albo DynamicResource, ale
nie tylko), WPF sprawdza najpierw, czy nie ma takiego zasobu przywiązanego
do samej kontrolki; jeśli jest, to zostanie on użyty. W przeciwnym wypadku
WPF przenosi się do kontenera, na którym położona jest kontrolka, i tam po-
szukuje danego zasobu, i tak dalej – aż osiągnie szczyt hierarchii. Jeśli i tam nie
odnajdzie zasobu, przeszukiwane są zasoby aplikacji (umieszczone w pliku
App.xaml), a potem – jeśli zaistnieje taka potrzeba, zasoby systemowe.
Mechanizm ten działa również dla stylów. Przyjrzyjmy się uważnie poniż-
szym przykładom:
Styl osadzony w zasobach aplikacji zadziała dla każdej kontrolki:
Listing 5. Styl umieszczony w zasobach aplikacji(...)Styl umieszczony w zasobach kontenera wpłynie na wszystkie elementy znaj-
dujące się wewnątrz niego:
Listing 6. Styl umieszczony w zasobach konteneraJeżeli natomiast kontrolka ma osadzony styl w swoich zasobach i zgadza się
on z jej typem, to właśnie on zostanie zaaplikowany:
Listing 7. Listing osadzony w zasobach kontrolkiZauważmy, że WPF po odnalezieniu stylu właściwego dla kontrolki przerywa
dalsze poszukiwania. Dlatego też ostatnia i przedostatnia etykieta nie jest po-
chylona (stałoby się tak, gdyby zaaplikowany został styl znajdujący się w za-
sobach aplikacji). Jeśli jednak opisany efekt jest pożądany, w deklaracji stylu
można użyć atrybutu BasedOn, wskazując styl, który chcemy zmodyfikować.
Rysunek 4. Zakres działania stylu
TEMPLATE
Wśród różnych własności kontrolek, które możemy ustawić przy pomocy
stylu, jedna zasługuje na szczególną uwagę, a jest nią własność Template.
Zawiera ona bowiem instancję klasy ControlTemplate, która definiuje wy-
gląd kontrolki. Każda standardowa kontrolka ma własną ControlTemplate
zapisaną w bibliotekachWPF, ale programista każdą z nich może zastąpić wła-
snym szablonem.
14 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Przyjrzyjmy się zatem dokładnie następującemu fragmentowi kodu:
Listing 8. Zamiana szablonu kontrolkiPo uruchomieniu programu zauważymy, że etykieta otrzymała czarną ramkę.
Spróbujmy dojść teraz, w jaki sposób wpłynęliśmy na nią stylem, by osiągnąć
zamierzony efekt.
Wygląd etykiety zmienił się oczywiście w efekcie przedefiniowania wartości
własności Template. Wstawiona tam instancja ControlTemplate pozwoliła
zdefiniować, w jaki sposób kontrolka zostanie wyświetlona w oknie. Zauważmy
namarginesie,żepodczaskorzystaniazControlTemplatemusimyokreślić,jaki
typ jest przez ten szablon opisywany. Warto również zadbać o to, byśmy przy-
padkiem podczas definiowania zawartości szablonu nie skorzystali z kontrolki,
którą właśnie stylujemy, ponieważ może się to skończyć przepełnieniem stosu,
gdy WPF będzie w nieskończoność próbował nałożyć na kontrolkę przygotowa-
ny przez nas styl. Poza wspomnianym ograniczeniem wewnątrz ControlTem-
plate możemy korzystać praktycznie ze wszystkich dostępnych kontrolekWPF.
Spośród wszystkich elementów, których możemy użyć wewnątrz szablo-
nu, jeden zasługuje na szersze zainteresowanie, a mowa o obiekcie o nazwie
ContentPresenter.
Label jest kontrolką, której zawartość definiuje się poprzez własność Con-
tent. Każda kontrolka tego typu (na przykład Button) umożliwia wstawienie
dojejwnętrzanietylkotekstu,aledowolnegoinnegoobiektu–wtymrównież
zagnieżdżonych kontrolek. Kiedy projektujemy szablon dla etykiety, musimy
również zachować tę funkcjonalność – w przeciwnym razie nie będziemy w
stanie wyświetlić jej zawartości we właściwy sposób. Z pomocą przychodzi tu
wtedy klasa ContentPresenter, której zadaniem jest wyświetlenie danych
przekazywanych poprzez własność Content. Tym sposobem nie musimy się
martwić, w jaki sposób wyświetlić ciąg znaków, a jak zagnieżdżone kontrolki:
ContentPresenter zrobi wszystko za nas.
Pojawia się tylko jeszcze jeden kłopot: musimy ustawić własność Con-
tent obiektu ContentPresenter na taką wartość, jaka w przyszłości zosta-
nie przypisana do etykiety. Aby to osiągnąć, korzystamy z rozszerzenia Tem-
plateBinding. Zapis:
Zwróćmy uwagę na specyficzny zapis settera modyfikującego własność
ControlTemplate. Ponieważ własność Template jest bardziej złożona niż
Foreground, jej wartości nie da się zapisać w prosty sposób w atrybucie.
Dlatego też konieczne jest zastosowanie alternatywnego zapisu – we-
wnątrz settera wstawiamy węzeł Setter.Value i dopiero tam definiujemy
nową wartość własności Template. Rozwiązanie takie można zastosować
dla własności każdej klasy, którą umieścimy w XAMLu.
Listing 9. Wiązanie z własnością stylowanej kontrolkiI faktycznie – od teraz kolor ramki pasuje do koloru tekstu w etykietach.
Listing 11. Ostylowane kontrolkiRysunek 5. Etykiety z własnym szablonem
CZĘŚCI STAŁE
Zaczynamy powoli mieć wystarczająco dużo narzędzi, aby zrealizować pier-
wotny pomysł z estetycznym polem tekstowym, ale wciąż pozostaje problem
standardowej funkcjonalności, którą musimy w naszej kontrolce zachować.
Okazuje się jednak, że również i ten problem da się stosunkowo łatwo roz-
wiązać. WPF określa bowiem dla każdej standardowej kontrolki pewne specjal-
ne elementy, które reprezentują specyficzne dla nich zachowania.W przypadku
pola tekstowego elementem takim jest ScrollViewer, któremu należy nadać
nazwę PART_ContentHost. W przypadku każdej kontrolki są to inne elemen-
ty, ale na szczęście wszystkie opisane są na odpowiednich stronach MSDN.
Spróbujmy teraz przygotować szablon dla własnego pola tekstowego.
Listing 12. Szablon dla pola tekstowego
15/ www.programistamag.pl /
WSTĘP DO WPF – CZĘŚĆ 2: STYLOWANIE KONTROLEK W WPF
Po wstawieniu pola tekstowego do okna zobaczymy efekt:
Rysunek 6. Pole tekstowe z prostym szablonem
Możemy teraz zmodyfikować nasz szablon tak, aby wprowadzić tekst w tle
oraz dodatkową ikonę (poniżej sama definicja szablonu):
Listing 13. Szablon dla pola tekstowego z filtremFilterRysunek 7. Prawie gotowe!
No proszę – jesteśmy bardzo blisko osiągnięcia zamierzonego celu, choć
widać, że konieczne będzie wprowadzenie jeszcze pewnych zmian, bo choć
pole tekstowe wygląda tak, jak chcieliśmy, to nie zachowuje się jeszcze zgod-
nie z naszymi oczekiwaniami: podpowiedź w tle jest widoczna cały czas, a
powinna przecież znikać w momencie, gdy zaczniemy wprowadzać tekst.
TRIGGERY
Z opisu brakującej funkcjonalności wynika jasno, że będziemy musieli do na-
szej kontrolki wprowadzić trochę logiki. Jednak mimo iż wydaje się, że bez
fragmentu kodu w C# nie uda nam się już obejść, WPF i w tym miejscu staje
na wysokości zadania.
ControlTemplate udostępnia bowiem mechanizm triggerów pozwalają-
cych na wprowadzenie do niej pewnej szczątkowej logiki. Każdy trigger składa
się z dwóch kluczowych elementów: warunku oraz zbioru setterów. Jego dzia-
łanie jest bardzo proste – jeżeli warunek jest spełniony, settery zostają zaapliko-
wane, w przeciwnym zaś wypadku – nie (oznacza to, że jeśli warunek w pew-
nym momencie jest spełniony, a potem przestanie być, to wszystkie własności
modyfikowane triggerem wrócą do swoich poprzednich wartości).
W naszym przypadku będziemy testować własność Text. Jeśli będzie
ona pusta, tekst podpowiedzi będzie wyświetlony, a w przeciwnym wypadku
– wygaszony.
Aby opisać tę zależność, będziemy potrzebowali najpierw dostępu do na-
mespace'a System, w którym znajduje się klasa string:
Listing 14. Wprowadzenie dodatkowego namespace'aAby trigger zadziałał prawidłowo, musimy wprowadzić też kilka zmian do szablo-
nu. Pierwszą z nich będzie nazwanie komponentu TextBlock wyświetlającego
podpowiedź, byśmy potem mogli się do niego odwołać z poziomu triggera. Po-
nadto domyślnie ustawimy jego widoczność na Hidden, by później przy pomocy
triggera w odpowiednich okolicznościach ustawić ją z powrotem na Visible:
Listing 15. Ukrycie tekstu „Filter”FilterTeraz wprowadzamy do ControlTemplate dodatkową sekcję:
Listing 16. Triggery szablonu(...)I voila! Cel został osiągnięty!
Rysunek 8. Działa!
CO DALEJ?
WPF daje bardzo dużo możliwości stylowania kontrolek. W niniejszym ar-
tykule opisałem, w jaki sposób można przygotować szablony dla prostych
kontrolek typu Label czy TextBox. W następnej części postaram się zaś
opowiedzieć o zaawansowanych technikach stylowania pozwalających przy-
gotowywać style dla kontrolek wyświetlających dane (na przykład listy).
Wojciech Sura wojciechsura@gmail.com
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
16 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Karol Rogowski
KRÓTKIE SPOJRZENIE WSTECZ
Zanim przejdziemy do właściwej części artykułu, chciałbym wykonać gest do-
brej woli w stronę osób, które nie miały okazji zapoznać się z numerem 10/2013
magazynu Programista. Zawiera on pierwszą część artykułu „ASP.NET SignalR
– czyli aplikacje czasu bardzo rzeczywistego”. Można było w niej znaleźć zaim-
plementowany serwer, z którego będzie korzystał tworzony dzisiaj klient. Nie
będę oczywiście tego tu dokładnie opisywał. Niemniej jednak komentarze pre-
zentowanego kodu powinny wystarczyć do zrozumienia rozwiązania.
Listing 1. Serwer do rozwiązania, nad którym będziemy pracować
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
/// /// Klasa Hub-a naszego chat-u
/// [HubName("ExampleChat")]
public class ChatHub : Hub
{
/// /// Metoda pozwalająca na wysłanie wiadoności do wszystkich
użytkowników
/// /// Nazwa użytkownika/// Treść wiadomościpublic void SendMessage(string name, string message)
{
var msg = string.Format("{0}: {1}", name, message);
Clients.All.newMessage(msg);
}
/// /// Metoda pozwalająca na połączenie do jakiegoś pokoju
/// /// Nazwa użytkownika/// Nazwa pokoju, do którego chcemy się
podłączyćpublic void JoinRoom(string name, string room)
{
Groups.Add(Context.ConnectionId, room);
var msg = string.Format("{0} joined room: {1}", name, room);
Clients.Group(room).newMessage(msg);
}
/// /// Metoda pozwalająca na wysłanie wiadoności do użytkowników w
danym pokoju
/// /// Nazwa użytkownika/// Nazwa pokoju/// Treść wiadomościpublic void SendMessageToRoom(string name, string room, string
message)
{
var msg = string.Format("{0} to room {2}: {1}", name, message,
room);
Clients.Group(room).newMessage(msg);
}
/// /// Nadpisane zdarzenie na łączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisujepublic override Task OnConnected()
{
this.SendMonitoringData("Połączono", Context.ConnectionId);
return base.OnConnected();
}
/// /// Nadpisane zdarzenie na rozłączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisujepublic override Task OnDisconnected()
{
this.SendMonitoringData("Rozłączono", Context.ConnectionId);
return base.OnDisconnected();
}
/// /// Nadpisane zdarzenie na ponowne połączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisujepublic override Task OnReconnected()
{
this.SendMonitoringData("Połączono ponownie", Context.
ConnectionId);
return base.OnReconnected();
}
/// /// Metoda pozwalająca na informowanie użytkowników o
zdarzeniach występujących na Hub-ie
/// /// Rodzaj zdarzenia/// Identyfikator połączenia, które
wywołało to zdarzenieprivate void SendMonitoringData(string eventType, string
connection)
{
Clients.All.newEvent(eventType, connection);
}
}
CEL NA DZISIAJ
Na początek właściwego artykułu chciałbym powiedzieć, jaki jest nasz cel.
Rezultatem, do którego dążymy w tym momencie, jest stworzenie czatu. Ma
on być oczywiście oparty o technologię SignalR i ma działać w czasie rzeczy-
wistym. Efektem, jaki na koniec będziemy chcieli uzyskać, będzie możliwość
odbycia rozmowy między dwiema przeglądarkami.
BIERZMY SIĘ DO PRACY
– SZKIELET STRONY
Jako że bardzo mocno wierzę w to, że najlepiej jest uczyć się na przykładach,
to omawianie tworzonego rozwiązania zaczniemy od przedstawienia szkie-
letu strony. Nie będzie tu na razie żadnego prawdziwego programowania.
Będą jedynie poukładane elementy, na których będziemy potem pracować.
Zobaczmy więc, jak wygląda to od strony kodu
ASP.NET SignalR – czyli aplikacje
czasu bardzo rzeczywistego. Część 2
Artykuł ten jest drugą, i zarazem ostatnią, częścią omawiania rozwiązania, jakim
jest SignalR. W pierwszej części przeprowadziliśmy wprowadzenie do tej technolo-
gii, jak również stworzyliśmy własny Hub, który będzie pełnił rolę swoistego serwe-
ra. W tym artykule napiszemy klienta, który to będzie pozwalał nam na prowadze-
nie rozmowy. Oczywiście w czasie rzeczywistym.
17/ www.programistamag.pl /
ASP.NET SIGNALR
Listing 2. Szkielet strony internetowej, na której będziemy pracować
title>Example Chat
Chat Programisty
Użytkownik:
Pokój:
Wiadomość:
Wiadomości
Informacje
Na podstawie przedstawionego powyżej kodu możemy, wydaje mi się, z dość
dobrą skutecznością odgadnąć, jakie funkcjonalności ma oferować nasze
rozwiązanie. Po pierwsze, będziemy chcieli mieć możliwość podania nazwy,
pod jaką chcemy występować na naszym internetowych czacie. Po drugie,
chcemy mieć oczywiście możliwość porozmawiania z kimś prywatnie. Bę-
dzie to zrealizowane na zasadzie pokoi. Jeżeli jakaś grupa użytkowników jest
podłączona do tego samego pokoju, to może odbierać informacje przesyłane
tylko do grupy osób z nim związanej. Następnie znajdziemy pole tekstowe
pozwalające na wpisanie w nie wiadomości, jaką chcemy wysłać. Kolejno
widzimy dwa przyciski. Pierwszy z nich pozwala na wysłanie wiadomości do
wszystkich połączonych użytkowników. Natomiast drugi tylko do tych znaj-
dujących się w konkretnym pokoju. Na końcu mamy dwa div-y, w których to
umieszczać będziemy informacje otrzymywane przez użytkownika. O tym,
dlaczego widzimy dwa rodzaje informacji, powiemy w dalszej części artykułu.
NIEZBĘDNE PLIKI JAVASCRIPT
JavaScript odgrywa coraz większą rolę w tworzeniu nowoczesnych rozwiązań.
Również w naszym przypadku tworzone rozwiązanie będzie opierać się o ten
język. Wiele razy zachwalałem narzędzie, jakim jest Nuget. Jak zapewne każdy
wie, służy on do pobierania gotowych paczek kodu, które są następnie umiesz-
czane w odpowiednich miejscach naszego rozwiązania. Również w przypadku
tworzonegoterazrozwiązaniaposłużymysiętymnarzędziem.Paczką,którąbę-
dziemy chcieli zainstalować, będzie Microsoft.AspNet.SignalR.JS. Jest to zestaw
plików pozwalających nam na korzystanie z technologii SignalR przy użyciu
języka JavaScript. Okno do instalacji paczki przedstawione jest na Rysunku 1.
Rysunek 1. Okno do instalacji paczki Microsoft.AspNet.SignalR.JS
Rysunek 1 nie wymaga chyba jakichś większych wyjaśnień. Tak samo jak,
mam nadzieję, ten, który przedstawię za chwilę. Mianowicie, do rozpoczęcia
faktycznych prac implementacyjnych brakuje nam jeszcze jednego elemen-
tu. Chodzi tu o serwer kliencki, który będzie obsługiwał metody zawarte w
stworzonym uprzednio Hub-ie. Paczka, o której mówię, nazywa się Microsoft
ASP.NET SignalR Utilities.
Rysunek 2. Instalacja paczki Microsoft ASP.NET SignalR Utilities
W tym momencie mamy do czynienia z bardzo ciekawą rzeczą. Chodzi mi o to,
że zainstalowana paczka nie pojawiła się bezpośrednio w naszej solucji. Wy-
nika to z faktu, że nie jest ona tam w ogóle potrzebna. Z tej paczki korzystam
bowiem z poziomu wiersza poleceń. Dlatego właśnie idźmy bezpośrednio do
miejsca, w którym znajduje się nasz projekt, i zobaczmy, czy folder z paczką
został odpowiednio do niego dodany.
Rysunek 3. Potwierdzenie, że nowa paczka została poprawnie dodana do
projektu
Skoro paczka jest na miejscu, możemy przejść do uruchomienia generowania
serwera klienckiego. Po pierwsze, musimy mieć odpaloną aplikację, na którą
działa prezentowany Hub. Musi ona być uruchomiona podczas generowania
pliku serwera.
Jeżeli aplikacja już chodzi, to należy przejść do konsoli poleceń. Prezento-
wany poniżej kod z konsoli jest oczywiście unikalny dla mojego środowiska.
Każdy musi po prostu odpowiednio dostosować adresy, tak by pasowały do
jego rozwiązania.
Rysunek 4. Generowanie pliki serwera klienckiego
18 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Po uruchomieniu powyższego polecenia, w miejscu, w którym znajduje
się narzędzie, powinniśmy odnaleźć plik server.js. Musi zostać on dodany do
naszego rozwiązania. W moim przypadku trafił on do folderu Script. Nie jest
to oczywiście żadne wymaganie, tylko moja preferencja co do organizacji pli-
ków. Zajrzyjmy więc do środka naszego świeżutkiego serwera.
Listing 3. Zawartość pliku Server.js
/*!
* ASP.NET SignalR JavaScript Library v1.0.0
* http://signalr.net/
*
* Copyright Microsoft Open Technologies, Inc. All rights
reserved.
* Licensed under the Apache 2.0
* https://github.com/SignalR/SignalR/blob/master/LICENSE.md
*
*/
/// /// (function ($, window) {
/// "use strict";
if (typeof ($.signalR) !== "function") {
throw new Error("SignalR: SignalR is not loaded. Please ensure
jquery.signalR-x.js is referenced before ~/signalr/hubs.");
}
var signalR = $.signalR;
function makeProxyCallback(hub, callback) {
return function () {
// Call the client hub method
callback.apply(hub, $.makeArray(arguments));
};
}
function registerHubProxies(instance, shouldSubscribe) {
var key, hub, memberKey, memberValue, subscriptionMethod;
for (key in instance) {
if (instance.hasOwnProperty(key)) {
hub = instance[key];
if (!(hub.hubName)) {
// Not a client hub
continue;
}
if (shouldSubscribe) {
// We want to subscribe to the hub events
subscriptionMethod = hub.on;
}
else {
// We want to unsubscribe from the hub events
subscriptionMethod = hub.off;
}
// Loop through all members on the hub and find client hub
functions to subscribe/unsubscribe
for (memberKey in hub.client) {
if (hub.client.hasOwnProperty(memberKey)) {
memberValue = hub.client[memberKey];
if (!$.isFunction(memberValue)) {
// Not a client hub function
continue;
}
subscriptionMethod.call(hub, memberKey,
makeProxyCallback(hub, memberValue));
}
}
}
}
}
$.hubConnection.prototype.createHubProxies = function () {
var proxies = {};
this.starting(function () {
// Register the hub proxies as subscribed
// (instance, shouldSubscribe)
registerHubProxies(proxies, true);
this._registerSubscribedHubs();
}).disconnected(function () {
// Unsubscribe all hub proxies when we "disconnect". This
is to ensure that we do not re-add functional call backs.
// (instance, shouldSubscribe)
registerHubProxies(proxies, false);
});
proxies.ExampleChat = this.createHubProxy('ExampleChat');
proxies.ExampleChat.client = { };
proxies.ExampleChat.server = {
joinRoom: function (name, room) {
return proxies.ExampleChat.invoke.
apply(proxies.ExampleChat, $.merge(["JoinRoom"],
$.makeArray(arguments)));
},
sendMessage: function (name, message) {
return proxies.ExampleChat.invoke.apply(proxies.
ExampleChat, $.merge(["SendMessage"],
$.makeArray(arguments)));
},
sendMessageToRoom: function (name, room, message) {
return proxies.ExampleChat.invoke.apply(proxies.
ExampleChat, $.merge(["SendMessageToRoom"],
$.makeArray(arguments)));
}
};
return proxies;
};
signalR.hub = $.hubConnection("/signalr", { useDefaultPath:
false });
$.extend(signalR, signalR.hub.createHubProxies());
}(window.jQuery, window));
Powyżej widzimy serwer kliencki, który został właśnie wygenerowany. Pole-
cam dokładne zapoznanie się z kodem. My nie będziemy tu omawiać całości,
a jedynie elementy, które uważam za najbardziej istotne. Będą to oczywi-
ście metody, które pochodzą z naszego serwerowego Hub-a. Zauważmy, że
wszystkie trzy z napisanych przez nas metod zostały przypisane do właściwo-
ści proxies.ExampleChat.server. Zauważmy też, że każda z tych metod
ostatecznie tak naprawdę wysyła wiadomość z odpowiednimi parametrami.
Co do reszty kodu, to naprawdę polecam zapoznanie się z nim w celu zaob-
serwowania dobrych praktyk tworzenia kodu.
POSKŁADAJMY WSZYSTKO RAZEM
Wszystkie potrzebne nam elementy mamy już przygotowane. To, co nam te-
raz pozostaje, to złożyć je w jedną działającą całość. Tak jak już wspomniałem,
naszym celem będzie utworzenie chatu umożliwiającego komunikację mię-
dzy przeglądarkami. Aby tego dokonać, musimy przygotować odpowiedni
skrypt kliencki, który obsłuży wymagane funkcjonalności.
Listing 4. Część skryptowa strony chatu
Example ChatPowyżej przedstawiono cały kod kliencki, wymagany do prawidłowego dzia-
łania naszej aplikacji. Na pierwszym miejscu mamy oczywiście odniesienie do
plików skryptowych spoza strony. Pierwsze dwa nie wymagają chyba żadne-
go wyjaśnienia. Co do trzeciego, to stanowi on odniesienie do skryptu, który,
tak jak pokazywaliśmy, zostaje wygenerowany dopiero w momencie urucho-
mienia aplikacji serwerowej. Ostatnim skryptem, do którego się odnosimy,
jest omówiony przed chwilą server.js.
Co do właściwego napisanego przez nas kodu, to pierwszym większym
elementem jest funkcja uruchamiana, gdy strona jest gotowa. Po pierwsze,
pobiera ona obiekt chatu do naszego obiektu. Następnie mamy podczepie-
nie się do zdarzeń oferowanych nam przez klienta. Są to zdarzenia nadejścia
nowej wiadomości oraz zdarzenie nadejścia informacji o nowym zdarzeniu w
naszym chat-cie. Kolejno mamy klasyczną obsługę kliknięć w elementy, a na
koniec wystartowanie działania naszego klienta.
Skupmy się teraz na funkcjach obsługujących sygnały przychodzące do
aplikacji. Ich nazwy to onNewMessage oraz onNewEvent. W obu przypad-
kach wzór zachowania wygląda dokładnie tak samo. Na początek przychodzi
do nas sygnał z jakąś informacją. Jak widzimy, patrząc na dwie wymienione
przed chwilą metody, informacja ta zostaje przekazana jako parametr. Na-
stępnie mamy obsługę sygnału. W tym właśnie momencie, jeżeli byłaby to
bardziej skomplikowana aplikacja, powinniśmy dokonywać wszelkich opera-
cji związanych z tym sygnałem. W naszym przypadku będzie to jedynie wy-
świetlenie informacji użytkownikowi.
Kolejnymi elementami są funkcje, w których to my wysyłamy sygnały do
serwera. Jak widać, jest to po prostu wywołanie przez nas którejś z przygoto-
wanych metod.
PORA TROCHĘ POGADAĆ
Nasz chat jest już skończony. Prawda, że napisanie go było bardzo proste?
Dosłownie za chwilę uruchomimy go i odbędziemy krótką rozmowę między
dwoma uruchomionymi przeglądarkami. Zanim jednak zaczniemy, to proszę
pamiętać, że aby strona, na której faktycznie prowadzimy rozmowę, zadziała-
ła, cała tworzona aplikacja musi być uruchomiona. Ja jeszcze dodatkowo, na
jednym z okien, będę miał uruchomione narzędzie do podglądu ruchu siecio-
wego. Uruchamiamy aplikację i próbujemy odbyć rozmowę między jej oknami.
reklama
Wybrane prelekcje:
Czy wiesz, że każdy z nas może
zbudować całkowicie niezależną
sieć bezprzewodową? O tym jak
przy pomocy domowych routerów WiFi buduje
się sieć meshową opowie Aaron Kaplan,
założyciel austriackiej społeczności
FunkFeuer.
Jeśli chciałbyś dowiedzieć się jak
usprawnić i uprzyjemnić pracę
w zespole, to metodologię SCRUM
przybliży ci Kate Terlecka - jedna z najbardziej
znanych twarzy polskiego Agile.
Jak projektować dobre wrażenie
użytkowników? Na to pytanie
odpowie Mikołaj Pastuszko, który
alergicznie reaguje na nieintuicyjne interfejsy
i nieprzemyślane architektury informacji.
Czy zastanawiałeś się kiedyś jak
Open Source zmieniło świat?
O tym, że warto dołączyć do tej
społeczności będzie mówił Kamil Gałuszka,
który mocno angażuje się w działania Open
Source (m.in. Hackerspace Silesia).
DWO to jedna z największych bezpłatnych
konferencji Open Source w Europie. Uczniowie
szkół średnich, studenci, ludzie z branży IT oraz
wszyscy, dla których wolność i społeczność
open source są ważne, spotykają się już po
raz siódmy aby uczestniczyć w 25 prelekcjach
i 8 warsztatach.
Jeśli chcesz stać się częścią jednej
z największych społeczności w kraju i poznać
kilkuset pasjonatów rozwiązań open source
dołącz do nas już teraz.
Wybrane warsztaty:
Czy można nauczyć dziecko
programowania? Czy każdy z nas
może uwolnić pingwina? W świat
Lego Mindstorms wprowadzi nas Agnieszka
Pawlicka, która wspólnie z uczestnikami
zbuduje i zaprogramuje Robo-pingwina.
Czy zastanawiałeś się kiedyś jak
to jest uczestniczyć w globalnym
projekcie open source? Jeśli tak to
Paweł Marynowski pokaże ci jak korzystając
z OpenStreetMap narysować swoją własną
okolicę w największej otwartej mapie świata.
Dni Wolnego
Oprogramowania ‘14
28 - 30 Marca 2014,
Bielsko-Biała
dwo.mikstura.it
wstęp bezpłatny
(wystarczy się zarejestrować)
20 / 2 . 2014 . (21) /
BIBLIOTEKI I NARZĘDZIA
Rysunek 5. Efekt rozmowy między dwoma prezentowanymi oknami
Na Rysunku 5 przedstawiłem efekt - krótkie rozmowy między dwoma okna-
mi. Jak widać, wykorzystano w nich zarówno możliwość odzywania się do
wszystkich, jak i wyłącznie do pokojów, do których jesteśmy podłączeni.
Na głównym widoku widzimy oczywiście informacje na temat prowadzo-
nej rozmowy. Poniżej mamy dodatkowo informacje o tym, co działo się jakby
„w tle”na serwerze. Są to informacje dotyczące tego, że ktoś się podłączał czy
też rozłączał. Efektu, który jest widoczny na pierwszy rzut oka, nie będziemy
dalej omawiać, a skupimy się bardziej na tym, co nie jest widoczne na pierw-
szy rzut oka, czyli na ruchu sieciowym i tym, co tam właściwie tak naprawdę
się działo.
W moim przypadku monitorowanie ruchu sieciowego było włączone
w przeglądarce, w której pisaliśmy, jako użytkownik Adam. Rzućmy na po-
czątku okiem na ogólny obraz tego, co się działo.
Rysunek 6. Ogólny obraz ruchu sieciowego
Jak widać, zarejestrowany ruch sieciowy jest bardzo skromny. Ja ze swojej
strony chciałbym zwrócić uwagę na dwa elementy. Pierwszym z nich będzie
moment, w którym nasza aplikacja„negocjuje”z serwerem.
Rysunek 7. „Negocjacja” aplikacji
Pojęcie„negocjacji”, jeżeli mówimy o programowaniu, występuje dość często.
Najczęściej, tak jak również w tym przypadku, definiuje ono sposób połącze-
nia między dwoma elementami. Widzimy, że mamy ustalony między innymi
identyfikator połączenia czy też token, który będzie w trakcie działania uży-
wany. Następnie widzimy pozostałe parametry charakteryzujące nawiązywa-
ne właśnie połączenie.
Drugim krokiem, na który chciałbym zwrócić uwagę, jest moment faktycz-
nego nawiązania połączenia.
Rysunek 8. Podłączenie się aplikacji
Rysunek 8 przedstawia moment, w którym aplikacja faktycznie nawiązuje
połączenie. Warto jest w tym momencie zwrócić uwagę na to, na jaki adres
wysyłane jest nasze żądanie. Jest on odpowiadający temu, co zostało zaak-
ceptowane w procesie negocjacji.
PODSUMOWANIE
W powyższym artykule przedstawiłem, jak przy pomocy technologii SignalR
stworzyć własny prosty chat. Uważam, że jak na otrzymany efekt, to ilość wło-
żonej pracy, jak i skomplikowanie projektu były bardzo niskie. Warto wiedzieć,
że omawiana technologia nie ogranicza się jedynie do rozwiązań webowych.
Możemy dzięki niej przesyłać sygnały między aplikacjami niemal każdego typu.
Pomimo tego, że prezentowany przykład był relatywnie prosty, zachęcam do
głębszegozapoznaniasięzprezentowanątechnologią.Możeonabowiemsłużyć
do tworzenia rozwiązań o wiele większych i o wiele bardziej skomplikowanych.
W przypadku jakichkolwiek uwag, pytań lub propozycji pomysłów na ko-
lejne artykuły proszę pisać na karol.rogowski@gmail.com lub szukać mnie na
https://twitter.com/KarolRogowski.
Karol Rogowski karol.rogowski@gmail.com
Absolwent Informatyki Politechniki Białostockiej. Microsoft Certificated Professional Deve-
loper .NET Web Developer. Obecnie pracuje jako Development Lead w firmie DevCore.Net.
Autor wielu treści na portalach technologicznych, głównie związanych z HTML 5 i JavaScript.
W wolnym czasie pokerzysta amator
22 / 2 . 2014 . (21) /
JĘZYKI PROGRAMOWANIA
Tomasz Nurkiewicz
J
ava 8 wprowadza najwięcej nowości od czasu piątej edycji wydanej bli-
sko 10 lat temu. W tym artykule przyjrzymy się najciekawszym i najbar-
dziej rewolucyjnym zmianom. Java z nudnego i rozwlekłego języka ma
szansę ponownie stać się popularnym, wygodnym i ciekawym narzędziem
dla nowych aplikacji. Java nadal ustępuje nowoczesnym językom na JVM, nie-
mniej jednak znacząco skraca ten dystans.
DOMYŚLNE METODY W INTERFEJSACH
Do czasu Javy 8 istniał jasny podział na interfejsy - jedynie deklarujące meto-
dy bez żadnej implementacji - oraz klasy abstrakcyjne. Aby zachować zgod-
ność wsteczną, Oracle postanowiło wprowadzić tzw. metody domyślne w
interfejsach. Teraz możemy dołączyć do dowolnej metody interfejsu także
implementację, którą klasa implementująca dany interfejs może, ale nie musi
przesłonić (ang. override):
Listing 1. Przykład metod domyślnych w interfejsach
public interface Encrypter {
byte[] encode(byte[] bytes);
default byte[] encodeStr(
String s, Charset charset) {
return this.encode(s.getBytes(charset));
}
default byte[] encodeChars(
char[] chars, Charset charset) {
final String s = String.valueOf(chars);
return this.encodeStr(s, charset);
}
}
W powyższym przykładzie interfejs deklaruje trzy metody encode*(), ale
dwie z nich posiadają implementacje opatrzone modyfikatorem default
(znanym z poprzednich wersji Javy w innym kontekście). Klasa implementująca
taki interfejs musi zaimplementować tylko jedną metodę, a nie wszystkie trzy:
Listing 2. Klasa implementująca interfejs z metodami domyślnymi
public class RotEncrypter implements Encrypter {
@Override
public byte[] encode(byte[] b) {
final byte[] result = new byte[b.length];
for (int i = 0; i < b.length; ++i) {
result[i] = (byte) (b[i] + 13);
}
return result;
}
}
Java 8 – najbardziej rewolucyjna
wersja w historii
Częstotliwość wydawania nowych wersji języka Java pozostawia wiele do życzenia.
JDK 6 oraz 7 były też pewnym rozczarowaniem ze względu na małą innowacyjność
i opóźnienia. Java 8, spodziewana już w połowie marca, ma szansę radykalnie od-
mienić dotychczasowe postrzeganie tej dość leciwej platformy. Wyrażenia lambda,
współbieżne kolekcje czy zupełnie nowe API do obsługi czasu przybliżają ten jeden
z najpopularniejszych języków programowania do konkurencji.
Klasa RotEncrypter implementuje tylko jedną metodę, a pozostałe są
zaimplementowane na jej bazie. Oczywiście nadal istnieje możliwość prze-
słonięcia pozostałych metod interfejsu – ale nie ma już takiego obowiązku.
W poprzednich wersjach Javy z reguły w takim przypadku tworzyliśmy zwy-
kły interfejs Encrypter oraz pomocniczą klasę (np. abstract class Ab-
stractEncrypter implements Encrypter) implementującą wszystkie
opcjonalne metody. Trudno nie odnieść wrażenia, że interfejsom w Javie 8
dużo bliżej do cech (ang. traits) w Scali, chociaż oczywiście nie są one aż tak
rozbudowane i elastyczne.
Powyżejprzedstawionojednozmożliwychzastosowańmetoddomyślnych.
Ich pierwotnym przeznaczeniem było jednak zachowanie wstecznej zgodno-
ści przy dodawaniu nowych metod d o istniejących interfejsów. W przeszłości
operacja taka natychmiast skutkowała błędem kompilacji (w końcu stary kod
nie mógł implementować nieistniejącej natenczas metody), teraz wystarczy
dostarczyć domyślną implementację nowych metod. Z tej cechy skorzystali
twórcy, pisząc nową metodę java.util.Collection.stream() – o której
w szczegółach za chwilę.
Uważni czytelnicy zastanawiają się zapewne, jak Java radzi sobie z konflik-
tami – gdy ma do dyspozycji więcej niż jedną implementację pochodzącą z
wielu interfejsów bazowych, które implementuje?
Listing 3. Konflikt domyślnych metod - kod nie kompiluje się!
interface Car {
default void fuel() {}
}
interface Boat {
default void fuel() {}
}
//Nie kompiluje się!
class Hovercraft implements Car, Boat {
}
Klasa Hovercraft implementuje dwa interfejsy, z których oba dostarczają
nie tylko deklarację, ale również implementację metody fuel(). Prowadzi
to do konfliktu, którego kompilator nie jest w stanie rozwiązać. Na szczęście
istnieje składnia pozwalająca explicite wywołać dowolną spośród skonflikto-
wanych metod lub dostarczyć trzecią, niezależną wersję:
Listing 4. Rozwiązywanie konfliktów metod domyślnych
class Hovercraft implements Car, Boat {
@Override public void fuel() {
Car.super.fuel();
}
}
23/ www.programistamag.pl /
JAVA 8 – NAJBARDZIEJ REWOLUCYJNA WERSJA W HISTORII
TYPY FUNKCYJNE
Użytkownicy bibliotek Guava (http://code.google.com/p/guava-libraries) lub Apa-
che Commons Collections (http://commons.apache.org/proper/commons-lang)
znają od dawna interfejsy Predicate czy Function. Są to próby
symulowania programowania funkcyjnego w Javie przed wersją 8. Pierwszy
z wymienionych interfejsów koncepcyjnie deklaruje jedną metodę, która
dla dowolnej instancji typu T udziela binarnej odpowiedzi true lub false.
Z kolei Function to interfejs, którego jedyna metoda pobiera jako
argument obiekt typu T, a zwraca obiekt typu R – w domyśle dokonuje
pewnej transformacji jednego obiektu w drugi. Te dość prymitywne próby
umożliwiały implementację funkcji wyższego rzędu na kolekcjach, takich jak
filter() czy map(). Java 8 wprowadza podobne typy do biblioteki standar-
dowej w pakiecie java.util.function:
Listing 5. Przydatne metody używające typów Function i Predicate
import java.util.function.Function;
import java.util.function.Predicate;
class FunctionalTypes {
public static Iterable filter(
Iterable in, Predicate pred) {
ArrayList out = new ArrayList<>();
for (T elem : in) {
if(pred.test(elem)) {
out.add(elem);
}
}
return out;
}
public static Iterable map(
Iterable in, Function fun) {
ArrayList out = new ArrayList<>();
for (T elem : in) {
out.add(fun.apply(elem));
}
return out;
}
}
Klasa FunctionalTypes definiuje w oparciu o wbudowane typy Predi-
cate i Function dwie niezwykle przydatne funkcje pomocnicze:
filter() i map(). Programiści znający jakikolwiek język funkcyjny skojarzą
je bez trudu. Dla tych z doświadczeniem jedynie w Javie śpieszę z tłumacze-
niem: filter() przyjmuje jako argument kolekcję pewnego typu i zwraca
kolekcję tego samego typu, ale tylko z elementami spełniającymi określone
kryterium. Z kolei map() przyjmuje kolekcję typu T (np. String) oraz funk-
cję z typu T w R (np. biorącą String, a zwracającą jego długość typu Inte-
ger). W rezultacie otrzymujemy kolekcję tej samej długości, ale na miejscu
każdego elementu wejściowego znajduje się ten element po zaaplikowaniu
przekazanej funkcji. Te narzędzia są dość trudne do zrozumienia, ale przykład
powinien wiele rozjaśnić:
Listing 6. Przykład użycia interfejsów Predicate oraz Function
List months = Arrays.asList(
"styczeń", "luty",
"marzec", "kwiecień",
"maj", "czerwiec",
"lipiec", "sierpień",
"wrzesień", "październik",
"listopad", "grudzień");
Iterable monthsWithR =
FunctionalTypes.filter(months,
new Predicate() {
@Override
public boolean test(String month) {
return month.contains("r");
}
});
Iterable monthLengths =
FunctionalTypes.map(months,
new Function() {
@Override
public Integer apply(String month) {
return month.length();
}
});
Pierwszy blok kodu wybiera spośród wszystkich nazw miesięcy tylko te, któ-
rych nazwy zawierają literkę „r“. Drugi przykład tworzy nową kolekcję, której
każdy element odpowiada ilości liter w nazwie odpowiadającego miesiąca.
Bezsprzecznie kod powyżej jest zbyt rozwlekły i nieczytelny, aby używać go w
codziennej pracy. Jednak standardowe typy Function, PredicateorazSupplier(funkcjabezargumentówzwracającawartośćtypuT)iCon-
sumer (funkcja pobierająca wartość typu T, ale nic niezwracająca) odgrywa-
ją kluczową rolę w implementacji wyrażeń lambda w nowej Javie.
Nim jednak przejdziemy do wyrażeń lambda, warto zwrócić uwagę na
nową adnotację @FunctionalInterface. Podobnie jak znana od dawna
@Override sygnalizuje ona jedynie kompilatorowi nasze zamiary. Adnotujemy
nią interfejs posiadający tylko jedną niezaimplementowaną metodę (i dowolną
ilość metod domyślnych, patrz: początek artykułu). Jeśli kiedykolwiek w tym in-
terfejsie pojawi się kolejna metoda bez definicji, kompilator zasygnalizuje błąd.
Poniżej przykład poprawnego interfejsu funkcyjnego z samego JDK:
Listing 7. Przykład interfejsu funkcyjnego
package java.util.function;
@FunctionalInterface
public interface Consumer {
void accept(T t);
default Consumer andThen(Consumer super T> after) {
//...
}
}
Dlaczego tak dużą wagę przykłada Java do interfejsów posiadających tyl-
ko jedną niezaimplementowaną metodę? Otóż instancje takich interfejsów
można automatycznie zamienić na wyrażenia lambda.
WYRAŻENIA LAMBDA
W Javie 8 każdy interfejs deklarujący tylko jedną metodę bez implementacji
(przykładami są także znane od lat java.lang.Runnable czy java.awt.
event.ActionListener)możnazdefiniować,korzystajączeznacznieuprosz-
czonej składni. Wszystkie poniższe wyrażenia są semantycznie równoważne:
Listing 8. Różne sposoby definiowania wyrażeń lambda
//A:
filter(months,
new Predicate() {
@Override
public boolean test(String month) {
return month.contains("r");
}
});
//B:
Predicate predicate =
(String month) -> month.contains("r");
filter(months, predicate);
//C:
filter(months,
(String month) -> month.contains("r"));
//D:
filter(months, m -> {
String needle = "r";
return m.contains(needle);
});
//E:
filter(months, m -> m.contains("r"));
24 / 2 . 2014 . (21) /
JĘZYKI PROGRAMOWANIA
Przykład A jest nam już znany. W przykładzie B tworzymy instancję imple-
mentującą typ Predicate, posługując się zupełnie nową składnią
w Javie 8 do definiowania wyrażeń lambda. Przed strzałką (->) umieszczamy
w nawiasie typy i nazwy parametrów funkcji. Po strzałce znajduje się definicja
funkcji. Jeśli na definicję składa się tylko jedno wyrażenie, możemy opuścić sło-
wo kluczowe return. Przykład C nie różni się niczym od poprzedniego, jednak
w tym wypadku umieściliśmy wyrażenie lambda bezpośrednio w miejscu użycia.
Przykład D obrazuje, jak zdefiniować wyrażenie lambda składające się
z wielu instrukcji. Tym razem konieczne są nawiasy klamrowe i słowo kluczo-
we return. Z kolei przykład E jest najbardziej zwięzły, pozwalamy bowiem
kompilatorowi na odgadnięcie typu (ang. type inference) zmiennej m z kon-
tekstu. Powyższe bloki kodu obrazują, jak bardzo zmieniła się składnia Javy
w wersji 8 (oczywiście pozostaje kompatybilna wstecznie). Jakby tego było
mało, istnieje jeszcze zwięźlejsza forma definiowania lambd w sytuacjach, gdy
widoczna jest metoda bądź konstruktor o sygnaturze pasującej do wyrażenia:
Listing 9. Wyrażenia lambda oparte o metody i konstruktory
//A:
map(months, month -> month.length());
map(months, String::length);
//B:
List primes = Arrays.asList("2", "3", "5");
map(primes, Integer::parseInt);
//C:
map(primes, p -> new Integer(p));
map(primes, Integer::new);
Składnia String::length jest dokładnym odpowiednikiem wyrażenia:
(String s) -> s.length() - czyli oznacza wywołanie metody na ar-
gumencie funkcji. Z kolei, mimo identycznej składni, Integer::parseInt
odpowiada wywołaniu statycznej metody klasy Integer: (String s) ->Integer.parseInt(s). Z reguły kompilator potrafi rozróżnić, który rodzaj wy-
rażenia lambda mieliśmy na myśli. Wreszcie pozostała składnia Integer::new,
którejodpowiadaniecodłuższe:(String s) -> new Integer(s)(nawiasem
mówiąc, wołanie konstruktora klasy Integer nie jest zalecane).
Ostatnim szczególnym przypadkiem definiowania wyrażeń lambda jest
wołanie metod bieżącej instancji klasy:
Listing 10. Wołanie metody bieżącej instancji
Iterable parseNums(List inputs) {
return map(inputs, this::parseStr);
}
Integer parseStr(String s) {
return Integer.parseInt(s);
}
this::parseStr to skrócona wersja od t -> this.parseStr(t). Natu-
ralnie możemy również definiować wyrażenia lambda o więcej niż jednym
parametrze (obie poniższe definicje są równoważne):
Listing 11. Wyrażenie lambda o więcej niż jednym parametrze
BiFunction fun =
(String s, Integer x) ->{return s.length() > x;};
BiFunction fun2 =
(s, x) -> s.length() > x;
Bezparametrowe wyrażenie lambda wymaga pustej pary nawiasów przed
strzałką:
Listing 12. Bezparametrowe wyrażenie lambda
new Thread(() -> handle(request)).start();
Ostatni przykład pokazuje nie tylko, jak łatwo stworzyć nowy wątek bez
konieczności żmudnego implementowania interfejsu Runnable. Warto też
zwrócić uwagę na fakt, że interfejs ten istniał jeszcze przed Javą 8. Mało tego,
każdy interfejs spełniający kryteria @FunctionalInterface można zaim-
plementować przy użyciu wyrażenia lambda! Interfejs taki dobrze jest ozna-
czyć przez @FunctionalInterface, ale nie jest to konieczne:
Listing 13. Typ użytkownika zastąpiony wyrażeniem lambda
interface MySocketHandler {
void handle(Socket socket);
}
//...
MySocketHandler handler = (Socket socket) -> {
System.out.println("Connect: " + socket);
//...
};
STRUMIENIE
Strumienie (java.util.stream.Stream, nie mylić ze strumieniami z pa-
kietu java.io) to zupełnie nowy (chociaż z całą pewnością nie nowatorski)
sposób pracy z kolekcjami w Javie, możliwy dzięki wprowadzeniu wyrażeń
lambda. Dla każdej kolekcji możemy stworzyć odzwierciedlający ją strumień
(przypomina iterator, lecz dużo potężniejszy) i wykonać na takim obiekcie
szereg transformacji, takich jak map() czy filter(). Strumienie mają dwie
niezwykle ważne cechy: po pierwsze dopóki nie zapragniemy otrzymać koń-
cowego wyniku bądź kolekcji, wszystkie operacje są leniwe, tzn. strumień je
zapamiętuje, ale wstrzymuje się z ich wykonaniem. Po drugie żadna operacja
na strumieniu nigdy nie modyfikuje oryginalnej kolekcji.
W Listingu 5 zaimplementowaliśmy ręcznie metody map() i filter().
Całe szczęście JDK 8 dostarcza gotowe implementacje:
Listing 14. Najprostsze przykłady użycia strumieni w Javie 8
import java.util.stream.Collectors;
//...
List monthsWithR2 = months.
stream().
filter(month -> month.contains("r")).
collect(Collectors.toList());
List monthLengths = months.
stream().
map(String::length).
collect(Collectors.toList());
Pierwszym, co rzuca się w oczy, jest wywołanie zupełnie nowej metody
stream(). Jest ona dostępna dla każdej kolekcji typu T i zwraca Stream.
Obiekt ten, jak nietrudno się domyśleć, udostępnia znane nam już metody
map() i filter(), oraz wiele innych. Ponieważ strumienie nie modyfikują
oryginalnej kolekcji, na koniec musimy zdecydować, do jakiego typu kolekcji
chcemy zebrać (ang. collect) rezultaty. Klasę Collectors poznamy za chwilę.
Dzięki obecności strumieni, które są swoistym „leniwym widokiem“ na
kolekcję, możemy wywołać łańcuch operacji bez obaw, że każda operacja
cząstkowa będzie budowała nową kolekcję od zera. Dopiero wywołanie tzw.
terminalnej operacji uruchamia wszystkie zdefiniowane transformacje - i tyl-
ko dla tych elementów, które faktycznie znajdą się w rezultacie:
Listing 15. Wiele operacji na jednym strumieniu
Iterable monthLengths =
months.
stream().
filter(month -> month.contains("r")).
map(String::length).
distinct().
limit(2).
collect(toList());
25/ www.programistamag.pl /
JAVA 8 – NAJBARDZIEJ REWOLUCYJNA WERSJA W HISTORII
Powyższy przykład pokazuje, jak można łączyć wiele operacji na jed-
nym strumieniu. Najpierw wyszukujemy miesiące posiadające w nazwie
literę „r“. Potem zamieniamy nazwy miesięcy na ich długości. distinct()
usuwa ze strumienia duplikaty, a limit(2) ogranicza wynikową kolekcję
do dwóch pierwszych rezultatów. Co ciekawe, dopiero ostatnie wywołanie
Collectors.toList() zaczyna iterować po wejściowej kolekcji. Gdyby nie
ono, wejściowa kolekcja w ogóle nie zostałaby odczytana, a koszt całego blo-
ku kodu byłby niemal zerowy, niezależnie od wielkości kolekcji.
Oprócz collect() istnieją inne operacje terminalne powodujące iterację
po kolekcji. Należą do nich m.in. min() i max() (znalezienie minimalnej/maksy-
malnej wartości w kolekcji, także w oparciu o wskazany Comparator), count()
czy all-/none-/anyMatch() - które sprawdzają, czy odpowiednio wszystkie,
żadne lub jakiekolwiek elementy kolekcji spełniają podane kryterium (predykat).
Aby oswoić się nieco ze strumieniami, napiszmy prosty program, który znajdzie
litery niewystępujące w nazwie żadnego miesiąca. W tym celu najpierw tworzy-
my syntetyczny strumień kodów ASCII liter od A do Z. Potem zamieniamy go na
strumień znaków. W następnym kroku filtrujemy ten strumień, pozostawiając
tylko te, które nie pojawiają się w nazwie żadnego miesiąca (stąd noneMatch()):
Listing 16. Litery niewystępujące w nazwie żadnego miesiąca
Set chars = IntStream.
rangeClosed((int) 'a', (int) 'z').
mapToObj(ascii -> (char) ascii).
filter(ch ->months.stream().noneMatch(
m -> m.indexOf(ch) >= 0)
).
collect(toSet());
Nie jest to rozwiązanie optymalne pod względem obliczeniowym, ale pozostaje
ciekawym przykładem możliwości strumienie w Javie 8. A’propos w nazwach
polskich miesięcy nie występują ani razu: b, f, h, q, v i x. Ale właściwie nie dowie-
dzieliśmy się jeszcze, jak wykonać dowolną operację na każdym elemencie ko-
lekcji, jak np. wypisanie na ekran czy zapis do pliku. W tym celu niezwykle przy-
datny okazuje się operator forEach(), świetnie zastępujący tradycją pętlę for:
Listing 17. Operator forEach():
for (String month : months) {
System.out.println(month);
}
months.stream().forEach(m -> {
System.out.println(m);
});
months.stream().forEach(System.out::println);
months.forEach(System.out::println);
Każde z powyższych wywołań jest równoważne: wypisze na ekran po kolei
nazwy wszystkich miesięcy. W przeciwieństwie do poznanych wcześniej ope-
ratorów, forEach() jako jedyny można wykonać bezpośrednio na kolekcji,
a nie tylko na strumieniu.
Ostatnia uwaga do strumieni: są one z natury ulotne i jednorazowe. Nie-
dopuszczalne jest utworzenie strumienia i używanie go wielokrotnie. Na
przykład poniższy kod przy powtórnym wykonaniu collect() rzuci Ille-
galStateException: stream has already been operated upon
or closed. Strumienie należy traktować podobnie do iteratorów - raz prze-
czytane nie mogą być ponownie użyte. Z tego powodu lepiej nigdy nie prze-
chowywać ani nie przekazywać instancji obiektu typu Stream, a jedynie
używać ich w miejscu utworzenia:
Listing 18. Próba ponownego użycia strumienia
Stream s = months.stream().
filter(m -> m.contains("r"));
s.forEach(System.out::println);
s.collect(toList()); //wyjątek!
RÓWNOLEGŁE STRUMIENIE
Trudno nie zauważyć, że strumienie oferują bardzo deklaratywny sposób pra-
cy z kolekcjami. W przeszłości transformacja, filtrowanie czy wyszukiwanie
wymagały ręcznej iteracji po kolekcji. Teraz, gdy te zachowania są zamknięte
w ściśle zdefiniowane wysokopoziomowe metody na strumieniach, twórcy
Javy pokusili się o zoptymalizowanie wielu operacji pod kątem wielowątko-
wości. Tak powstały równoległe strumienie. Nie różnią się pod względem API
niczym od standardowych strumieni, natomiast starają się wykonać więk-
szość operacji na wielu rdzeniach/wielu procesorach.
Listing 19. Równoległe strumienie
Iterable monthLengths =
months.
parallelStream().
filter(month -> month.contains("r")).
map(String::length).
distinct().
limit(2).
collect(toList());
Jedyna różnica w stosunku do normalnych strumieni polega na zamianie
wywołania stream() na parallelStream(). Od tej pory Java będzie pró-
bowała wykonać większość operacji, dzieląc wejściową kolekcję na mniejsze
części i przetwarzając je równolegle. Dla przykładu JVM podzieli kolekcję na
kilka mniejszych zakresów, przefiltruje każdy z osobna na innym rdzeniu/
procesorze, po czym połączy rezultaty. Należy jednak pamiętać, że niektó-
re operacje będą zachowywały się odmiennie dla równoległych strumieni.
Np. o ile kolejność iteracji w forEach() jest przewidywalna w normalnych
strumieniach (od pierwszego do ostatniego elementu), o tyle równoległy
strumień ma prawo wykonać blok kodu wewnątrz forEach() w dowolnej,
nieokreślonej kolejności.
Inną pułapką jest brak kontroli nad pulą wątków używaną przez
parallelStream(). Okazuje się bowiem, że wszystkie równoległe strumie-
nie w całej JVM (a zatem np. we wszystkich aplikacjach wdrożonych jednocze-
śnie na tym samym serwerze aplikacyjnym) współdzielą jedną, globalną pulę
wątków. Jest ona dostępna również dla nas poprzez wywołanie java.util.
concurrent.ForkJoinPool.commonPool(). Pula ta ma domyślnie tyle
wątków, iloma rdzeniami/procesorami dysponujemy. Ilość tą można zmienić,
ale również jedynie globalnie dla całej maszyny wirtualnej - korzystając ze
zmiennej środowiskowej: -Djava.util.concurrent.ForkJoinPool.
common.parallelism=50 (gdzie 50 to pożądana liczba wątków).
KOLEKTORY (COLLECTOR)
Dotychczas widzieliśmy zaledwie dwa kolektory: toList() i toSet() do-
stępne poprzez klasę java.util.stream.Collectors. Wiemy też, że
jest to sposób zgromadzenia wyników przetwarzania strumienia z powro-
tem do kolekcji pożądanego typu. Kolektory są jednak na tyle silną abstrak-
cją, że warto poznać inne dostępne implementacje, jak również nauczyć
się, jak tworzyć własne. Na początek poznajmy kolektor toMap(), który dla
każdego elementu ze strumienia wykonuje dwie funkcje: pierwsza wylicza
klucz w mapie, a druga - wartość. Zbiór takich par tworzy mapę. Załóżmy
na przykład, że chcemy skonstruować mapę, w której kluczami będą nazwy
miesięcy pisane wielkimi literami, a wartościami - ilość wystąpień litery „e“
w nazwie:
Listing 20. Przykład użycia Collectors.toMap()
Map monthByLen = months.
stream().
collect(
toMap(
String::toUpperCase,
m -> StringUtils.countMatches(m, "e")
));
2/2014 (21) www•programistamag•pl Cena 12.90 zł (w tym VAT 23%) Index: 285358 WPF:STYLOWANIEKONTROLEK •POCZĄTKIZFORTRAN •CONCEPTSLITE:ROZSZERZENIE STANDARDUC++14 Własny debugger dla Windows Badanie jakości kodu C ++ Interface a implementacja Przedstwiamy szcze- góły interfejsu Windows Debug API O narzędziach i metry- kach do analizy jakości oprogramowania Separowanie interfejsu użytkownika od logiki aplikacji Javarewolucyjne zmiany
4 / 2 . 2014 . (21) / REDAKCJA/EDYTORIAL W tym wydaniu Programisty absolutna gratka dla progra- mistów Java – obszerny artykuł Tomasza Nurkiewicza „Java 8 – najbardziej rewolucyjna wersja w historii” opisujący przełomo- we zmiany w nowej wersji tego języka. Współautor cyklu „Bliżej silikonu”, Mateusz Jurczyk, nadal pozostaje przy temacie niskopoziomowego programowania i, tym razem indywidualnie, przybliży nam, jak napisać własny debugger w systemie Windows. Dziś pierwsza część tego arty- kułu, a na drugą odsłonę zapraszamy już w kolejnym numerze. Jako że język C++ z pewnością nie należy do najprostszych, ciekawą propozycją dla jego użytkowników jest artykuł Sła- womira Zborowskiego o badaniu jakości kodu stworzonego w tymże języku. Tekst opisuje różne rodzaje metod badania jako- ści i narzędzia do tego przeznaczone – poczynając od detekcji copy-paste, aż po analizę statyczną. Programistów C# powinien zainteresować natomiast ar- tykuł Wojciecha Sury „Interface a implementacja” o rozwiązy- waniu problemów dotyczących łączenia GUI z implementacją. Prezentuje on złe i dobre podejścia do tego tematu i pokazuje, jak stworzyć aplikację, w której widoki można zmieniać niczym rękawiczki. To jednak nie wszystko, co można znaleźć w najnowszym wydaniu Programisty, więc jak zwykle zachęcamy do znalezie- nia wygodnego miejsca i dokładnego zapoznania się z najnow- szym wydaniem, w którym każdy, mamy nadzieję, znajdzie dla siebie interesujący temat. Z wyrazami szacunku Redakcja Wydawca/ Redaktor naczelny: Anna Adamczyk annaadamczyk@programistamag.pl Redaktor prowadzący: Łukasz Łopuszański lukaszlopuszanski@programistamag.pl Korekta: Tomasz Łopuszański Kierownik produkcji: Krzysztof Kopciowski bok@keylight.com.pl DTP: Krzysztof Kopciowski Dział reklamy: reklama@programistamag.pl tel. +48 663 220 102 tel. +48 604 312 716 Prenumerata: prenumerata@programistamag.pl Współpraca: Michał Bartyzel Mariusz Sieraczkiewicz Michał Leszczyński Marek Sawerwain Łukasz Mazur Rafał Kułaga Sławomir Sobótka Michał Mac Gynvael Coldwind Bartosz Chrabski Adres wydawcy: Dereniowa 4/47 02-776 Warszawa Druk: ArtDruk – www.artdruk.com ul. Napoleona 4 05-230 – Kobyłka Nakład: 5000 egz. Redakcja zastrzega sobie prawo do skrótów i opracowań tekstów oraz do zmiany planów wydawniczych, tj. zmian w zapowiadanych tematach artykułów i terminach publikacji, a także nakładzie i objętości czasopisma. O ile nie zaznaczono inaczej, wszelkie prawa do materiałów i znaków towarowych/firmowych zamieszczanych na łamach magazynu Programista są zastrzeżone. Kopiowanie i rozpowszechnianie ich bez zezwolenia jest Zabronione. Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również za inne straty i wydatki poniesione w związku z wykorzystaniem informacji prezentowanych na łamach magazynu Programista. Magazyn Programista wydawany jest przez Dom Wydawniczy Anna Adamczyk Zamów prenumeratę magazynu Programista przez formularz na stronie http://programistamag.pl/typy-prenumeraty/ lub zrealizuj ją na podstawie faktury Pro-forma. W spawie faktur Pro-Forma prosimy kontktować się z nami drogą mailową redakcja@programistamag.pl. Prenumerata realizowana jest także przez RUCH S.A. Zamówienia można składać bezpośrednio na stronie www.prenumerata.ruch.com.pl Pytania prosimy kierować na adres e-mail: prenumerata@ruch.com.pl lub kontaktując się telefonicznie z numerem: 801 800 803 lub 22 717 59 59 (godz.: 7:00 – 18:00 (koszt połączenia wg taryfy operatora).
5/ www.programistamag.pl / SPIS TREŚCI BIBLIOTEKI I NARZĘDZIA Interface a implementacja....................................................................................................................... Wojciech Sura 6 Wstęp do WPF – część 2: Stylowanie kontrolek w WPF..................................................................... Wojciech Sura 12 ASP.NET SignalR – czyli aplikacje czasu bardzo rzeczywistego. Część 2.......................................... Karol Rogowski 16 JĘZYKI PROGRAMOWANIA Java 8 – najbardziej rewolucyjna wersja w historii............................................................................... Tomasz Nurkiewicz 22 PoczątkizjęzykiemFortran...................................................................................................................... Radosław Suduł 30 Concepts Lite. Rozszerzenie standardu C++14................................................................................... Robert Matusewicz 36 PROGRAMOWANIE SYSTEMOWE Jak napisać własny debugger w systemie Windows – część 1.......................................................... Mateusz “j00ru” Jurczyk 38 TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ Badanie jakości kodu C++........................................................................................................................ Sławomir Zborowski 46 LABORATORIUM BOTTEGA Refaktoryzacja testów legacy w kierunku wykonywalnych specyfikacji. Część II: Techniki uła- twiające utrzymanie testów.................................................................................................................... Rafał Jamróz 54 Brakujący element Agile. Część 1: Feedback........................................................................................ Paweł Badeński 60 PLANETA IT Szczyt za szczytem..................................................................................................................................... Łukasz Sobótka 64 STREFA CTF Ghost in the Shellcode 2014 – Pwn Adventure 2................................................................................ Michał "Redford" Kowalczyk 66 KLUB LIDERA IT Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 6)....................... Mariusz Sieraczkiewicz 70 KLUB DOBREJ KSIĄŻKI Scala od podszewki................................................................................................................................... Marek Sawerwain 72
6 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Wojciech Sura NIE METODA, A AKCJA Spróbujmy spojrzeć na interface użytkownika z nieco innej perspektywy. W momencie, gdy użytkownik klika przycisk lub wybiera opcję z menu, jego intencją jest wykonanie pewnej akcji: otwarcie nowego dokumentu, skopio- wanie fragmentu tekstu do schowka albo zamknięcie programu. Przez akcję rozumiemy zatem pojedynczą operację, którą można wywołać z poziomu in- terface'u użytkownika naszej aplikacji. Najprostszym sposobem zaimplementowania takiej akcji jest wywołanie określonej metody w momencie, gdy użytkownik wybierze odpowiedni ele- ment interface’u. Sytuacja skomplikuje się jednak nieco, gdy okaże się, że ta sama akcja może zostać wywołana z poziomu kilku różnych miejsc albo – co gorsza – że jej wywołanie uwarunkowane jest zaistnieniem pewnych szcze- gólnych okoliczności. Żaden z opisanych problemów nie jest oczywiście nie do rozwiązania. Tyle tylko, że oprogramowanie odpowiednich mechanizmów zajmuje czas, wymaga rozszerzenia architektury aplikacji i panowania nad wszystkim tak, aby na koniec nie utonąć w gąszczu zależności, warunków i flag. Odpowiedzią na powyższe potrzeby jest mechanizm, który spełnia nastę- pujące kryteria. Po pierwsze, pozwala na zdefiniowanie akcji jako osobnego bytu, stanowiącego pomost pomiędzy interfacem użytkownika a kodem źró- dłowym. Po drugie, umożliwia określenie zależności pomiędzy elementami interface’u, a później automatycznie pilnuje ich w trakcie pracy programu. Dzięki temu programista nie musi skupiać się na niepotrzebnej pracy, bo integralności interface’u pilnuje jeden spójny mechanizm. Na koniec dobrze byłoby, gdyby był on zaprojektowany w sposób, który ułatwia rozwój zarów- no jego samego, jak i aplikacji, którą zarządza. Chciałbym zaproponować dwa rozwiązania. Pierwszym z nich – z uwagi na brak takowego w standardowych bibliotekach .NET dla Windows Forms – jest otwarty mechanizm o nazwie Visual State Manager. Postaram się wyja- śnić, w jaki sposób można z niego skorzystać i go rozwijać, a także na jakich pomysłach opiera się jego działanie, co pozwoli w łatwy sposób przygotować w razie potrzeby jego odpowiednik dla innego języka programowania lub frameworka. Drugim z nich jest mechanizm przygotowany przez Microsoft działający w środowiskuWPF, którego można użyć zarówno w aplikacjach de- sktopowych, Windows 8, jak i dla Windows Store Apps lub Windows Phone. VISUAL STATE MANAGER Visual State Manager dla Windows Forms opiera się na istnieniu i współpracy trzech rodzajów obiektów: akcji, warunków i operatorów kontrolek (Action, Condition, Control operator). Zadaniem programisty jest zdefiniowanie zależ- ności pomiędzy tymi trzema warstwami, a następnie dostarczanie informacji o zajściu zdarzeń mogących mieć wpływ na warunki, a w efekcie na akcje. Warunek Warunek jest prostym obiektem przechowującym pojedynczą wartość typu bool mówiącą o tym, czy jest on w tym momencie spełniony, czy też nie. Mechanizm managera wykorzystuje warunki do regulowania dostępności, widoczności i zaznaczenia elementów interface'u odpowiedzialnych za wy- woływanie akcji. public interface ICondition { bool Value { get; } event ValueChangedHandler ValueChanged; } Warunek dostępny jest w postaci bardzo prostego w implementacji in- terface’u Icondition: wystarczy dostarczyć tylko własność informującą o obecnym stanie warunku oraz zdarzenie powiadamiające o tym, iż stan ów właśnie się zmienił. Dla wygody przygotowane jest kilka klas implemen- tujących ten interface. Pierwszą z nich jest Condition – najprostsza imple- mentacja. Ponadto możemy skorzystać z klasy CompositeCondition, któ- ra pozwala na połączenie kilku warunków przy pomocy operacji logicznych „i” oraz „lub”, a także NegateCondition, która neguje wskazania innego warunku. Akcja Akcja stanowi abstrakcję dla pewnego działania, które może podjąć użytkow- nik. Każdą akcję – jak już wcześniej wspomniałem – możemy scharakteryzo- wać trzema cechami: dostępnością, widocznością oraz zaznaczeniem. Do- stępność określa, czy użytkownik może w danym momencie akcję wywołać, widoczność informuje, czy kontrolki odpowiedzialne za tę akcję powinny być w ogóle wyświetlane, zaś zaznaczenie przydaje się w przypadku akcji odpo- wiadających za przełączenie jakiegoś stanu (na przykład zawijanie linii lub widoczność paska narzędzi). Zadaniem akcji jest również automatyczne przywiązanie się do wska- zanych kontrolek w interface użytkownika. Dzięki temu programista zwol- niony jest z obowiązku ustawienia tym kontrolkom odpowiednich handle- rów zdarzeń, a także dbania o synchronizację stanów – wszystko dzieje się automatycznie. Skorzystanie z akcji jest stosunkowo proste, ponieważ wystarczy tylko ją zainstancjonować, przekazując w locie parametry definiujące, w jaki sposób powinna działać: »» Metodę,któramazostaćwywołanapowybraniuakcjiprzezużytkownika; »» (opcjonalnie) warunek regulujący dostępność akcji; »» (opcjonalnie) warunek regulujący zaznaczenie elementów interface'u po- wiązanych z akcją; »» (opcjonalnie) warunek regulujący widoczność elementów interface'u po- wiązanych z akcją; »» (opcjonalnie) kontrolki, które będą odpowiadały za wywołanie akcji. Interface a implementacja W każdym programie okienkowym prędzej czy później programista staje przed zadaniem powiązania wizualnych kontrolek z kodem źródłowym, który realizuje za ich sprawą różne zadania. Najprostszym i często stosowanym sposobem jest napi- sanie kodu bezpośrednio w handlerach zdarzeń. Sposób ten oczywiście zadziała, ale to jest chyba wszystko dobre, co można o nim powiedzieć. W niniejszym artyku- le chciałbym zaproponować nieco inne podejście do rozwiązywania tego problemu.
8 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Operator kontrolek Ostatnią z opisywanych warstw stanowi operator kontrolek, który pozwala ujednolicić sposób współpracy akcji z komponentami wizualnymi. Domyśl- nie dostarczona jest implementacja dla większości standardowych kontrolek, więc jeśli skorzystamy z nich, nie trzeba robić już nic więcej. W przypadku mniej standardowych, Visual State Manager spróbuje ustawić odpowiednie właściwości przy pomocy refleksji, a jeśli i to rozwiązanie okaże się niesatys- fakcjonujące, programista może rozszerzyć mechanizmy VSM poprzez do- starczenie pośrednika, który umożliwi współpracę Visual State Managera z dowolnym zestawem niestandardowych komponentów. JAK TO DZIAŁA? Spróbujmy napisać prosty przykład – standardowe operacje na liście elemen- tów: dodawanie, edytowanie i usuwanie. Zaczniemy od zdefiniowania logiki zależności pomiędzy kontrolkami i akcjami. »» Dodać element możemy w każdym momencie; »» Edycja dostępna jest tylko wówczas, gdy zaznaczony jest dokładnie jeden element; »» Usuwanie dostępne jest wtedy, gdy zaznaczony jest przynajmniej jeden element (jeden lub więcej). Teraz możemy zaprojektować odpowiednią formatkę. Rysunek 1. Główne okno programu Kolejną czynnością będzie zaimplementowanie zdefiniowanych wcześniej zależności. Dla czytelności kodu proponuję umieścić odpowiednie pola wraz z ustawiającą je metodą w osobnym pliku – na przykład DataEditForm.Logic. cs. Zaczynamy od dodania skrótów do namespace’u i skrótu do typu: Listing 1. Wymagane odniesienia do namespace'ów using VisualStateManager; using Action = VisualStateManager.Action; Kolejnym krokiem będzie przygotowanie pól przechowujących odpowiednie warunki i akcje: Listing 2. Deklaracje warunków i akcji public partial class DataEditForm { private Condition singleItemSelected; private Condition itemsSelected; private Action addElementAction; private Action editElementAction; private Action removeElementsAction; } Definiowanie zależności jest stosunkowo proste, ponieważ odbywa się w kon- struktorach odpowiednich klas. W naszym przypadku możemy więc napisać: Listing 3. Metoda inicjalizująca warunki i akcje public void InitializeActions() { singleItemSelected = new Condition(false); itemsSelected = new Condition(false); addElementAction = new Action(DoAdd, bAdd); editElementAction = new Action(DoEdit, singleItemSelected, bEdit); removeElementsAction = new Action(DoRemove, itemsSelected, bRemove); } Jak wcześniej wspomniałem, konstruktory akcji są dosyć elastyczne i po- zwalają określić metodę, która ma zostać wykonana, gdy użytkownik wywoła akcję, warunki dla dostępności, zaznaczenia i widoczności kontrolek, a także listę komponentów, które będą tę akcję wyzwalały. Kontrolowanie stanów kontrolek będzie odbywało się automatycznie, ale musimy najpierw nauczyć VSM, w jakich sytuacjach warunki się zmieniają. Zrobimy to, implementując zdarzenie reagujące na zmianę zaznaczenia listy: Listing 4. Zmiana wartości warunku private void lbData_SelectedValueChanged(object sender, EventArgs e) { singleItemSelected.Value = lbData.SelectedItems.Count == 1; itemsSelected.Value = lbData.SelectedItems.Count > 0; } Na koniec warto wspomnieć o jednej rzeczy: ponieważ kontrolki wizualne muszą być już zainstancjonowane, gdy przygotowujemy logikę akcji, meto- da InitializeActions powinna zostać wywołana po wywołaniu metody InitializeComponent. Wygodnym miejscem jest więc konstruktor okna, w którym obie metody możemy wywołać we właściwej kolejności: Listing 5. Inicjalizowanie okna public DataEditForm() { InitializeComponent(); InitializeActions(); } I to wszystko – mechanizm jest już przygotowany do pracy. Naturalnie do za- implementowania pozostają jeszcze same akcje – przykładowy projekt moż- na odnaleźć w materiałach do artykułu dostępnych do ściągnięcia ze strony magazynu Programista. Jakie zalety ma zastosowane powyżej rozwiązanie? Po pierwsze, me- chanizm będzie pilnował za nas, aby użytkownik nie wywołał akcji, gdy nie będzie takiej możliwości: ogranicza to liczbę błędów mogących powstać w trakcie pracy programu. Zauważmy też, że każda kontrolka, która jest odpo- wiedzialna za wywołanie akcji, automatycznie będzie kontrolowana przez mechanizm (przynajmniej dopóki nie spróbujemy ręcznie ustawiać handle- rów zdarzeń na metody realizujące akcje, ale przed tym Visual State Manager ani inny gotowy mechanizm nie jest już w stanie zabezpieczyć). Drugą zaletą jest stosunkowa łatwość rozbudowy możliwości formatki. Jeśli zmienią się okoliczności wywołania niektórych akcji, wystarczy dodać odpowiednie warunki lub zbudować warunki kompozytowe, a wszystko wciąż będzie działało prawidłowo. Bardziej wymagającym programistom pozostanie oczywiście opcja implementacji interface’u ICondition w celu wprowadzenia bardziej skomplikowanych zależności. Jeśli zaistnie- je potrzeba dodania jeszcze jednego miejsca, z poziomu którego będzie można wywołać akcję, sprowadzi się to tylko do dodania odpowiedniej kontrolki do konstruktora klasy Action. Poza tym wprowadzenie do- datkowej warstwy pomiędzy interfacem użytkownika i implementację pozwala na bardzo łatwą wymianę tego pierwszego. Sprowadzi się to bowiem tylko do wprowadzenia odpowiednich modyfikacji w metodzie InitializeActions. Trzecią zaletą jest fakt, iż podczas korzystania zVisual State Managera pro- gramista może zdefiniować zależności rządzące formatką w bardzo naturalny sposób. Jest to znacznie wygodniejsze od ręcznego implementowania me- chanizmów zabezpieczających.
9/ www.programistamag.pl / INTERFACE A IMPLEMENTACJA WINDOWS PRESENTATION FOUNDATION Biblioteki Windows Presentation Foundation, w przeciwieństwie do Windows Forms, dostarczają gotowe rozwiązanie dla zadanego problemu. ICommand Kluczową rolę odgrywa tu interface ICommand, który jest odpowiednikiem akcji z zaprezentowanego wcześniej mechanizmu, i wygląda następująco: Listing 6. Interface ICommand public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); } Przeznaczenia składowych ICommandmożna się łatwo domyślić: Executema wykonać komendę, CanExecute powinno stwierdzić, czy w danym momen- cie może ona zostać wykonana, zaś zdarzenie CanExecuteChanged infor- muje o sytuacji, w której dostępność komendy uległa zmianie. CanExecute jest zazwyczaj wywoływana w reakcji na zdarzenie CanExecuteChanged. Najprostszym sposobem wykorzystania komend jest po prostu zaimple- mentowanie interface'u ICommand, a następnie przypisanie instancji takiej klasy do własności Command kontrolek wizualnych. Przykładowa implemen- tacja dostępna jest na Listingu 7. Listing 7. Przykładowa implementacja ICommand internal class SimpleCommand : ICommand { private bool canExecute; private Action
10 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA RoutedCommand Na koniec przyjrzymy się nieco bardziej zaawansowanym technikom progra- mowania z pomocą komend w WPFie. Biblioteki te dostarczają bowiem kilka klas i mechanizmów, które pozwalają znacznie rozszerzyć funkcjonalność komend. Koncepcja komend wWPFie składa się z czterech kluczowych elementów: »» Komendy (Command), reprezentującej akcję, która ma zostać wykonana; »» Źródła komendy (Command source) – obiektu, który wywołuje komendę; »» Celu komendy (Command target) – obiektu, na którym jest wykonywana komenda »» Powiązania komendy (Command binding), który wiąże komendę z jej lo- giką w aplikacji. Aby zrozumieć, w jaki sposób powyższe elementy ze sobą współpracują, prześledźmy, jaką ścieżkę musimy przejść, by komenda została wykonana. Wszystko zaczyna się od źródła komendy (Command Source), które wyzwala komendę. Źródłem takim może być na przykład element menu, przycisk albo skrót klawiaturowy. Następnie mechanizmy WPF określają cel komendy. Może on być zdefiniowany jawnie w deklaracji kontrolki będącej źródłem komendy; jeśli tak nie jest, to jako cel komendy przyjmowany jest obiekt, który w danym momencie ma fokus. Gdy cel komendy zostanie określony, do głosu dochodzi routing, który stara się odnaleźć kontrolkę posiadającą odpowiednie powiązanie (com- mand binding) pozwalające na wykonanie wybranej komendy. Routing dzia- ła identycznie, jak w przypadku routed events, to jest najpierw wykonywany jest tunneling, a później bubbling w poszukiwaniu handlerów zdarzeń – od- powiednio PreviewExecute lub Execute. Po odnalezieniu odpowiedniego handlera jest on wykonywany i na tym cała operacja się kończy. Choć proces ten może wydawać się skomplikowany, w rzeczywistości umożliwia zaimplementowanie komend przy pomocy mniejszej ilości kodu źródłowego, a ponadto pozwala na bardziej precyzyjne rozplanowanie logiki w zależności od źródła i celu komendy. Implementacja Na wstępie zauważmy, że w przypadku klas udostępnianych przez WPF komenda sama w sobie nie wykonuje żadnego kodu związanego z logiką aplikacji – jej zadaniem jest tylko wyzwolenie mechanizmu mającego za zadanie odnalezienie celu komendy i wykonania routingu. Oznacza to, że komendy same w sobie są całkowicie odizolowane od aplikacji, dlatego też w bibliotekach WPF odnajdziemy pewien zestaw standardowych komend, z których możemy od razu skorzystać, na przykład otwórz, zapisz, kopiuj, wklej czy odtwórz. Odpowiednie instancje odnajdziemy w namespace System. Windows.Input (klasy MediaCommands, ApplicationCommands, Navi- gationCommands i ComponentCommands) oraz System.Windows.Doc- uments (EditingCommands). Jeśli wśród nich nie odnajdziemy potrzebnej nam komendy, możemy również skorzystać z klasy RoutedCommand, co ma miejsce w poniższym przykładzie. Zaczynamy od zdefiniowania komend. W naszym (uproszczonym) przy- padku umieścimy je w zasobach okna; jeśli programujemy zgodnie z zasada- mi modelu MVVM, wówczas bardziej właściwym miejscem będzie viewmodel. Listing 12. Osadzenie zdarzeń w zasobach okna Teraz możemy określić, które kontrolki mają wyzwalać zdarzenia:
Listing 13. Powiązanie elementów interface'u z komendamiKolejnym krokiem jest przygotowanie command bindings, które pozwolą na
powiązanie komend z logiką aplikacji. Przykładowy program jest bardzo pro-
sty, więc powiązania te można umieścić praktycznie w dowolnym miejscu; w
naszym przypadku podwiesimy je również pod klasę okna:
Listing 14. Definiowanie akcji i warunków wykonania dla komend Jakwidać,prawiecałykododpowiedzialnyzalogikękomendudałosięupchnąć
w XAMLu. W code-behind musimy umieścić tylko to, czego w XAMLu zrobić się
już nie dało, czyli obsłużyć zdarzenia CanExecute oraz Executed. I voila!
Listing 15. Reakcja na zmianę warunków
private void editCommandBinding_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = lbData.SelectedItems.Count == 1;
}
private void removeCommandBinding_CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = lbData.SelectedItems.Count > 0;
}
Zaprezentowane mechanizmy są obecne w większości frameworków. Zdecy-
dowałem się opisać tylko dwa z nich, bo w większości przypadków rządzące
nimi reguły są podobne i łatwo jest przestawić się z jednego modelu na inny.
Warto jednak z nich korzystać, wprowadzając dodatkową warstwę pomiędzy
interfacem użytkownika i logikę programu – w znacznym stopniu ułatwia to
potem jego rozwój i konserwację.
Kody źródłowe wszystkich trzech aplikacji są dostępne do ściągnięcia ze
strony internetowej magazynu Programista w zakładce„download”.
Wojciech Sura wojciechsura@gmail.com
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
11/ www.programistamag.pl / MATERIAŁ PROMOCYJNY J est to ogólnopolski konkurs programistyczny. Dla każdego lubiącego intelektualny pot. Pierwsze zagadnienie zostało już opublikowane 25 lutego. Przez kolejne tygodnie uczestnicy zmagać się będą z zadaniami związanymi z szeroko pojętym tworzeniem oprogramowania: algorytmiką, architekturą, refaktoryzacją itp. MasterCodere’m zostanie ten, który suma- rycznie uzyska największą ilość punktów. W tym momencie na stronie www.mastercoder.pl zarejestrowało się już prawie 200 osób. Rejestracja jest wciąż otwarta, więc liczba uczestników nie- ustannie rośnie. Spośród nich 5 najlepszych osób zostanie zaproszonych na finał w oddziale organizatora konkursu. Dlakogojestprzeznaczonykonkurs?Iczegomogą spodziewaćsięuczestnicywkolejnychzadaniach? Dla wszystkich, którzy programując, dobrze się bawią. Fakt, czy pracujesz jako programista zawodowo, nie ma tutaj znaczenia. To jest konkurs dla pasjona- tów, zarówno profesjonalistów, jak i studentów oraz amatorów – twierdzi jeden z autorów pytań, Przemysław Różycki – Solution Architect w firmie Cybercom Poland – i dodaje, że uczestnik MasterCodera powinien być gotowy na wszystko! A najbardziej na to, iż oceniając, mamy na uwadze istotny fakt – oprogramowania używająludzie.Inawetjeśliprogramykonkursowemająograniczonąużyteczność, to będą oceniane tak, jakby były częścią realnych projektów. Co oznacza nie tylko wysoką jakość, ale również czytelność kodu i łatwość rozbudowy. Jak wiadomo, prawdziwy klient z krwi i kości często zmienia zdanie, oczekując naszej reaktywno- ści, więc niewykluczone, że nam również w którymś zadaniu się odmieni. Obecniejestwielekonkursówprogramistycznych, które są organizowane dla ludzi z branży IT Nie twierdzimy, że nasz konkurs jest wyjątkowy. Możemy jednak zapewnić, że nasz konkurs jest przekrojowy. Nie dotyczy jedynie algorytmiki czy danego języka progra- mowania. U nas sprawdzić może się każdy, niezależnie od ulubionej technologii, w wielu aspektach związanych z tworzeniem oprogramowania: algorytmy, refaktory- zacja,bazydanychitp.Dodatkowąwartościąjestbezpośrednikontaktzorganizato- rami, którzy liczą na kreatywność uczestników i wierzą, że uczestnicy zaproponują nietypoweiinnowacyjnerozwiązaniaproblemów,anietylkote,którepasujądoszta- mywyznaczonejprzezautorów – podkreśla inny pasjonat i współorganizator kon- kursu – JarosławTrepczyński, Senior Software Consultant w Cybercom Poland. Zachęcamy więc wszystkich do zapoznania się z większą ilością szczegó- łów na stronie konkursu: www.mastercodera.pl. Jesteśmy dopiero po pierw- szym zadaniu, więc wciąż liczymy jeszcze na zgłoszenia pasjonatów progra- mowania. Głęboko wierzymy, że konkurs MasterCoder na stałe wpisze się w kalendarz kluczowych wydarzeń w branży IT. MasterCoder by Cybercom Poland Olimpiada w Soczi zakończona? Czas rozpocząć MasterCodera! Kolejny rok bez powołania na Igrzyska Olimpijskie? Nic straconego. Mamy dla Ciebie coś o wiele lepszego. Nie musisz skakać na nartach jak nasz złoty medalista. Nikt nie będzie kazał biegać Ci z pękniętą stopą. Curling na lodzie może być dla Ciebie niezrozumiały, a przekaz płynący z łyżwiarstwa figurowego totalnie niejasny! Ruszył MasterCoder - gdzie jedyne znaczenie ma zdolność analitycznego myślenia, kreatyw- ność w rozwiązywaniu problemów oraz wszechstronność technologiczna. O Cybercom Poland Sp. z o.o. Firma Cybercom została założona w 1995 roku w Szwecji. Od momentu powstania firma dynamicznie się rozwija, zarówno poprzez wzrost orga- niczny, jak i przejęcia firm w strategicznych miejscach na świecie. Obec- nie Grupa Cybercom ma biura w dziewięciu krajach i zatrudnia 1 600 osób. W Polsce Cybercom posiada dwa oddziały: w Warszawie i Łodzi, w których jest obecnie zatrudnionych ponad 140 kompetentnych i doświadczonych specjalistów. Główne obszary działań to telekomunikacja, przemysł, me- dia, bankowość i finanse, handel detaliczny oraz sektor publiczny. Firma specjalizuje się w rozwiązaniach internetowych, bezpieczeństwa, usługach mobilnych oraz telekomunikacyjnych. Świadczy też pełen zakres usług consultingowych i outsourcingowych, testowania oraz R&D dla dużych i średnich firm. Globalne zaplecze i zweryfikowane narzędzia sprawiają, że w sposób więcej niż satysfakcjonujący firma może realizować zarówno lo- kalne projekty o zasięgu ogólnopolskim, jak i międzynarodowym. Dowiedz się więcej o firmie na: http:www.cybercom.pl
12 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Wojciech Sura T ematyka, którą chcę poruszyć, dotyka zaawansowanych zagadnień frameworka WPF – zakładam więc, że czytelnik zaznajomiony jest z jego podstawowymi koncepcjami, takimi jak dependency properties, bindings czy routed events. W razie potrzeby zachęcam do sięgnięcia po jeden z poprzednich numerów Programisty, w którym tematy te zostały omówione. KOSZTOWNE ZACHCIANKI Jakiś czas temu zachciało mi się wprowadzić do mojego programu este- tycznie wyglądające pole tekstowe służące do szybkiego filtrowania danych – takie, jakie obecnie ma Visual Studio. Rysunek 1. Estetyczne pole wyszukiwania Napisanie własnego komponentu wizualnego (nawet od zera) to dla mnie żadna nowość, ale mój zapał do pisania takiego pola tekstowego szybko ostygł, gdy uświadomiłem sobie, ile pracy będzie kosztowało mnie oprogra- mowanie całej jego funkcjonalności znanej ze współczesnegoWindowsa – od wprowadzania tekstu, poprzez ustawianie kursora w dowolnym miejscu, za- znaczenie (klawiaturą i myszką) oraz przenoszenie zaznaczonego tekstu, aż do obsługi schowka i standardowego menu kontekstowego. Próbowaliście kiedyś napisać pole tekstowe od zera? Ja tak – było to w cza- sach liceum, gdy uparłem się zaimplementować w DOS-owym trybie graficznym framework podobny do VCLa znanego z Delphi. Znalazłem w sobie dostatecznie dużo samozaparcia, by oprogramować wprowadzanie znaków przy pomocy kla- wiatury i przesuwanie kursora klawiszami strzałek, ale poddałem się już wówczas, gdy doszło do implementacji mechanizmu pozwalającego użytkownikowi na wpisanie do pola więcej tekstu niż mieściło się w obszarze widocznym na ekranie. Rysunek 2. Pole tekstowe pisane od zera Jak zapewne się domyślacie, WPF pozwala na zaprojektowanie takiego kom- ponentu przy znacznie mniejszym nakładzie pracy. Spróbuję pokazać, w jaki sposób można to osiągnąć. MODYFIKOWANIE ISTNIEJĄCYCH KONTROLEK Windows Presentation Foundation daje użytkownikowi możliwość wprowa- dzania zmian do istniejących kontrolek przy pomocy mechanizmu stylów. Style w WPF działają w podobny sposób, jak w CSS w HTMLu – umożliwiają automatyczne ustawianie wartości własnościom określonych kontrolek. Działanie stylów WPF najłatwiej jest wyjaśnić na przykładzie, więc zapro- jektujmyprosteoknowXAMLu,naktórymbędziemymoglieksperymentować. Listing 1. Proste okno w XAMLu Zacznijmy od czegoś prostego; spróbujmy przygotować własny styl dla etykie-
ty, przy pomocy którego zmienimy jej kolor na czerwony. Oto odpowiedni kod:
Listing 2. Styl dla etykietyPierwszym, co rzuca się w oczy, jest atrybut TargetType. WPF wymaga bo-
wiem, by jednoznacznie określić, jakiego typu obiekty będziemy chcieli mo-
dyfikować naszym stylem. Wewnątrz stylu definiujemy natomiast listę sette-
rów, z których każdy opisuje, którą własność obiektu chcemy zmienić i jaka
ma być jej nowa wartość. Styl jest gotowy, ale teraz trzeba go zaaplikować do
etykiety znajdującej się w oknie.
APLIKOWANIE STYLU
Najprostszym sposobem zaaplikowania stylu jest po prostu jego bezpośred-
nie przypisanie do kontrolki, którą chcemy zmodyfikować, i wygląda to mniej
więcej następująco:
Wstęp do WPF – część 2:
Stylowanie kontrolek w WPF
Po jakimś czasie pracy z dowolnym frameworkiem graficznym programista zaczyna
odkrywać, że standardowy zestaw kontrolek to zbyt mało, aby napisać wygodną, dyna-
miczną i estetyczną aplikację. Na ratunek przychodzą wówczas różne darmowe i płatne
internetowe repozytoria udostępniające multum dodatkowych komponentów. W przy-
padku WPF nie zawsze konieczne jest jednak korzystanie z zewnętrznych bibliotek, po-
nieważ we framework ten wbudowany jest bardzo elastyczny mechanizm pozwalający
na stylowanie kontrolek – umożliwiając tym samym łatwą zmianę ich wyglądu, a także
– w pewnym zakresie – również zachowania.
13/ www.programistamag.pl / WSTĘP DO WPF – CZĘŚĆ 2: STYLOWANIE KONTROLEK W WPF Listing 3. Bezpośrednie zaaplikowanie styluRozwiązanie to w praktyce pojawia się stosunkowo rzadko, ponieważ jest mało praktyczne – po pierwsze, równie dobrze możemy przypisać wartości do odpowiednich własności modyfikowanej kontrolki bez zastosowania sty- lu, a po drugie przypisanie takiego stylu do wielu kontrolek skończy się ko- piowaniem dużych ilości kodu. Znacznie lepszym rozwiązaniem jest umiesz- czenie stylu w zasobach i odwołanie się do niego przy pomocy rozszerzeń StaticResource lub DynamicResource. Listing 4. Zaaplikowanie stylu osadzonego w zasobach (...)Teraz jest znacznie lepiej – kod stał się znacznie bardziej czytelny i elastyczny,
a efekt końcowy pozostał niezmieniony.
Rysunek 3. Style zaaplikowane do etykiet
ZAKRES DZIAŁANIA
WPF traktuje style znajdujące się w zasobach w różny sposób, w zależno-
ści od tego, czy jest do nich przypisany jakiś klucz (x:Key). W poprzednim
przykładzie styl został umieszczony w zasobach pod kluczem SimpleStyle,
w efekcie czego zostanie on zastosowany tylko do tych kontrolek, które jaw-
nie sobie tego zażyczą (poprzez ustawienie stylu na {StaticResource
SimpleStyle} ). Jeżeli natomiast zapiszemy w zasobach styl, nie podając
żadnego klucza, będzie on automatycznie aplikowany do wszystkich kontro-
lek, które dziedziczą z klasy określonej jako TargetType.
Aby zrozumieć dokładnie, jak działa ten mechanizm, trzeba wiedzieć, w
jaki sposób WPF poszukuje zasobów. Kiedy kontrolka próbuje dostać się do
zasobów (na przykład przez StaticResource albo DynamicResource, ale
nie tylko), WPF sprawdza najpierw, czy nie ma takiego zasobu przywiązanego
do samej kontrolki; jeśli jest, to zostanie on użyty. W przeciwnym wypadku
WPF przenosi się do kontenera, na którym położona jest kontrolka, i tam po-
szukuje danego zasobu, i tak dalej – aż osiągnie szczyt hierarchii. Jeśli i tam nie
odnajdzie zasobu, przeszukiwane są zasoby aplikacji (umieszczone w pliku
App.xaml), a potem – jeśli zaistnieje taka potrzeba, zasoby systemowe.
Mechanizm ten działa również dla stylów. Przyjrzyjmy się uważnie poniż-
szym przykładom:
Styl osadzony w zasobach aplikacji zadziała dla każdej kontrolki:
Listing 5. Styl umieszczony w zasobach aplikacji (...)Styl umieszczony w zasobach kontenera wpłynie na wszystkie elementy znaj-
dujące się wewnątrz niego:
Listing 6. Styl umieszczony w zasobach kontenera Jeżeli natomiast kontrolka ma osadzony styl w swoich zasobach i zgadza się
on z jej typem, to właśnie on zostanie zaaplikowany:
Listing 7. Listing osadzony w zasobach kontrolkiZauważmy, że WPF po odnalezieniu stylu właściwego dla kontrolki przerywa
dalsze poszukiwania. Dlatego też ostatnia i przedostatnia etykieta nie jest po-
chylona (stałoby się tak, gdyby zaaplikowany został styl znajdujący się w za-
sobach aplikacji). Jeśli jednak opisany efekt jest pożądany, w deklaracji stylu
można użyć atrybutu BasedOn, wskazując styl, który chcemy zmodyfikować.
Rysunek 4. Zakres działania stylu
TEMPLATE
Wśród różnych własności kontrolek, które możemy ustawić przy pomocy
stylu, jedna zasługuje na szczególną uwagę, a jest nią własność Template.
Zawiera ona bowiem instancję klasy ControlTemplate, która definiuje wy-
gląd kontrolki. Każda standardowa kontrolka ma własną ControlTemplate
zapisaną w bibliotekachWPF, ale programista każdą z nich może zastąpić wła-
snym szablonem.
14 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Przyjrzyjmy się zatem dokładnie następującemu fragmentowi kodu: Listing 8. Zamiana szablonu kontrolki Po uruchomieniu programu zauważymy, że etykieta otrzymała czarną ramkę.
Spróbujmy dojść teraz, w jaki sposób wpłynęliśmy na nią stylem, by osiągnąć
zamierzony efekt.
Wygląd etykiety zmienił się oczywiście w efekcie przedefiniowania wartości
własności Template. Wstawiona tam instancja ControlTemplate pozwoliła
zdefiniować, w jaki sposób kontrolka zostanie wyświetlona w oknie. Zauważmy
namarginesie,żepodczaskorzystaniazControlTemplatemusimyokreślić,jaki
typ jest przez ten szablon opisywany. Warto również zadbać o to, byśmy przy-
padkiem podczas definiowania zawartości szablonu nie skorzystali z kontrolki,
którą właśnie stylujemy, ponieważ może się to skończyć przepełnieniem stosu,
gdy WPF będzie w nieskończoność próbował nałożyć na kontrolkę przygotowa-
ny przez nas styl. Poza wspomnianym ograniczeniem wewnątrz ControlTem-
plate możemy korzystać praktycznie ze wszystkich dostępnych kontrolekWPF.
Spośród wszystkich elementów, których możemy użyć wewnątrz szablo-
nu, jeden zasługuje na szersze zainteresowanie, a mowa o obiekcie o nazwie
ContentPresenter.
Label jest kontrolką, której zawartość definiuje się poprzez własność Con-
tent. Każda kontrolka tego typu (na przykład Button) umożliwia wstawienie
dojejwnętrzanietylkotekstu,aledowolnegoinnegoobiektu–wtymrównież
zagnieżdżonych kontrolek. Kiedy projektujemy szablon dla etykiety, musimy
również zachować tę funkcjonalność – w przeciwnym razie nie będziemy w
stanie wyświetlić jej zawartości we właściwy sposób. Z pomocą przychodzi tu
wtedy klasa ContentPresenter, której zadaniem jest wyświetlenie danych
przekazywanych poprzez własność Content. Tym sposobem nie musimy się
martwić, w jaki sposób wyświetlić ciąg znaków, a jak zagnieżdżone kontrolki:
ContentPresenter zrobi wszystko za nas.
Pojawia się tylko jeszcze jeden kłopot: musimy ustawić własność Con-
tent obiektu ContentPresenter na taką wartość, jaka w przyszłości zosta-
nie przypisana do etykiety. Aby to osiągnąć, korzystamy z rozszerzenia Tem-
plateBinding. Zapis:
Zwróćmy uwagę na specyficzny zapis settera modyfikującego własność
ControlTemplate. Ponieważ własność Template jest bardziej złożona niż
Foreground, jej wartości nie da się zapisać w prosty sposób w atrybucie.
Dlatego też konieczne jest zastosowanie alternatywnego zapisu – we-
wnątrz settera wstawiamy węzeł Setter.Value i dopiero tam definiujemy
nową wartość własności Template. Rozwiązanie takie można zastosować
dla własności każdej klasy, którą umieścimy w XAMLu.
Listing 9. Wiązanie z własnością stylowanej kontrolkiI faktycznie – od teraz kolor ramki pasuje do koloru tekstu w etykietach.
Listing 11. Ostylowane kontrolki Rysunek 5. Etykiety z własnym szablonem
CZĘŚCI STAŁE
Zaczynamy powoli mieć wystarczająco dużo narzędzi, aby zrealizować pier-
wotny pomysł z estetycznym polem tekstowym, ale wciąż pozostaje problem
standardowej funkcjonalności, którą musimy w naszej kontrolce zachować.
Okazuje się jednak, że również i ten problem da się stosunkowo łatwo roz-
wiązać. WPF określa bowiem dla każdej standardowej kontrolki pewne specjal-
ne elementy, które reprezentują specyficzne dla nich zachowania.W przypadku
pola tekstowego elementem takim jest ScrollViewer, któremu należy nadać
nazwę PART_ContentHost. W przypadku każdej kontrolki są to inne elemen-
ty, ale na szczęście wszystkie opisane są na odpowiednich stronach MSDN.
Spróbujmy teraz przygotować szablon dla własnego pola tekstowego.
Listing 12. Szablon dla pola tekstowego
15/ www.programistamag.pl / WSTĘP DO WPF – CZĘŚĆ 2: STYLOWANIE KONTROLEK W WPF Po wstawieniu pola tekstowego do okna zobaczymy efekt: Rysunek 6. Pole tekstowe z prostym szablonem Możemy teraz zmodyfikować nasz szablon tak, aby wprowadzić tekst w tle oraz dodatkową ikonę (poniżej sama definicja szablonu): Listing 13. Szablon dla pola tekstowego z filtremFilter Rysunek 7. Prawie gotowe!
No proszę – jesteśmy bardzo blisko osiągnięcia zamierzonego celu, choć
widać, że konieczne będzie wprowadzenie jeszcze pewnych zmian, bo choć
pole tekstowe wygląda tak, jak chcieliśmy, to nie zachowuje się jeszcze zgod-
nie z naszymi oczekiwaniami: podpowiedź w tle jest widoczna cały czas, a
powinna przecież znikać w momencie, gdy zaczniemy wprowadzać tekst.
TRIGGERY
Z opisu brakującej funkcjonalności wynika jasno, że będziemy musieli do na-
szej kontrolki wprowadzić trochę logiki. Jednak mimo iż wydaje się, że bez
fragmentu kodu w C# nie uda nam się już obejść, WPF i w tym miejscu staje
na wysokości zadania.
ControlTemplate udostępnia bowiem mechanizm triggerów pozwalają-
cych na wprowadzenie do niej pewnej szczątkowej logiki. Każdy trigger składa
się z dwóch kluczowych elementów: warunku oraz zbioru setterów. Jego dzia-
łanie jest bardzo proste – jeżeli warunek jest spełniony, settery zostają zaapliko-
wane, w przeciwnym zaś wypadku – nie (oznacza to, że jeśli warunek w pew-
nym momencie jest spełniony, a potem przestanie być, to wszystkie własności
modyfikowane triggerem wrócą do swoich poprzednich wartości).
W naszym przypadku będziemy testować własność Text. Jeśli będzie
ona pusta, tekst podpowiedzi będzie wyświetlony, a w przeciwnym wypadku
– wygaszony.
Aby opisać tę zależność, będziemy potrzebowali najpierw dostępu do na-
mespace'a System, w którym znajduje się klasa string:
Listing 14. Wprowadzenie dodatkowego namespace'aAby trigger zadziałał prawidłowo, musimy wprowadzić też kilka zmian do szablo-
nu. Pierwszą z nich będzie nazwanie komponentu TextBlock wyświetlającego
podpowiedź, byśmy potem mogli się do niego odwołać z poziomu triggera. Po-
nadto domyślnie ustawimy jego widoczność na Hidden, by później przy pomocy
triggera w odpowiednich okolicznościach ustawić ją z powrotem na Visible:
Listing 15. Ukrycie tekstu „Filter”Filter Teraz wprowadzamy do ControlTemplate dodatkową sekcję:
Listing 16. Triggery szablonu(...) I voila! Cel został osiągnięty!
Rysunek 8. Działa!
CO DALEJ?
WPF daje bardzo dużo możliwości stylowania kontrolek. W niniejszym ar-
tykule opisałem, w jaki sposób można przygotować szablony dla prostych
kontrolek typu Label czy TextBox. W następnej części postaram się zaś
opowiedzieć o zaawansowanych technikach stylowania pozwalających przy-
gotowywać style dla kontrolek wyświetlających dane (na przykład listy).
Wojciech Sura wojciechsura@gmail.com
Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.
16 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Karol Rogowski KRÓTKIE SPOJRZENIE WSTECZ Zanim przejdziemy do właściwej części artykułu, chciałbym wykonać gest do- brej woli w stronę osób, które nie miały okazji zapoznać się z numerem 10/2013 magazynu Programista. Zawiera on pierwszą część artykułu „ASP.NET SignalR – czyli aplikacje czasu bardzo rzeczywistego”. Można było w niej znaleźć zaim- plementowany serwer, z którego będzie korzystał tworzony dzisiaj klient. Nie będę oczywiście tego tu dokładnie opisywał. Niemniej jednak komentarze pre- zentowanego kodu powinny wystarczyć do zrozumienia rozwiązania. Listing 1. Serwer do rozwiązania, nad którym będziemy pracować using System.Threading.Tasks; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; ////// Klasa Hub-a naszego chat-u
/// [HubName("ExampleChat")]
public class ChatHub : Hub
{
/// /// Metoda pozwalająca na wysłanie wiadoności do wszystkich
użytkowników
/// /// Nazwa użytkownika/// Treść wiadomościpublic void SendMessage(string name, string message)
{
var msg = string.Format("{0}: {1}", name, message);
Clients.All.newMessage(msg);
}
/// /// Metoda pozwalająca na połączenie do jakiegoś pokoju
/// /// Nazwa użytkownika/// Nazwa pokoju, do którego chcemy się
podłączyćpublic void JoinRoom(string name, string room)
{
Groups.Add(Context.ConnectionId, room);
var msg = string.Format("{0} joined room: {1}", name, room);
Clients.Group(room).newMessage(msg);
}
/// /// Metoda pozwalająca na wysłanie wiadoności do użytkowników w
danym pokoju
/// /// Nazwa użytkownika/// Nazwa pokoju/// Treść wiadomościpublic void SendMessageToRoom(string name, string room, string
message)
{
var msg = string.Format("{0} to room {2}: {1}", name, message,
room);
Clients.Group(room).newMessage(msg);
}
/// /// Nadpisane zdarzenie na łączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisuje public override Task OnConnected()
{
this.SendMonitoringData("Połączono", Context.ConnectionId);
return base.OnConnected();
}
/// /// Nadpisane zdarzenie na rozłączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisuje public override Task OnDisconnected()
{
this.SendMonitoringData("Rozłączono", Context.ConnectionId);
return base.OnDisconnected();
}
/// /// Nadpisane zdarzenie na ponowne połączenie się z Hub-em
/// /// Obiekt odpowiedni dla metody, którą nadpisuje public override Task OnReconnected()
{
this.SendMonitoringData("Połączono ponownie", Context.
ConnectionId);
return base.OnReconnected();
}
/// /// Metoda pozwalająca na informowanie użytkowników o
zdarzeniach występujących na Hub-ie
/// /// Rodzaj zdarzenia/// Identyfikator połączenia, które
wywołało to zdarzenieprivate void SendMonitoringData(string eventType, string
connection)
{
Clients.All.newEvent(eventType, connection);
}
}
CEL NA DZISIAJ
Na początek właściwego artykułu chciałbym powiedzieć, jaki jest nasz cel.
Rezultatem, do którego dążymy w tym momencie, jest stworzenie czatu. Ma
on być oczywiście oparty o technologię SignalR i ma działać w czasie rzeczy-
wistym. Efektem, jaki na koniec będziemy chcieli uzyskać, będzie możliwość
odbycia rozmowy między dwiema przeglądarkami.
BIERZMY SIĘ DO PRACY
– SZKIELET STRONY
Jako że bardzo mocno wierzę w to, że najlepiej jest uczyć się na przykładach,
to omawianie tworzonego rozwiązania zaczniemy od przedstawienia szkie-
letu strony. Nie będzie tu na razie żadnego prawdziwego programowania.
Będą jedynie poukładane elementy, na których będziemy potem pracować.
Zobaczmy więc, jak wygląda to od strony kodu
ASP.NET SignalR – czyli aplikacje
czasu bardzo rzeczywistego. Część 2
Artykuł ten jest drugą, i zarazem ostatnią, częścią omawiania rozwiązania, jakim
jest SignalR. W pierwszej części przeprowadziliśmy wprowadzenie do tej technolo-
gii, jak również stworzyliśmy własny Hub, który będzie pełnił rolę swoistego serwe-
ra. W tym artykule napiszemy klienta, który to będzie pozwalał nam na prowadze-
nie rozmowy. Oczywiście w czasie rzeczywistym.
17/ www.programistamag.pl / ASP.NET SIGNALR Listing 2. Szkielet strony internetowej, na której będziemy pracować
title>Example ChatChat Programisty
Użytkownik:Pokój:
Wiadomość:
18 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Po uruchomieniu powyższego polecenia, w miejscu, w którym znajduje się narzędzie, powinniśmy odnaleźć plik server.js. Musi zostać on dodany do naszego rozwiązania. W moim przypadku trafił on do folderu Script. Nie jest to oczywiście żadne wymaganie, tylko moja preferencja co do organizacji pli- ków. Zajrzyjmy więc do środka naszego świeżutkiego serwera. Listing 3. Zawartość pliku Server.js /*! * ASP.NET SignalR JavaScript Library v1.0.0 * http://signalr.net/ * * Copyright Microsoft Open Technologies, Inc. All rights reserved. * Licensed under the Apache 2.0 * https://github.com/SignalR/SignalR/blob/master/LICENSE.md * */ /// /// (function ($, window) {
/// "use strict";
if (typeof ($.signalR) !== "function") {
throw new Error("SignalR: SignalR is not loaded. Please ensure
jquery.signalR-x.js is referenced before ~/signalr/hubs.");
}
var signalR = $.signalR;
function makeProxyCallback(hub, callback) {
return function () {
// Call the client hub method
callback.apply(hub, $.makeArray(arguments));
};
}
function registerHubProxies(instance, shouldSubscribe) {
var key, hub, memberKey, memberValue, subscriptionMethod;
for (key in instance) {
if (instance.hasOwnProperty(key)) {
hub = instance[key];
if (!(hub.hubName)) {
// Not a client hub
continue;
}
if (shouldSubscribe) {
// We want to subscribe to the hub events
subscriptionMethod = hub.on;
}
else {
// We want to unsubscribe from the hub events
subscriptionMethod = hub.off;
}
// Loop through all members on the hub and find client hub
functions to subscribe/unsubscribe
for (memberKey in hub.client) {
if (hub.client.hasOwnProperty(memberKey)) {
memberValue = hub.client[memberKey];
if (!$.isFunction(memberValue)) {
// Not a client hub function
continue;
}
subscriptionMethod.call(hub, memberKey,
makeProxyCallback(hub, memberValue));
}
}
}
}
}
$.hubConnection.prototype.createHubProxies = function () {
var proxies = {};
this.starting(function () {
// Register the hub proxies as subscribed
// (instance, shouldSubscribe)
registerHubProxies(proxies, true);
this._registerSubscribedHubs();
}).disconnected(function () {
// Unsubscribe all hub proxies when we "disconnect". This
is to ensure that we do not re-add functional call backs.
// (instance, shouldSubscribe)
registerHubProxies(proxies, false);
});
proxies.ExampleChat = this.createHubProxy('ExampleChat');
proxies.ExampleChat.client = { };
proxies.ExampleChat.server = {
joinRoom: function (name, room) {
return proxies.ExampleChat.invoke.
apply(proxies.ExampleChat, $.merge(["JoinRoom"],
$.makeArray(arguments)));
},
sendMessage: function (name, message) {
return proxies.ExampleChat.invoke.apply(proxies.
ExampleChat, $.merge(["SendMessage"],
$.makeArray(arguments)));
},
sendMessageToRoom: function (name, room, message) {
return proxies.ExampleChat.invoke.apply(proxies.
ExampleChat, $.merge(["SendMessageToRoom"],
$.makeArray(arguments)));
}
};
return proxies;
};
signalR.hub = $.hubConnection("/signalr", { useDefaultPath:
false });
$.extend(signalR, signalR.hub.createHubProxies());
}(window.jQuery, window));
Powyżej widzimy serwer kliencki, który został właśnie wygenerowany. Pole-
cam dokładne zapoznanie się z kodem. My nie będziemy tu omawiać całości,
a jedynie elementy, które uważam za najbardziej istotne. Będą to oczywi-
ście metody, które pochodzą z naszego serwerowego Hub-a. Zauważmy, że
wszystkie trzy z napisanych przez nas metod zostały przypisane do właściwo-
ści proxies.ExampleChat.server. Zauważmy też, że każda z tych metod
ostatecznie tak naprawdę wysyła wiadomość z odpowiednimi parametrami.
Co do reszty kodu, to naprawdę polecam zapoznanie się z nim w celu zaob-
serwowania dobrych praktyk tworzenia kodu.
POSKŁADAJMY WSZYSTKO RAZEM
Wszystkie potrzebne nam elementy mamy już przygotowane. To, co nam te-
raz pozostaje, to złożyć je w jedną działającą całość. Tak jak już wspomniałem,
naszym celem będzie utworzenie chatu umożliwiającego komunikację mię-
dzy przeglądarkami. Aby tego dokonać, musimy przygotować odpowiedni
skrypt kliencki, który obsłuży wymagane funkcjonalności.
Listing 4. Część skryptowa strony chatu
20 / 2 . 2014 . (21) / BIBLIOTEKI I NARZĘDZIA Rysunek 5. Efekt rozmowy między dwoma prezentowanymi oknami Na Rysunku 5 przedstawiłem efekt - krótkie rozmowy między dwoma okna- mi. Jak widać, wykorzystano w nich zarówno możliwość odzywania się do wszystkich, jak i wyłącznie do pokojów, do których jesteśmy podłączeni. Na głównym widoku widzimy oczywiście informacje na temat prowadzo- nej rozmowy. Poniżej mamy dodatkowo informacje o tym, co działo się jakby „w tle”na serwerze. Są to informacje dotyczące tego, że ktoś się podłączał czy też rozłączał. Efektu, który jest widoczny na pierwszy rzut oka, nie będziemy dalej omawiać, a skupimy się bardziej na tym, co nie jest widoczne na pierw- szy rzut oka, czyli na ruchu sieciowym i tym, co tam właściwie tak naprawdę się działo. W moim przypadku monitorowanie ruchu sieciowego było włączone w przeglądarce, w której pisaliśmy, jako użytkownik Adam. Rzućmy na po- czątku okiem na ogólny obraz tego, co się działo. Rysunek 6. Ogólny obraz ruchu sieciowego Jak widać, zarejestrowany ruch sieciowy jest bardzo skromny. Ja ze swojej strony chciałbym zwrócić uwagę na dwa elementy. Pierwszym z nich będzie moment, w którym nasza aplikacja„negocjuje”z serwerem. Rysunek 7. „Negocjacja” aplikacji Pojęcie„negocjacji”, jeżeli mówimy o programowaniu, występuje dość często. Najczęściej, tak jak również w tym przypadku, definiuje ono sposób połącze- nia między dwoma elementami. Widzimy, że mamy ustalony między innymi identyfikator połączenia czy też token, który będzie w trakcie działania uży- wany. Następnie widzimy pozostałe parametry charakteryzujące nawiązywa- ne właśnie połączenie. Drugim krokiem, na który chciałbym zwrócić uwagę, jest moment faktycz- nego nawiązania połączenia. Rysunek 8. Podłączenie się aplikacji Rysunek 8 przedstawia moment, w którym aplikacja faktycznie nawiązuje połączenie. Warto jest w tym momencie zwrócić uwagę na to, na jaki adres wysyłane jest nasze żądanie. Jest on odpowiadający temu, co zostało zaak- ceptowane w procesie negocjacji. PODSUMOWANIE W powyższym artykule przedstawiłem, jak przy pomocy technologii SignalR stworzyć własny prosty chat. Uważam, że jak na otrzymany efekt, to ilość wło- żonej pracy, jak i skomplikowanie projektu były bardzo niskie. Warto wiedzieć, że omawiana technologia nie ogranicza się jedynie do rozwiązań webowych. Możemy dzięki niej przesyłać sygnały między aplikacjami niemal każdego typu. Pomimo tego, że prezentowany przykład był relatywnie prosty, zachęcam do głębszegozapoznaniasięzprezentowanątechnologią.Możeonabowiemsłużyć do tworzenia rozwiązań o wiele większych i o wiele bardziej skomplikowanych. W przypadku jakichkolwiek uwag, pytań lub propozycji pomysłów na ko- lejne artykuły proszę pisać na karol.rogowski@gmail.com lub szukać mnie na https://twitter.com/KarolRogowski. Karol Rogowski karol.rogowski@gmail.com Absolwent Informatyki Politechniki Białostockiej. Microsoft Certificated Professional Deve- loper .NET Web Developer. Obecnie pracuje jako Development Lead w firmie DevCore.Net. Autor wielu treści na portalach technologicznych, głównie związanych z HTML 5 i JavaScript. W wolnym czasie pokerzysta amator
22 / 2 . 2014 . (21) / JĘZYKI PROGRAMOWANIA Tomasz Nurkiewicz J ava 8 wprowadza najwięcej nowości od czasu piątej edycji wydanej bli- sko 10 lat temu. W tym artykule przyjrzymy się najciekawszym i najbar- dziej rewolucyjnym zmianom. Java z nudnego i rozwlekłego języka ma szansę ponownie stać się popularnym, wygodnym i ciekawym narzędziem dla nowych aplikacji. Java nadal ustępuje nowoczesnym językom na JVM, nie- mniej jednak znacząco skraca ten dystans. DOMYŚLNE METODY W INTERFEJSACH Do czasu Javy 8 istniał jasny podział na interfejsy - jedynie deklarujące meto- dy bez żadnej implementacji - oraz klasy abstrakcyjne. Aby zachować zgod- ność wsteczną, Oracle postanowiło wprowadzić tzw. metody domyślne w interfejsach. Teraz możemy dołączyć do dowolnej metody interfejsu także implementację, którą klasa implementująca dany interfejs może, ale nie musi przesłonić (ang. override): Listing 1. Przykład metod domyślnych w interfejsach public interface Encrypter { byte[] encode(byte[] bytes); default byte[] encodeStr( String s, Charset charset) { return this.encode(s.getBytes(charset)); } default byte[] encodeChars( char[] chars, Charset charset) { final String s = String.valueOf(chars); return this.encodeStr(s, charset); } } W powyższym przykładzie interfejs deklaruje trzy metody encode*(), ale dwie z nich posiadają implementacje opatrzone modyfikatorem default (znanym z poprzednich wersji Javy w innym kontekście). Klasa implementująca taki interfejs musi zaimplementować tylko jedną metodę, a nie wszystkie trzy: Listing 2. Klasa implementująca interfejs z metodami domyślnymi public class RotEncrypter implements Encrypter { @Override public byte[] encode(byte[] b) { final byte[] result = new byte[b.length]; for (int i = 0; i < b.length; ++i) { result[i] = (byte) (b[i] + 13); } return result; } } Java 8 – najbardziej rewolucyjna wersja w historii Częstotliwość wydawania nowych wersji języka Java pozostawia wiele do życzenia. JDK 6 oraz 7 były też pewnym rozczarowaniem ze względu na małą innowacyjność i opóźnienia. Java 8, spodziewana już w połowie marca, ma szansę radykalnie od- mienić dotychczasowe postrzeganie tej dość leciwej platformy. Wyrażenia lambda, współbieżne kolekcje czy zupełnie nowe API do obsługi czasu przybliżają ten jeden z najpopularniejszych języków programowania do konkurencji. Klasa RotEncrypter implementuje tylko jedną metodę, a pozostałe są zaimplementowane na jej bazie. Oczywiście nadal istnieje możliwość prze- słonięcia pozostałych metod interfejsu – ale nie ma już takiego obowiązku. W poprzednich wersjach Javy z reguły w takim przypadku tworzyliśmy zwy- kły interfejs Encrypter oraz pomocniczą klasę (np. abstract class Ab- stractEncrypter implements Encrypter) implementującą wszystkie opcjonalne metody. Trudno nie odnieść wrażenia, że interfejsom w Javie 8 dużo bliżej do cech (ang. traits) w Scali, chociaż oczywiście nie są one aż tak rozbudowane i elastyczne. Powyżejprzedstawionojednozmożliwychzastosowańmetoddomyślnych. Ich pierwotnym przeznaczeniem było jednak zachowanie wstecznej zgodno- ści przy dodawaniu nowych metod d o istniejących interfejsów. W przeszłości operacja taka natychmiast skutkowała błędem kompilacji (w końcu stary kod nie mógł implementować nieistniejącej natenczas metody), teraz wystarczy dostarczyć domyślną implementację nowych metod. Z tej cechy skorzystali twórcy, pisząc nową metodę java.util.Collection.stream() – o której w szczegółach za chwilę. Uważni czytelnicy zastanawiają się zapewne, jak Java radzi sobie z konflik- tami – gdy ma do dyspozycji więcej niż jedną implementację pochodzącą z wielu interfejsów bazowych, które implementuje? Listing 3. Konflikt domyślnych metod - kod nie kompiluje się! interface Car { default void fuel() {} } interface Boat { default void fuel() {} } //Nie kompiluje się! class Hovercraft implements Car, Boat { } Klasa Hovercraft implementuje dwa interfejsy, z których oba dostarczają nie tylko deklarację, ale również implementację metody fuel(). Prowadzi to do konfliktu, którego kompilator nie jest w stanie rozwiązać. Na szczęście istnieje składnia pozwalająca explicite wywołać dowolną spośród skonflikto- wanych metod lub dostarczyć trzecią, niezależną wersję: Listing 4. Rozwiązywanie konfliktów metod domyślnych class Hovercraft implements Car, Boat { @Override public void fuel() { Car.super.fuel(); } }
23/ www.programistamag.pl / JAVA 8 – NAJBARDZIEJ REWOLUCYJNA WERSJA W HISTORII TYPY FUNKCYJNE Użytkownicy bibliotek Guava (http://code.google.com/p/guava-libraries) lub Apa- che Commons Collections (http://commons.apache.org/proper/commons-lang) znają od dawna interfejsy Predicate czy Function. Są to próby
symulowania programowania funkcyjnego w Javie przed wersją 8. Pierwszy
z wymienionych interfejsów koncepcyjnie deklaruje jedną metodę, która
dla dowolnej instancji typu T udziela binarnej odpowiedzi true lub false.
Z kolei Function to interfejs, którego jedyna metoda pobiera jako
argument obiekt typu T, a zwraca obiekt typu R – w domyśle dokonuje
pewnej transformacji jednego obiektu w drugi. Te dość prymitywne próby
umożliwiały implementację funkcji wyższego rzędu na kolekcjach, takich jak
filter() czy map(). Java 8 wprowadza podobne typy do biblioteki standar-
dowej w pakiecie java.util.function:
Listing 5. Przydatne metody używające typów Function i Predicate
import java.util.function.Function;
import java.util.function.Predicate;
class FunctionalTypes {
public static Iterable filter(
Iterable in, Predicate pred) {
ArrayList out = new ArrayList<>();
for (T elem : in) {
if(pred.test(elem)) {
out.add(elem);
}
}
return out;
}
public static Iterable map(
Iterable in, Function fun) {
ArrayList out = new ArrayList<>();
for (T elem : in) {
out.add(fun.apply(elem));
}
return out;
}
}
Klasa FunctionalTypes definiuje w oparciu o wbudowane typy Predi-
cate i Function dwie niezwykle przydatne funkcje pomocnicze:
filter() i map(). Programiści znający jakikolwiek język funkcyjny skojarzą
je bez trudu. Dla tych z doświadczeniem jedynie w Javie śpieszę z tłumacze-
niem: filter() przyjmuje jako argument kolekcję pewnego typu i zwraca
kolekcję tego samego typu, ale tylko z elementami spełniającymi określone
kryterium. Z kolei map() przyjmuje kolekcję typu T (np. String) oraz funk-
cję z typu T w R (np. biorącą String, a zwracającą jego długość typu Inte-
ger). W rezultacie otrzymujemy kolekcję tej samej długości, ale na miejscu
każdego elementu wejściowego znajduje się ten element po zaaplikowaniu
przekazanej funkcji. Te narzędzia są dość trudne do zrozumienia, ale przykład
powinien wiele rozjaśnić:
Listing 6. Przykład użycia interfejsów Predicate oraz Function
List months = Arrays.asList(
"styczeń", "luty",
"marzec", "kwiecień",
"maj", "czerwiec",
"lipiec", "sierpień",
"wrzesień", "październik",
"listopad", "grudzień");
Iterable monthsWithR =
FunctionalTypes.filter(months,
new Predicate() {
@Override
public boolean test(String month) {
return month.contains("r");
}
});
Iterable monthLengths =
FunctionalTypes.map(months,
new Function() {
@Override
public Integer apply(String month) {
return month.length();
}
});
Pierwszy blok kodu wybiera spośród wszystkich nazw miesięcy tylko te, któ-
rych nazwy zawierają literkę „r“. Drugi przykład tworzy nową kolekcję, której
każdy element odpowiada ilości liter w nazwie odpowiadającego miesiąca.
Bezsprzecznie kod powyżej jest zbyt rozwlekły i nieczytelny, aby używać go w
codziennej pracy. Jednak standardowe typy Function, PredicateorazSupplier(funkcjabezargumentówzwracającawartośćtypuT)iCon-
sumer (funkcja pobierająca wartość typu T, ale nic niezwracająca) odgrywa-
ją kluczową rolę w implementacji wyrażeń lambda w nowej Javie.
Nim jednak przejdziemy do wyrażeń lambda, warto zwrócić uwagę na
nową adnotację @FunctionalInterface. Podobnie jak znana od dawna
@Override sygnalizuje ona jedynie kompilatorowi nasze zamiary. Adnotujemy
nią interfejs posiadający tylko jedną niezaimplementowaną metodę (i dowolną
ilość metod domyślnych, patrz: początek artykułu). Jeśli kiedykolwiek w tym in-
terfejsie pojawi się kolejna metoda bez definicji, kompilator zasygnalizuje błąd.
Poniżej przykład poprawnego interfejsu funkcyjnego z samego JDK:
Listing 7. Przykład interfejsu funkcyjnego
package java.util.function;
@FunctionalInterface
public interface Consumer {
void accept(T t);
default Consumer andThen(Consumer super T> after) {
//...
}
}
Dlaczego tak dużą wagę przykłada Java do interfejsów posiadających tyl-
ko jedną niezaimplementowaną metodę? Otóż instancje takich interfejsów
można automatycznie zamienić na wyrażenia lambda.
WYRAŻENIA LAMBDA
W Javie 8 każdy interfejs deklarujący tylko jedną metodę bez implementacji
(przykładami są także znane od lat java.lang.Runnable czy java.awt.
event.ActionListener)możnazdefiniować,korzystajączeznacznieuprosz-
czonej składni. Wszystkie poniższe wyrażenia są semantycznie równoważne:
Listing 8. Różne sposoby definiowania wyrażeń lambda
//A:
filter(months,
new Predicate() {
@Override
public boolean test(String month) {
return month.contains("r");
}
});
//B:
Predicate predicate =
(String month) -> month.contains("r");
filter(months, predicate);
//C:
filter(months,
(String month) -> month.contains("r"));
//D:
filter(months, m -> {
String needle = "r";
return m.contains(needle);
});
//E:
filter(months, m -> m.contains("r"));
24 / 2 . 2014 . (21) / JĘZYKI PROGRAMOWANIA Przykład A jest nam już znany. W przykładzie B tworzymy instancję imple- mentującą typ Predicate, posługując się zupełnie nową składnią
w Javie 8 do definiowania wyrażeń lambda. Przed strzałką (->) umieszczamy
w nawiasie typy i nazwy parametrów funkcji. Po strzałce znajduje się definicja
funkcji. Jeśli na definicję składa się tylko jedno wyrażenie, możemy opuścić sło-
wo kluczowe return. Przykład C nie różni się niczym od poprzedniego, jednak
w tym wypadku umieściliśmy wyrażenie lambda bezpośrednio w miejscu użycia.
Przykład D obrazuje, jak zdefiniować wyrażenie lambda składające się
z wielu instrukcji. Tym razem konieczne są nawiasy klamrowe i słowo kluczo-
we return. Z kolei przykład E jest najbardziej zwięzły, pozwalamy bowiem
kompilatorowi na odgadnięcie typu (ang. type inference) zmiennej m z kon-
tekstu. Powyższe bloki kodu obrazują, jak bardzo zmieniła się składnia Javy
w wersji 8 (oczywiście pozostaje kompatybilna wstecznie). Jakby tego było
mało, istnieje jeszcze zwięźlejsza forma definiowania lambd w sytuacjach, gdy
widoczna jest metoda bądź konstruktor o sygnaturze pasującej do wyrażenia:
Listing 9. Wyrażenia lambda oparte o metody i konstruktory
//A:
map(months, month -> month.length());
map(months, String::length);
//B:
List primes = Arrays.asList("2", "3", "5");
map(primes, Integer::parseInt);
//C:
map(primes, p -> new Integer(p));
map(primes, Integer::new);
Składnia String::length jest dokładnym odpowiednikiem wyrażenia:
(String s) -> s.length() - czyli oznacza wywołanie metody na ar-
gumencie funkcji. Z kolei, mimo identycznej składni, Integer::parseInt
odpowiada wywołaniu statycznej metody klasy Integer: (String s) ->Integer.parseInt(s). Z reguły kompilator potrafi rozróżnić, który rodzaj wy-
rażenia lambda mieliśmy na myśli. Wreszcie pozostała składnia Integer::new,
którejodpowiadaniecodłuższe:(String s) -> new Integer(s)(nawiasem
mówiąc, wołanie konstruktora klasy Integer nie jest zalecane).
Ostatnim szczególnym przypadkiem definiowania wyrażeń lambda jest
wołanie metod bieżącej instancji klasy:
Listing 10. Wołanie metody bieżącej instancji
Iterable parseNums(List inputs) {
return map(inputs, this::parseStr);
}
Integer parseStr(String s) {
return Integer.parseInt(s);
}
this::parseStr to skrócona wersja od t -> this.parseStr(t). Natu-
ralnie możemy również definiować wyrażenia lambda o więcej niż jednym
parametrze (obie poniższe definicje są równoważne):
Listing 11. Wyrażenie lambda o więcej niż jednym parametrze
BiFunction fun =
(String s, Integer x) ->{return s.length() > x;};
BiFunction fun2 =
(s, x) -> s.length() > x;
Bezparametrowe wyrażenie lambda wymaga pustej pary nawiasów przed
strzałką:
Listing 12. Bezparametrowe wyrażenie lambda
new Thread(() -> handle(request)).start();
Ostatni przykład pokazuje nie tylko, jak łatwo stworzyć nowy wątek bez
konieczności żmudnego implementowania interfejsu Runnable. Warto też
zwrócić uwagę na fakt, że interfejs ten istniał jeszcze przed Javą 8. Mało tego,
każdy interfejs spełniający kryteria @FunctionalInterface można zaim-
plementować przy użyciu wyrażenia lambda! Interfejs taki dobrze jest ozna-
czyć przez @FunctionalInterface, ale nie jest to konieczne:
Listing 13. Typ użytkownika zastąpiony wyrażeniem lambda
interface MySocketHandler {
void handle(Socket socket);
}
//...
MySocketHandler handler = (Socket socket) -> {
System.out.println("Connect: " + socket);
//...
};
STRUMIENIE
Strumienie (java.util.stream.Stream, nie mylić ze strumieniami z pa-
kietu java.io) to zupełnie nowy (chociaż z całą pewnością nie nowatorski)
sposób pracy z kolekcjami w Javie, możliwy dzięki wprowadzeniu wyrażeń
lambda. Dla każdej kolekcji możemy stworzyć odzwierciedlający ją strumień
(przypomina iterator, lecz dużo potężniejszy) i wykonać na takim obiekcie
szereg transformacji, takich jak map() czy filter(). Strumienie mają dwie
niezwykle ważne cechy: po pierwsze dopóki nie zapragniemy otrzymać koń-
cowego wyniku bądź kolekcji, wszystkie operacje są leniwe, tzn. strumień je
zapamiętuje, ale wstrzymuje się z ich wykonaniem. Po drugie żadna operacja
na strumieniu nigdy nie modyfikuje oryginalnej kolekcji.
W Listingu 5 zaimplementowaliśmy ręcznie metody map() i filter().
Całe szczęście JDK 8 dostarcza gotowe implementacje:
Listing 14. Najprostsze przykłady użycia strumieni w Javie 8
import java.util.stream.Collectors;
//...
List monthsWithR2 = months.
stream().
filter(month -> month.contains("r")).
collect(Collectors.toList());
List monthLengths = months.
stream().
map(String::length).
collect(Collectors.toList());
Pierwszym, co rzuca się w oczy, jest wywołanie zupełnie nowej metody
stream(). Jest ona dostępna dla każdej kolekcji typu T i zwraca Stream.
Obiekt ten, jak nietrudno się domyśleć, udostępnia znane nam już metody
map() i filter(), oraz wiele innych. Ponieważ strumienie nie modyfikują
oryginalnej kolekcji, na koniec musimy zdecydować, do jakiego typu kolekcji
chcemy zebrać (ang. collect) rezultaty. Klasę Collectors poznamy za chwilę.
Dzięki obecności strumieni, które są swoistym „leniwym widokiem“ na
kolekcję, możemy wywołać łańcuch operacji bez obaw, że każda operacja
cząstkowa będzie budowała nową kolekcję od zera. Dopiero wywołanie tzw.
terminalnej operacji uruchamia wszystkie zdefiniowane transformacje - i tyl-
ko dla tych elementów, które faktycznie znajdą się w rezultacie:
Listing 15. Wiele operacji na jednym strumieniu
Iterable monthLengths =
months.
stream().
filter(month -> month.contains("r")).
map(String::length).
distinct().
limit(2).
collect(toList());
25/ www.programistamag.pl / JAVA 8 – NAJBARDZIEJ REWOLUCYJNA WERSJA W HISTORII Powyższy przykład pokazuje, jak można łączyć wiele operacji na jed- nym strumieniu. Najpierw wyszukujemy miesiące posiadające w nazwie literę „r“. Potem zamieniamy nazwy miesięcy na ich długości. distinct() usuwa ze strumienia duplikaty, a limit(2) ogranicza wynikową kolekcję do dwóch pierwszych rezultatów. Co ciekawe, dopiero ostatnie wywołanie Collectors.toList() zaczyna iterować po wejściowej kolekcji. Gdyby nie ono, wejściowa kolekcja w ogóle nie zostałaby odczytana, a koszt całego blo- ku kodu byłby niemal zerowy, niezależnie od wielkości kolekcji. Oprócz collect() istnieją inne operacje terminalne powodujące iterację po kolekcji. Należą do nich m.in. min() i max() (znalezienie minimalnej/maksy- malnej wartości w kolekcji, także w oparciu o wskazany Comparator), count() czy all-/none-/anyMatch() - które sprawdzają, czy odpowiednio wszystkie, żadne lub jakiekolwiek elementy kolekcji spełniają podane kryterium (predykat). Aby oswoić się nieco ze strumieniami, napiszmy prosty program, który znajdzie litery niewystępujące w nazwie żadnego miesiąca. W tym celu najpierw tworzy- my syntetyczny strumień kodów ASCII liter od A do Z. Potem zamieniamy go na strumień znaków. W następnym kroku filtrujemy ten strumień, pozostawiając tylko te, które nie pojawiają się w nazwie żadnego miesiąca (stąd noneMatch()): Listing 16. Litery niewystępujące w nazwie żadnego miesiąca Set chars = IntStream.
rangeClosed((int) 'a', (int) 'z').
mapToObj(ascii -> (char) ascii).
filter(ch ->months.stream().noneMatch(
m -> m.indexOf(ch) >= 0)
).
collect(toSet());
Nie jest to rozwiązanie optymalne pod względem obliczeniowym, ale pozostaje
ciekawym przykładem możliwości strumienie w Javie 8. A’propos w nazwach
polskich miesięcy nie występują ani razu: b, f, h, q, v i x. Ale właściwie nie dowie-
dzieliśmy się jeszcze, jak wykonać dowolną operację na każdym elemencie ko-
lekcji, jak np. wypisanie na ekran czy zapis do pliku. W tym celu niezwykle przy-
datny okazuje się operator forEach(), świetnie zastępujący tradycją pętlę for:
Listing 17. Operator forEach():
for (String month : months) {
System.out.println(month);
}
months.stream().forEach(m -> {
System.out.println(m);
});
months.stream().forEach(System.out::println);
months.forEach(System.out::println);
Każde z powyższych wywołań jest równoważne: wypisze na ekran po kolei
nazwy wszystkich miesięcy. W przeciwieństwie do poznanych wcześniej ope-
ratorów, forEach() jako jedyny można wykonać bezpośrednio na kolekcji,
a nie tylko na strumieniu.
Ostatnia uwaga do strumieni: są one z natury ulotne i jednorazowe. Nie-
dopuszczalne jest utworzenie strumienia i używanie go wielokrotnie. Na
przykład poniższy kod przy powtórnym wykonaniu collect() rzuci Ille-
galStateException: stream has already been operated upon
or closed. Strumienie należy traktować podobnie do iteratorów - raz prze-
czytane nie mogą być ponownie użyte. Z tego powodu lepiej nigdy nie prze-
chowywać ani nie przekazywać instancji obiektu typu Stream, a jedynie
używać ich w miejscu utworzenia:
Listing 18. Próba ponownego użycia strumienia
Stream s = months.stream().
filter(m -> m.contains("r"));
s.forEach(System.out::println);
s.collect(toList()); //wyjątek!
RÓWNOLEGŁE STRUMIENIE
Trudno nie zauważyć, że strumienie oferują bardzo deklaratywny sposób pra-
cy z kolekcjami. W przeszłości transformacja, filtrowanie czy wyszukiwanie
wymagały ręcznej iteracji po kolekcji. Teraz, gdy te zachowania są zamknięte
w ściśle zdefiniowane wysokopoziomowe metody na strumieniach, twórcy
Javy pokusili się o zoptymalizowanie wielu operacji pod kątem wielowątko-
wości. Tak powstały równoległe strumienie. Nie różnią się pod względem API
niczym od standardowych strumieni, natomiast starają się wykonać więk-
szość operacji na wielu rdzeniach/wielu procesorach.
Listing 19. Równoległe strumienie
Iterable monthLengths =
months.
parallelStream().
filter(month -> month.contains("r")).
map(String::length).
distinct().
limit(2).
collect(toList());
Jedyna różnica w stosunku do normalnych strumieni polega na zamianie
wywołania stream() na parallelStream(). Od tej pory Java będzie pró-
bowała wykonać większość operacji, dzieląc wejściową kolekcję na mniejsze
części i przetwarzając je równolegle. Dla przykładu JVM podzieli kolekcję na
kilka mniejszych zakresów, przefiltruje każdy z osobna na innym rdzeniu/
procesorze, po czym połączy rezultaty. Należy jednak pamiętać, że niektó-
re operacje będą zachowywały się odmiennie dla równoległych strumieni.
Np. o ile kolejność iteracji w forEach() jest przewidywalna w normalnych
strumieniach (od pierwszego do ostatniego elementu), o tyle równoległy
strumień ma prawo wykonać blok kodu wewnątrz forEach() w dowolnej,
nieokreślonej kolejności.
Inną pułapką jest brak kontroli nad pulą wątków używaną przez
parallelStream(). Okazuje się bowiem, że wszystkie równoległe strumie-
nie w całej JVM (a zatem np. we wszystkich aplikacjach wdrożonych jednocze-
śnie na tym samym serwerze aplikacyjnym) współdzielą jedną, globalną pulę
wątków. Jest ona dostępna również dla nas poprzez wywołanie java.util.
concurrent.ForkJoinPool.commonPool(). Pula ta ma domyślnie tyle
wątków, iloma rdzeniami/procesorami dysponujemy. Ilość tą można zmienić,
ale również jedynie globalnie dla całej maszyny wirtualnej - korzystając ze
zmiennej środowiskowej: -Djava.util.concurrent.ForkJoinPool.
common.parallelism=50 (gdzie 50 to pożądana liczba wątków).
KOLEKTORY (COLLECTOR)
Dotychczas widzieliśmy zaledwie dwa kolektory: toList() i toSet() do-
stępne poprzez klasę java.util.stream.Collectors. Wiemy też, że
jest to sposób zgromadzenia wyników przetwarzania strumienia z powro-
tem do kolekcji pożądanego typu. Kolektory są jednak na tyle silną abstrak-
cją, że warto poznać inne dostępne implementacje, jak również nauczyć
się, jak tworzyć własne. Na początek poznajmy kolektor toMap(), który dla
każdego elementu ze strumienia wykonuje dwie funkcje: pierwsza wylicza
klucz w mapie, a druga - wartość. Zbiór takich par tworzy mapę. Załóżmy
na przykład, że chcemy skonstruować mapę, w której kluczami będą nazwy
miesięcy pisane wielkimi literami, a wartościami - ilość wystąpień litery „e“
w nazwie:
Listing 20. Przykład użycia Collectors.toMap()
Map monthByLen = months.
stream().
collect(
toMap(
String::toUpperCase,
m -> StringUtils.countMatches(m, "e")
));