C# Księga Przykładów (2006) - Jones Allen [wersja 11 MB] [Sharp]

526 Pages • 184,357 Words • PDF • 11.1 MB
Uploaded at 2021-09-24 18:12

This document was submitted by our user and they confirm that they have the consent to share it. Assuming that you are writer or own the copyright of this document, report to us by using this DMCA report button.


Spis treści Wstęp 1

.

.

.

.

.

.

.

.

.

.

Tworzenie aplikacji

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

ix

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

1

1.1 Tworzenie aplikacji trybu tekstowego ... .... .... . ... .. ....... . . . . .. . .. . ..... ... . 2 1.2 Utworzenie aplikacji Windows ...............................................

.

4

1.3 Utworzenie i wykorzystanie modułu kodu .. ... . .. . .. ..... . .... ... .... ... .. ...... 7 1.4 Utworzenie biblioteki kodów i korzystanie z niej ..... .. . . ... ...... .. ... . .. .. ....... 9 1.5 Dostęp do argumentów linii poleceń . . . .......... ...... ... ..... .. . . ... ... ..... 1O 1.6 Selektywne włączanie kodu podczas jego budowy. ....... ..... .. ... ........... ... 12 1.7 Dostęp do elementu programu o tej samej nazwie, co słowo kluczowe .. ... ..... . .. .. . 15 1.8 Tworzenie i zarządzanie parami kluczy silnej nazwy .. .. . .. .. .. . .. ... . .. . ......... . 16 1.9 Nadawanie asemblacji silnej nazwy .... ................. ............ .. ..... 18 .

.

.

1.1O Sprawdzenie, czy asemblacja o silnej nazwie nie została zmodyfikowana... .. .... ..... 19 1.1 1 Opóźnienie podpisu asemblacji ...... ...... .. .. ... . .. . .. ... ...... ...... ..... 20 1.12 Podpisywanie asemblacji podpisem cyfrowym Authenticode ........ .. . . . ......... 22 1.13 Utworzenie testowego certyfikatu wydawcy oprogramowania i obdarzenie go zaufaniem .. 25 1.14 Zarządzanie globalnym buforem asemblacji (GAC). ...... . ..... . ... .. . ..... . .. ... 26 1.15 Ochrona przed dekompilacją kodu przez osoby postronne .. .. . ... ... ....... ...... . 27

2

Praca z danymi

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

29

2.1 Efektywne manipulowanie zawartością łańcuchów . . ... . .. ... .. . ... ........ . ... . . 29 2.2 Przekodowanie łańcucha za pomocą innego schematu kodowania znaków . ............ 3 1 2.3 Konwersja podstawowych typów wartości na tablicę bajtową ....................... 3 4 2.4 Kodowanie danych binarnych jako tekstu.......................... ............ 36 .

2.5 Testowanie poprawności danych wejściowych przy użyciu wyrażeń regularnych ......... 38 2.6 Wykorzystywanie skompilowanych wyrażeń regularnych.................... ...... 4 1 .

2.7 Tworzenie daty i czasu z łańcucha ......................... .... ......... ..... 4 3 .

2.8 Dodawanie, odejmowanie i porównywanie dat i czasów......... ................... 44 2.9 Sortowanie tablicy lub wykazu tablic ArrayUst ................................... 4 6 2.1O Kopiowanie kolekcji do tablicy ....... ................................. ..... 47 2.1 1 Tworzenie kolekcji elementów jednolitego typu ......... _

.

·„

.· .

.

.

:.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

. „

48

2.12 Zapamiętywanie serializowanego obiektu w pliku ... : ............................ 50

3

Domeny aplikacji, odbicie i metadane

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.· .

.

·.

53

3.1 Tworzenie domeny aplikacji . . . .. .. ... .. ... . .. . ... . ........... . .. . ...... ..... 53 3.2 Przekazanie obiektów poza granice domeny aplikacji ..... ......................... 55 3.3 Unikanie ładowania zbędnych asemblacji do domen aplikacji ........................ 56 3.4 Utworzenie typu, który nie może przekroczyć granic domeny aplikacji ........ .. . ...... 57 3.5 Ładowanie asemblacji do aktualnej domeny aplikacji .............................. 58 3.6 Wykonanie asemblacji w innej domenie aplikacji ................... ............. 60 .

3.7 Utworzenie instancji typu w innej domenie aplikacji ..... . ......... . .............. 62 .

Ili

C#

lv

-

Księga przykładów

3.8 Przekazywanie danych między domenami aplikacji

.

3.9 Zwalnianie asemblacji i domen aplikacji

.

.

.

.

.

.

.

.

.

.

..... ........ ... .. ..... ...... . 6 5 .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

67

3.1O Uzyskiwanie informacji o typie. ........ . ... ..... .. ... ... ... .. . ... .. .... .. .. 6 8 .

3.1 1 Testowanie typu obiektu

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

70

3.12 Tworzenie instancji obiektu przy użyciu odbicia . . .. ... . .. ... . . .... . ... ... ... .. .. 7 1 3.1 3 Tworzenie niestandardowego atrybutu ....... . .. .. . .... . . .. ...... ... ... ... ... . 7 4 3.14 Sprawdzenie atrybutów elementu programu przy użyciu odbicia ... ... . ... .. . ... .... 76

4

Wątki, procesy i synchronizacja

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

4.1 Wykonanie metody przy użyciu puli wątków

.

.

.

.

4.2 Asynchroniczne wykonywanie metody

.

.

.

.

.

.

.

.

.

.

4.3 Wykonanie metody przy użyciu timera

.

.

.

.

.

.

.

.

.

.

... .. .

.

.

.

.

.

.

.

.

.

. .. ..

.

.

. .

.

.

..

.

.

.

.

.

.

.

.

.

.

.

. . .

4.4 Wykonanie metody poprzez sygnalizację obiektu WaitHandle . 4.5 Wykonanie metody przy użyciu nowego wątku

.

.

.

.

.

.

.

.

.

.

.

.

... . .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

......

.

.. .

.

.

.

... .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

89

.

.

.

91

.

.

.

93

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Przetwarzanie XML

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

95

.

99

.

.

.

104

.

.

106

.

5

.

.

.

.

79

.

4.6 Nadzór nad wykonaniem wątku . . .... .. . ... . 4.7 Rozpoznanie momentu zakończenia wątku... . . . .. . 4.8 Synchronizacja działalności wielu wątków . .. ..... .. .. .. .. ........ ... ... .. ... . 4.9 Tworzenie instancji kolekcji przystosowanej do działania w trybie wielowątkowym .. 4.10 Zapoczątkowanie nowego procesu . . . 4.1 1 Zakończenie procesu .. .... ... .... ... ... .. ..... . ...... ... ... ... ... ... .. . 4.12 Ograniczenie równoczesności działania do jednej tylko instancji aplikacji . .. ... .... .. . .

79

.. . 82

.

.

.

.

.

100

10 8 111

113

5.1 Przedstawienie struktury dokumentu XML w postaci drzewa .. . .... ... ... ..... ... . 1 14 .

5.2 Wstawianie węzłów do dokumentu XML

.

.. ..... .... .. ... ... ... .. ... ... . . . ... . 1 17

5.3 Szybkie dołączanie węzłów do dokumentu XML .. ...... ... ... ........ ... . .. .. .. 1 19 5.4 Odnajdywanie określonych elementów przy pomocy nazwy. . .. ... ... ...... . .. .. .. 12 1 .

5.5 Odczytywanie węzłów XML w określonej przestrzeni nazw XML. . ... ... ..... ... ... . 12 3 .

5.6 Odnajdywanie elementów poprzez przeszukiwanie XPath .. ... ... ... ..... ... ... ... . 124 5.7 Odczyt i zapis XML bez ładowania do pamięci całego dokumentu

.

.

.

.

.... . .

.

.

. . .

.

.

.

.

.

127

5.8 Sprawdzenie zgodności dokumentu XML ze schematem . ..... ...... ......... .. .. 1 3 0 .

5.9 Serializacja XML przy pomocy obiektów niestandardowych . .... .. .... . ........ .. . 1 3 5 .

5.1O Tworzenie schematu dla klasy .NET

.

. .. .... .. ... .. . ... ..... ... ... . .. . .. . ... 1 3 8

5.11 Generowanie klasy ze schematu . . .. ... .. ... ... ... ... ..... . .. ... ... ... .... 1 3 9 .

5.12 Wykonywanie transformacji XSL .... . . ...... ... ... ... .......... . ...... . ... 1 3 9 .

6

Formularze Windows

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

6.1 Programowe dodawanie kontrolki ........... . ... ... ........................ ...... .. . . . 6.3 Przetwarzanie wszystkich kontrolek formularza . ... .. . . 6.4 Śledzenie widocznych formularzy w aplikacji .... .. .. ..... . .. . . . . .. .. . ......... . 6.5 Znajdowanie wszystkich formularzy podrzędnych MDI .... . 6.6 Zapamiętywanie rozmiaru i położenia formularza ... . . .. . . . .. . 6.7 Wymuszenie przewijania okna listy ....... .............. . ... ..... ..... ... ... 6 8 Ograniczenie zawartości okna tekstowego do wejściowych danych numerycznych . . ... 6 .9 Wykorzystanie okna autouzupelniania typu combo . . .. . .

6.2 Przypisanie danych do kontrolki . .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

. ·.

.

.

.

.

.

.

.

.

.

.

.

.

145

.

.

.

147

.

.

.

15 0

.

15 1

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

14 4

.

.

.

143

.

.

.

14 8

15 3 15 4 15 5

v

Rozdział

6.1O Sortowanie zawartości listy według dowolnej kolumny .... ..... ................. 158 6.1 1 Przypisanie menu kontekstowego do kontrolki .......... ..... ... .. ..... .. . .. ... 160 6.12 Wykorzystywanie części głównego menu do menu kontekstowego . ............... . 16 1 6.13 Tworzenie formularza wielojęzycznego. .... . .. .. .. . . .. .. ..... .. .. . . . .. ..

.

.

.. . 16 3

6.14 Tworzenie formularzy nieruchomych ...... .. ...... . . .... . . ... ...... .. ....... 165 6.15 Przekształcenie formularza bez ramki w obiekt ruchomy ..... ............ . ....... 166 6.16 Tworzenie animowanej ikony paska systemowego. ...... ... .... ... . ..... ....... 168 6.17 Test kontrolki wejściowej. ... . ... . . .. . ... .. .. .... ... .. ... ... ... .... . ... .

.

.

169

6.18 Wykonanie operacji przeciągania i upuszczania .... . .. .... . .... . ..... .. .... ... . 17 1 6.19 Używanie pomocy kontekstowej .. ... . ..... .. .. .. ......... .... . ... ......... 173 6.20 Zastosowanie stylu Windows XP do kontrolek ....... .. ..... ........ .. ........ . 174

7

ASP.NET i formularze Web

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

177

7.1 Przekierowanie użytkownika na inną stronę .... ... .. ..... ... ....... .... .... .. .. 178 7.2 Zapamiętywanie informacji między zgłoszeniami .. .... ... .. . .... . . . .... .. . . .... 179 .

7.3 Tworzenie zmiennych członkowskich strony z informacją o stanie . .. .. . ... .. .. .... .. 18 3 7.4 Odpowiedż na zdarzenia po stronie klienta w języku JavaScript .. .. .... .. . . .. .. ... .. 185 7.5 Pokazanie wyskakującego okna przy użyciu JavaScript . .. . . . .. .. . . . ... . . . . . . . .. .. 187 7.6 Programowe ustawianie fokusu kontrolki . . ... .... . ... .... . . . . .. ... . ... ... . . . . 188 .

7.7 Umożliwienie załadowania pliku przez użytkownika . ... .. . .... . . . . .... . .. .... . .. . 18 9 7.8 Wykorzystanie uwierzytelnienia llS ....... ...... . .. ... . .. .. . ... . .. .... . . . .. .. 192 7.9 Użycie uwierzytelniania opartego o formularze.. ... ... .. .. .. ....... .. ... .. . . .. .. 196 7.1O Wykonanie selektywnych testów poprawności danych wejściowych .... ............ 199 7.1 1 Dynamiczne dołączanie kontrolek do formularza Web ... ....... ...... .. . .... . . .. 201 7.12 Dynamiczne renderowanie obrazu . .. ..... . . . ..... . ...... ....... . . ...... .. .. 203 7.13 Programowe ładowanie kontrolek użytkownika.... . .. .. ..... .. .. .. ... ..... .. ... 207 7.14 Wykorzystywanie buforowania stron i fragmentów. .. . .. .. ......... .. ... ... . .... 2 1 1 7.15 Powtórne użycie danych z bufora ASP.NET . ........ ........... .. .. ... ... . .... 2 12 7.16 Włączanie wykrywania i usuwania błędów (debugging) strony Web .. .. . ... .. . . .. ... 2 15 7.17 Zmiana zezwoleń dla kodu ASP.NET . . . . . . . . . .. . . .... . ............. ..... .... 2 18

8

Grafika, multimedia I drukowanie

.

.

.

.

.

.

.



.

.

.





.

.



.

.



"



.

.



.

.









221

8.1 Odnajdywanie wszystkich zainstalowanych czcionek .. .... ... ..... ... .... .... .... 222 8.2 Wykonanie testu trafienia dla konturów ... .. . .. .. ... ... .... .. ... . . .. .. ... .... 224 .

8.3 Utworzenie kontrolki o nieprostokątnym kształcie. ... ..... .. . ...... .. ..... .. . .... 227 8.4 Utwórz ruchomego „duszka" .. . . .. .. ... . . .. . .. ... . . . .. . .... .. .. .... .... .. .. 229 8.5 Utworzenie obrazu dającego się przewijać .. ....... ..... ..... .... .. .... .. ... .. 2 3 2 .

8.6 Wykonanie zrzutu ekranu ... ... . .. .. . .... .. . . . . ... . . . . . ... .. . . . .. .. ...... . 2 3 3 .

8.7 Zastosownie podwójnego buforowania w celu przyspieszenia przerysowywania . .... ... 2 35 8.8 Pokazanie miniatur obrazów . ... . . ...... ... .... .. ....... ......... ....... ... 2 38 8.9 Wygenerowanie prostego dżwięku 'beep' ..... .... .... ...... ... ..... . ... .. . . . . . 2 3 9 8.1O Odtworzenie pliku WAV lub MP3 ..................... ...................... 240 8.1 1 Przedstawienie animacji z DirectShow . .. ... ... .. .. .. ..... ... ... . .... ... . ... . 242 8.12 Wyszukiwanie informacji o zainstalowanych drukarkach . .... . ...... ..... .... .... 245 8.13 Drukowanie prostego dokumentu . ...... ....... ......... ...... ...... .. ..... 247 8.14 Drukowanie dokumentu wielostronicowego . .... ... ... . ... .. ...... .. . . . . . . . . . . 249

C#

vl

-

Księga przykładów

8.15 Drukowanie sformatowanego tekstu . ... . .. . . .... . . . . . . . ... . . . . . . .. . . .. . . . . 252 .

9

8.16 Wyświetlenie dynamicznego podglądu wydruku

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

254

8.17 Zarządzanie zadaniami drukowania

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

257

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Pliki, katalogi i operacje wejścia/wyjścia (1/0)

.

.

.

.

9.1 Odczytywanie informacji o pliku lub katalogu

.

.

.

.

.

.

.

.

.

.

.

.

9.2 Ustawienie atrybutów pliku i katalogu

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

9.3 Kopiowanie, przenoszenie i usuwanie pliku lub katalogu.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

9.4 Obliczanie rozmiaru katalogu ...

. .

.

.

.

.

.

.

.

.

.

. . .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

265

.

.

.

.

.

.

266

.

.

.

.

.

.

269

.

.

.

.

270

. . . .

.

.

.

261

.

.

.

26 2

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

9.6 Wyświetlenie drzewa katalogów na bieżąco w kontrolce TreeView

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

271

.

.

.

.

.

.

.

.

.

.

.

.

.



.

.

.

.

274

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

276

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

277

.

.

.

.

.

.

.

280

.

.

.

.

.

.

.

28 1 283

.

.

.

.

.

9.7 Odczytywanie i zapisywanie pliku tekstowego

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

9.8 Odczytywanie i zapisywanie pliku binarnego

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

. .

9.9 Asynchroniczny odczyt pliku

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

9.1o Wyszukanie plików odpowiadających wyrażeniu zawierającemu znaki zastępcze 9.1 1 Testowanie identyczności dwóch plików

.

.

.

.

.

.

.

.

.

.

9.12 Posługiwanie się łańcuchami reprezentującymi nazwy plików

.

.

.

9.13 Określenie, czy ścieżka dotyczy katalogu, czy pliku

.

.

.

.

.

.

.

.

.

.

9.14 Praca ze ścieżkami względnymi .

.

.

.

.

.

.

.



.

.

.

.

.

.

.

9.15 Utworzenie pliku tymczasowego

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

...

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

. . .

.

.

.

.

.

.

.

.

.

.

.

284

.

.

.

.

.

.

.

.

.

.

.

285

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

286

9.16 Odczytanie wielkości całkowitej wolnej przestrzeni dyskowej

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

287

9.17 Wyświetlenie wspólnych plikowych okien dialogowych . 9.18 Używanie izolowanego magazynu

.

.

.

.

.

.

.

9.19 Monitorowanie zmian w systemie plików

.

.

.

.

.

.

.

9.20 Dostęp do portu COM

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Dostęp do baz danych

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

...

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

288

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

291

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

293

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

295

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

297

10.1 Połączenia z bazą danych

.

. ..... . . . . . . . . . . . . . . . . . . . . . . . . . . ... .. . . . . . . . .. . 298

10.2 Tworzenie pul połączeń

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

10.3 Wykonanie polecenia SQL lub procedury składowanej

.

.

.

.

.

.

..

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

10.4 Wykorzystanie parametrów w poleceniu SQL lub procedurze składowanej 10.5 Przetwarzanie wyników zapytania SQL przy użyciu czytnika danych

.

.

10.6 Uzyskanie dokumentu XML w zapytaniu SQL

.

.

.

.

. . .

.

.

.

.

10.7 Wyszukiwanie wszystkich instancji SQL Server 2000 w sieci

11

.

.

.

9.5 Wyszukiwanie informacji o wersji pliku

1O

.

.

Programowanie sieciowe i międzysieciowe 1 1. 1 Pobranie pliku poprzez HTIP

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

1 1.2 Pobranie pliku I przetwarzanie go przy użyciu strumienia .

. . .

.

.

.

. ... . 300 .

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

303

.

.

.

.

.

3 06

.

.

.

309

. . .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

3 12

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

3 15

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

317 3 18

.

.

.

.

.

.

..

.

.

.

.

.

.

.

.

.

.. 3 19

1 1.3 Uzyskanie strony HTML z witryny wymagającej uwierzytelnienia

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

32 1

1 1.4 Wyświetlenie strony Web w aplikacji Windows .

.

.

.

.

.

.

.. . .

.

.

.

.

.

.

.

.

322

.

.

.

.

.

.

.

. .. .

.

.

.

.

.

1 1.5 Uzyskanie adresu IP aktualnego komputera . ... . . . . . .. . .. . . . . .. . . . . .. . . . . . . . . . 325 1 1.6 Uzyskanie nazwy hosta dla adresu IP

.

.

.

1 1.7 Pingowanie adresu IP

.

.

.

.

.

.

.

.

1 1.8 Komunikacja przy użyciu TCP . .

...

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

...

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

. .. ...

.

.

.

.

.

.

.

.

...

1 1.9 Uzyskanie adresu IP klienta z gniazda połączenia

.

.

.

.

.

.

.

.

.

...

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

3 26

.

.

.

.

.

.

.

.

.

.

.

327

.

.

.

.

.

.

.

.

.

.

.

3 30

.

.

.

.

.

.

.

.

.

3 34

..

1 1.1O Ustawienie opcji gniazda . . . . ... ... .. . .. .. . . . .. ... .. . . . . . . .. . . . .. . . . . . . . 3 36 .

vii

Rozdział

1 1.11 Utworzenie wielowątkowego serwera T CP ............ .. ... ..... ............ 3 37 1 1.12 Asynchroniczne używanie T CP ...................... ... ........ ..... ..... 3 39 1 1.13 Komunikacja przy użyciu UDP .. .. ............. ........ ..... ... .. .. ....... 3 4 2 1 1.14 Wysyłanie wiadomości e-mail poprzez SMT P ...... . ...... ....... ............ 3 45 1 1.15 Przesyłanie i odbieranie wiadomości e-mail za pomocą MAPI ............... ..... 3 46

12

Usługi Web XML I zdalny dostęp

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

349

12.1 Unikanie kodowania na stałe adresu URL usług XML Web ........... . .... ... ... .. 350 12.2 Wykorzystanie buforowania odpowiedzi usługi XML Web......................... 352 12.3 Buforowanie danych usługi XML Web ... ...... ......... ..... ... ...... .. ... .. 353 12.4 Utworzenie transakcyjnej metody Web .... ........ ............ .. .... ... .... . 355 12.5 Definiowanie poświadczeń dla usługi XML Web ................................ 357 12.6 Asynchroniczne wywołanie metody Web ..................................... 359 12.7 Przekształcenie obiektu na zdalny .................... ...... ..... ....... .... 36 1 12.8 Rejestracja wszystkich zdalnych klas zdefiniowanych w asemblacji ... ............. 365 .

12.9 Utrzymywanie zdalnego obiektu w llS ....................................... 367 12.10 Wyzwolenie zdarzenia przez zdalny kanał.......... ......................... 369 .

12.1 1 Kontrolowanie czasu życia zdalnego obiektu .. ...... . ..... .. ....... ...... . .. . 373 12.12 Zarządzanie wersjami zdalnych obiektów ................................. .. 37 4 .

12.13 Utworzenie metody jednokierunkowej korzystającej z usług XML Web lub zdalnego

dostępu ....... ....... ... ...... .......... ............... .... . . . . ......... 376

13

Bezpieczeństwo w czasie wykonania

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

379

13.1 Dopuszczenie częściowo zaufanego kodu do asemblacji o silnych nazwach .......... 380 13.2 Wyłączenie zabezpieczeń dostępu do kodu ................................... 382 13.3 Wyłączenie sprawdzania uprawnień do wykonania .............................. 384 13.4 Zagwarantowanie, że moduł runtime przyzna asemblacji specjalne uprawnienia ........ 385 13.5 Ogranicznie uprawnień przyznawanych asemblacji ............................. 387 13.6 Przeglądanie żądań uprawnień wykonywanych przez asemblację ................. . 389 .

13.7 Określenie posiadania uprawnień podczas wykonywania programu . ... .. .. .. .... ... 3 9 1 13.8 Ograniczenie prawa d o rozszerzenia klasy i nadpisywania jej pó l ................... 3 92 13.9 Przeglądanie ewidencji asemblacji .........................................

.

394

13.1O Posługiwanie się ewidencją podczas ładowania asemblacji ....... ......... . ..... 395 13.1 1 Modyfikowanie zabezpieczeń czasu wykonania przy użyciu ewidencji domeny aplikacji . 397 13.12 Posługiwanie się zabezpieczeniami czasu wykonania przy użyciu zasad zabezpieczeń

domeny aplikacji .. ....... ....... .... .................................. 400 13.13 Określenie, czy aktualny użytkownik jest członkiem danej grupy Windows ..... ..... 403 .

13.14 Ograniczanie użytkowników mających prawo do wykonania kodu ... ...... .... .... 406 13.15 Personifikacja użytkownika systemu Windows .................. ......... .. .. 4 1 1

14

Kryptografia

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

415

14.1 Utworzenie kryptograficznie losowego numeru ....................... ........ 4 16 .

14.2 Wyliczanie kodu hash dla hasta .. ......................................... 4 17 .

14.3 Wyliczanie kodu hash dla pliku ..... ...................................... 420 .

14.4 Weryfikacja kodu hash ..................................... .... ....... 4 2 1 .

.

14.5 Zapewnienie integralności danych przy użyciu kodu hash z kluczem................. 4 2 3

vlli

C#

-

Księga przykładów

14.6 Zabezpieczenie pliku przy użyciu symetrycznego szyfrowania ......... .. . .

14.7 Wyprowadzenie klucza szyfrowania symetrycznego z hasła ..... ... .

.

.

.

.

.

.

.

.

.

.

. 4 26

......... . 4 3 1 .

14.8 Bezpieczne wysyłanie utajnionych danych przy użyciu szyfrowania asymetrycznego .. .

14.9 Bezpieczne zapamiętywanie klucza szyfrowania asymetrycznego ... .

14.10 Bezpieczna wymiana symetrycznego klucza sesji ..... . ... .

15

Współdziałanie z niezarządzanym kodem

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

...... . ....... 4 4 1

.

.

.

.

.

.

.

.

.

.

.

.

.

.

15.2 Przejęcie uchwytu kontrolki, okna lub pliku ..

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

445

... 446

. ..... ............ ... .. .. .

433

.

15.1 Wywołanie funkcji z niezarządzanej biblioteki DLL .. ............... .. . 15.3 Wywołanie niezarządzanej funkcji wykorzystującej strukturę .

.

. ........... 4 38

.

.

448

... ..... . ......... 4 5 0 .

15.4 Wywołanie niezarządzanej funkcji posługującej się wywołaniem zwrotnym

... ... .

.

...........

.

454

15.6 Użycie komponentu COM w kliencie NET ...................................

.

4 56

15.5 Odczytywanie niezarządzanej informacji o błędach . .

.

....... ....

.

.

.

.

.

.

.

.

15.7 Szybkie zwolnienie komponentu COM .. .......... ....... .

15.8 Użycie parametrów opcjonalnych

.

.

... ..... .. . .

.

.

.

.

.

.

.

.

.

. ........... 4 5 8 .

.................. .. .. .

.

15.9 Użycie kontrolki ActiveX w kliencie NET .. .. ......... ............... .... .

.

45 3

.

.

.

.

458

.

460

15.1O Wykorzystanie komponentu .NET przez COM.... ...................... ..... 46 1 .

16

Powszechnie używane Interfejsy I wzorce

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

463

16.1 Implementacja serializowalnego typu.. .. ........ ..... ...... . ............ 46 4 .

.

.

.

16.2 Implementacja typu klonowalnego ............. .. . .. ... ...... .

.

.

.

.

.

.

........ 468

16.3 Implementacja typów porównywalnych ... ..... ......................... .

.

471

16.4 Implementacja typu wyliczalnego.

.

475

.

.

.

.

.

..... .. ..................... .

.

16.5 Implementacja klasy jednorazowego użycia ..

.

.

.

.

. .. .

.

.

.

.. . . . ...... . ....... ..... 480 .

.

.

.

.

.

.

16.6 Implementowanie typu formatowalnego ..... .... ....................... . .

.

.

16.7 Implementacja niestandardowej klasy wyjątku ... .

.

.

.

.

484

........................... 487

16.8 Implementacja niestandardowego argumentu zdarzenia ................. 16.9 Implementacja wzorca pojedynczej instancji ........

.

.

.

.

.

.

. . . . 491 .

.

.

... .................. 4 92 .

16.1O Implementacja wzorca obserwatora............. .......................... 4 9 3 .

17

Integracja z systemem Windows

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

499

17.1 Dostęp do informacji o środowisku wykonywczym .. . . .. . .... ........ ...... .... 5 00 17.2 Wyszukiwanie wartości zmiennej środowiska . .... ........ ........... ... .

.

.

.

.

17.3 Zapisanie zdarzenia do dziennika zdarzeń Windows . .................... .... .

.

.

503

.

5 04

17.4 Dostęp do rejestru Windows . ............. . ........ . ... .... .... . . . . ... . .. 5 06 .

17.5 Utworzenie usługi Windows ... ............... .

.

.

.

..... .. ....... . .. .. 5 10 .

17.6 Utworzenie instalatora usługi Windows .......... ... .

.

.

.

.

.

.

...................... 5 14

17.7 Utworzenie skrótu na pulpicie lub w menu Start.. .. ... . .. . ........ ... ... ... .... 5 16

Indeks

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

51 9

Wstęp Posługiwanie się aplikacjami .NET Framework firmy Microsoft w C# bardziej zależy od możli­ wości funkcjonalnego wykorzystania biblioteki klas .NET Framework, niż od znajomości jerzyka C#. Podręcznik C#: Księga przykładów wykorzystuje zasoby biblioteki klas .NET Framework i dostarcza określone rozwiązania, dotyczące tak podstawowych, jak i bardziej złożonych prob­ lemów programistycznych. Każde rozwiązanie (tutaj zwane przepisem) zostało przedstawione w formie opisu, zilustrowanego przykładami odpowiedniego kodu programu. Intencją podręcznika C#: Księga przykładów nie jest nauczanie programowania ani języka C#, ale jeśli czytelnik ma podstawowe doświadczenie w programowaniu aplikacji w środowisku .NET Framework przy użyciu jerzyka C#, niniejsza książka powinna mu posłużyć jako nieoce­ nione źródło informacji. Można w niej znaleźć gotowe rozwiązania problemów lub odpowiednie do tego wskazówki. C#: Księga przykładów może również służyć jako uzupełniające źródło wiedzy o bibliotece klas .NET Framework. Trudno stać się specjalistą od jerzyka C# i biblioteki klas .NET Framework wyłącznie na podstawie lektury: konieczne jest więc eksperymentowanie w praktyce i pisanie coraz co nowych, kolejnych programów. Struktura i zawartość niniejszej książki, jak również osadzenie rozwiązań w czasie rzeczywistym, oferują dobrą pozycję startową dla rozpoczęcia własnych prób.

Uwaga Cały zamieszczony w niniejszej książce kod został napisany i przetestowany w .NET Framework wersji 1.1. W wielu przypadkach przykładowe kody działają również w wersji 1.0, ale nie można udzielić gwarancji, gdyż nie zostały przetestowane.

Próbki kodu Sekwencje kodu dla wszystkich przepisów w C#: Księga przykładów są dostępne online, pod adresem http:l/microsoft.comlmspmslbooks/6456.asp. Aby pobrać przykładowe pliki, należy klik­ nąć łącze Companion Content w menu More Information, znajdującym się w prawej części ekranu. Nastąpi załadowanie strony Companion Content, zawierającej odsyłacz umożliwiający ściągnięcie tych plików. Aby zainstalować przykładowe pliki, należy kliknąć odsyłacz Download The Book's Sample Filcs i wykonać instrukcje w programie ustawień. Odsyłacz do przykłado­ wego kodu zostanie dołączony do menu Start. Kod jest zapisany jako zestaw rozwiązań i projektów Visual Studio .NET 2003, podzie­ lony na rozdziały i numery przepisów. Każdy rozdział stanowi oddzielne rozwiązanie, a prze­ pis - oddzielny projekt, w obrębie rozwiązania dla danego rozdziału. Niektóre z przepisów w rozdziałach 11 i 12, demonstrujących programowanie sieciowe, zawierają oddzielne projekty składowe dla klienta i serwera.

x

C#

-

Księga przykładów

Chociaż wszystkie przykłady są napisane jako projekty Visual Studio .NET, większość z nich zawiera pojedynczy plik źródłowy, który można skompilować i uruchomić niezależnie od Visual Studio .NET. Jeśli system Visual Studio .NET 2003 nie jest używany, można zlokalizować kod dla danego przepisu , dzięki poruszaniu się po strukturze katalogowej kodu z przykładu; np. aby znaleźć kod przepisu 4.3, należy przejrzeć katalog „Chapter04\Recipe04-03". Przy używaniu kompilatora wywoływanego z linii poleceń, należy pamiętać o włączeniu odniesień do wszyst­ kich wymaganych asemblacji biblioteki klas .NET. Niektóre przykładowe aplikacje wymagają dołączenia argumentów z linii poleceń. Zostaną one opisane w dalszej części przepisu. Przy użyciu Visual Studio .NET można wprowadzić te argumenty do właściwości projektu (pod węzłem debugowania

-

debugging

-

pozycji Con­

figuration Properties). Aby wejść w katalog lub wykorzystać nazwy plików zawierające spacje, należy całą nazwę ująć w cudzysłów. Do zainstalowania dwóch wirtualnych katalogów, użytych w przykładach dla rozdziałów

7 oraz 1 2, potrzebne są pewne dodatkowe kroki. Zostały one opisane w pliku tekstowym readrne.txt na stronie internetowej umożliwiającej ściągnięcie kodów źródłowych.

Wymagania systemu Aby uruchomić kod przykładów, należy dysponować następujacyrn oprogramowaniem: • Microsoft .NET Framework SOK, wersji 1.1 • Microsoft Visual Studio .NET 2003 (zalecany) • Microsoft Windows 2000, Windows XP lub Microsoft Windows Server 2003 • Microsoft SQL Server 2000 lub MSDE - dla przepisów w rozdziale 1O • Microsoft Internet Information Services (IIS) - dla niektórych przepisów w rozdziałach

7 i 12 Minimalna konfiguracja sprzętu: procesor klasy Pentium II 450 MHz, co najmniej 128 MB pamięci RAM przy pracy w systemie Microsoft Windows 2000 lub 256 MB przy pracy w syste­

mie Windows XP, Windows 2000 Server albo Windows Server 2003, przestrzeń dyskowa około 5 GB w celu zainstalowania Visual Studio .NET 2003. Wymienione parametry stanowią mini­

mum, jednak zwiększenie dostępnej pojemności dysku znacznie ułatwi projektowanie.

Uwaga

Wprawdzie treścią książki C#: Księga przykładów jest implementacja platformy

.NET Framework dla systemu Windows, równie ważne jest dostarczenie wszystkim pro­ gramistom C# niezbędnych zasobów informacji - niezależnie od wykorzystywanej przez nich platformy projektu i narzędzi. Większość umieszczonych tutaj przykładów działa we wszystkich implementacjach .NET; wyjątek stanowią te tematy, które nie zostały wdro­ żone na wszystkich platformach .NET, takich jak Windows Forms, ADO.NET i ASP.NET.

1 Tworzenie aplikacji Ten rozdział przybliża podstawowe czynności, niezbędne przy tworzeniu własnych programów napisanych w języku C#. Znajdują się tu przepisy dotyczące takich czynności, jak: • Budowa aplikacji trybu tekstowego oraz aplikacji postaci formularza Windows (przepisy

1. 1 i 1.2). • Tworzenie i użytkowanie modułów programowych i bibliotek (przepisy 1.3 i 1.4). • Dostęp z aplikacji do argumentów linii poleceń (przepis 1.5). • Wykorzystanie dyrektyw i atrybutów kompilatora do selektywnego włączania kodu podczas

budowy (przepis 1.6). • Dostęp do elementów programu, ucworwnych w innych językach, których nazwy kolidują

ze słowami kluczowymi języka C# (przepis 1.7). • Nadanie asembłacjom silnych nazw i weryfikacja asemblacji o silnej nazwie (przepisy 1.8,

1.9, 1 . 10 , i 1 . 1 1). • Podpisywanie asembłacji podpisem cyfrowym Microsoft Authenticode (przepisy 1.12

i 1. 13). • Zarządzanie asemblacjami o dzielonym dostępie, przechowywanymi w globalny buforze

asemblacji (GAC- Global Assembly Cache) (przepis 1. 14). • Ochrona przed osobami mogącymi zdekompilować asembłację (przepis 1. 15).

Uwaga Wszystkie narzędzia dyskutowane w niniejszym rozdziale są dostarczane łącznie z platformą Microsoft .NET Framework lub .NET Framework SDK. Narzędzia stanowiące część Framework znajdują się w głównym katalogu wersji Framework prze­ znaczonej dla czytelników niniejszej książki. Na przykład: po zainstalowaniu wersji .NET Framework 1 .1 w lokalizacji domyślnej, znajdą się w katalogu C:\WINDOWS\Microsoft. NEnFramework\v1 . 1 .4322. Proces instalacyjny .NET doda automatycznie ten katalog do ścieżki środowiska użytkownika.

Ciqg dalszy na nastrpnej stronie

2

C#

Księga przykładów

-

Cil/g dalszy z poprzedniej strony Narzędzia dostarczone z platformą SOK znajdują się w podkatalogu Bin - wewnątrz katalogu, w którym zostanie później zainstalowana platforma SOK. Ten katalog nie jest automatycznie dodawany do ścieżki dostępu; należy ręcznie edytować ścieżkę, aby mieć wygodny dostęp do narzędzi . Większość narzędzi umożliwia tworzenie zarówno długich, jak i skróconych formatów dla przełączników w linii poleceń, kontrolujących ich funkcjonalność. Niniejszy rozdział zawsze podaje długi format, który wymaga wprawdzie więcej pisania, ale niesie również więcej informacji. Skrócony format każdego przełącznika można znaleźć w dokumentacji narzędziowej w .NET Framework SOK.

1 .1 Tworzenie aplikacji trybu tekstowego Problem Chcesz zbudować aplikację, która umożliwia użytkownikowi wprowadzanie danych, ale nie wymaga graficznego interfejsu użytkownika Windows (GUI), a na tekstowej konsoli prezentuje wyniki działania i wyświetla znak zachęty.

Rozwiązanie Użyj w swojej implementacji metody

static

o nazwie

Main

z jednym z poniższych wpisów

w co najmniej jednym z twoich plików kodów źródłowych.

public static void Main ( ) ; public static void Main ( st ring [ ] a rg s ) ; public static int Main ( ) ; public static int Main ( st ring ( ] a rgs ) ; Podczas kompilacji należy użyć przełącznika ltarget:exe kompilatora C# (csc.exe).

Omówienie Domyślnie kompilator C# tworzy aplikację trybu tekstowego ( Consok application), nie jest więc konieczny przełącznik ltarget:exe. Użycie go może być przyda me, jeśli skrypty służące do budowy aplikacji mają być wykorzystywane wielokrotnie. Podane niżej przykłady przedstawiają klasę o nazwie

ConsokUtils (zasoby konsoli), zdefiniowaną w pliku o nazwie ConsoleUtils.cs:

u s ing System ; public class ConsoleUtil s {

li

Metoda wyświet lenia znaku zachęty i odczytu odpowiedzi z kon soli

public static st ring ReadSt ring ( st ring msg ) { Consol e . Write ( msg ) ;

Rozdział 1 : Tworzenie aplikacji

3

retu rn System . Consol e . Readline( ) ;

} li

Metoda wyświet lenia komunikatu w konsoli

public static void WriteSt ring(st ring msg ) { System . Console . Writeline(msg ) ;

} li

Główna metoda używana do testowania metod Con soleUt ility

public static void Main( ) { st ring name

li

=

ReadSt ring (" Plea se ent e r you r name :

"

);

Powitanie czyteln ika

WriteSt ring("Welcome to t he C# P rog ramme r ' s Cookboo k , " + name ) ; I

} }

Aby wbudować klasę ConsokUtils do aplikacji konsolowej o nazwie ConsoleUtils.exe, należy użyć polecenia csc /target:exe ConsoleUtils.cs. Wynikową asemblację wykonywalną można uruchomić bezpośrednio z linii poleceń. Metoda Main aplikacji ConsoleUtiłs.exe poprosi użyt­ kownika o wprowadzenie imienia, a następnie powita, jak pokazano niżej. Please ente r you r name : Rupe rt Welcome to the C# P rog rammer's Cookbook , Rupe rt

W rzeczywistości aplikacje rzadko składają się z pojedynczych plików źródłowych. Na przykład przytoczona tutaj klasa Hello World wykorzystuje klasę ConsokUtils do wyświetlenia polecenia „Hdlo, world" na konsoli. (Hello World jest przechowywane w pliku HelloWorld.es). public class HelloWo rld { public static void Main( ) { ConsoleUti l s . WriteSt ring( " Hello , wo rld" ) ;

} } Aby utworzyć aplikację konsolową złożoną z co najmniej dwóch plików kodu źródłowego, należy określić wszystkie pliki źródłowe jako argumenty kompilatora. Na przykład przytoczone niżej polecenie utworzy aplikację o nazwie MyFirstApp.exe z plików źródłowych HelloWorld.es i ConsoleUtils.cs. c s c /ta rget : exe /main : HelloWo rld /ou t : MyFirstAp p . exe HelloWo rl d . cs ConsoleUtil s . cs

Przełącznik /out pozwala określić nazwę skompilowanej asemblacji. Jeśli nie wstanie użyty, będzie nadana nazwa pliku zapisanego jako pierwszy - w danym przypadku HelloWorld.es. Ponieważ obie klasy: Hello World i ConsokUtils zawierają metody Main, kompilator nie może automatycznie określić, która metoda reprezentuje właściwy punkt wejściowy (entty point) asemblacj i. Należy użyć przełącznika kompilatora /main, aby zidentyfikować nazwę klasy zawie­ rającej właściwy dla aplikacji punkt wejściowy (entry point).

4

C#

-

Księga przykładów

1 .2 Utworzenie aplikacji Windows Problem Chcesz utworzyć aplikację wykorzystującą GUI (Graphical User Interface - graficzny interfejs użytkownika) w oparciu o formularze Windows.

Rozwiązanie Zastosuj metodę statyczną o nazwie Main w swoim pliku kodu źródłowego. Utwórz w metodzie

Main

System. Windows.Forms.Form Qest to główny formu­ static o nazwie Run klasy System. Windows.Forms.Application. Wykorzystaj przełącznik /target:winexe dla kompilatora C# instancję klasy, rozszerzającej klasę

larz twojej aplikacji). Przekaż ten formularz jako obiekt do metody (csc.exe) przy kompilacji swojej asemblacji.

Omówienie Między budową aplikacji z prostym interfejsem GUI dla systemu Windows a utworzeniem w pełni ukształtowanej aplikacji w oparciu o cen system istnieje ogromna różnica. Są jednak czynności, które trzeba wykonać bez względu na to, czy się pisze odpowiednik „Hello World" systemu Windows, czy opracowanie następnej wersji systemu Microsoft Word. Chodzi tu mię­ dzy innymi o następujące czynności: • Dla każdego formularza wymaganego w aplikacji należy utworzyć klasę, która obejmuje

również klasę

System. Windows.Forms.Form.

• W każdej z klas formularzy należy zadeklarować pola reprezentujące kontrolki, które

w danym formularzu wystąpią, np. przyciski, etykiety, listy i ramki tekstowe. Te pola powinny być zadeklarowane jako prywame

(private} łub co najmniej chronione (protected},

aby inne elementy programu nie miały do nich bezpośredniego dostępu. Dla ujawnienia metod lub właściwości kontrolek należy zaimplementować niezbędne pola w klasie formu­ larza, umożliwiając niebezpośredni, kontrolowany dostęp. • W klasie formularza należy zadeklarować metody zarządzające zdarzeniami wywoływa­

nymi przez kontrolki, takimi jak kliknięcie przycisku myszy lub wciśnięcie klawisza, kiedy

aktywnym elementem sterującym jest ramka tekstowa. Te metody powinny być prywatne

(private)

lub chronione

(protected)

i stosować się do standardu .NET

event pattern

(wzór

zdarzenia), opisanego w przepisie I 6.10. Dotyczy to metod, w których wstanie zdefinio­ wana funkcjonalność aplikacji (lub metod przez nie wywoływanych).

• Należy zadeklarować konstruktora dla klasy formularza, który nada instancje wszystkim

kontrolkom formularza i skonfiguruje ich stan początkowy (rozmiar, kolor, pozycję, zawar­ tość itd.). Konstruktor powinien także przypisać odpowiednie metody programu obsługi zdarzeń danej klasy do zdarzeń poszczególnych kontrolek. • Należy zadeklarować metodę statyczną

(static)

o nazwie

Main

-

zazwyczaj w postaci pola

klasy głównego formularza danej aplikacji. Ta metoda stanowi punkt wejściowy (entry point) aplikacji i jako taka może posiadać re same podpisy, co metody wymienione w przepisie I. I.

Rozdział 1 : Tworzenie aplikacji W metodzie

Main

5

należy utworzyć instancję głównego formularza aplikacji i przekazać

ją jako argument do metody

static Application.Run.

To spowoduje wizualizację głównego

formularza i uruchomi dla bieżącego wątku standardową pętlę poleceń Windows, w wyniku czego możliwe będzie przeniesienie danych wejściowych użytkownika (typu naciśnięcia kla­ wisza, kliknięcia myszą itp.) do utworzonej aplikacji w charakterze zdarzeń. Klasa

WelcomeForm , przedstawiona w poniższym fragmencie programu, stanowi prostą aplikację

formularzy Windows (Windows Forms), demonstrując wymienione wyżej techniki. W trakcie działania program monituje użytkownika, żeby wprowadził imię, a następnie wyświetla okno poleceń z powitaniem w programie

C#: Ksirga przykładów.

using System . Windows . Fo rms ; public class WelcomeForm : Form {

li

P rywa t n i członkowie p rzechowu j ący odnośniki do kon t rolek f o rmula rza .

p rivate Label l a bel l ; p rivate TextBox textBox l ; p rivate Button button l ;

li li

Kon st rukto r s łużący d o two rzenia instanc j i fo rmula rza i konfigu rowania kont role k .

public WelcomeFo rm ( ) {

li

Utwo rzenie instan c j i kont rolek użytych w f o rmula rzu .

this . labell = new Label ( ) ; this . textBoxl = new TextBox ( ) ; this . buttonl = new Button ( ) ;

li li

Zawies zenie logiki układu f o rmula rza na czas kon f igu rowan ia kont rolek .

this . Su spend Layout ( ) ;

li

Kon f igu rowanie label l, wyświetlaj ącej zachętę dla użyt kownika .

this . label l . Location = new System . D rawing . Point ( 16 , 36 ) ; this . label l . Name = " label l " ; t h i s . label l . Size = new System . D rawing . Size ( 128 , 16) ; this . label l . Tab!ndex = 0 ; t hi s . label l . Text = " Please ente r you r name : " ;

li

Konfigu rowanie textBox l , odbie raj ącego dane użytkownika .

this . textBox l . Location = new System . D rawing . Point ( 152 , 32 ) ; this . textBox l . Name = "textBox l " ; this . textBox l . Tabindex = 1 ; this . textBoxl . Text = " " ;

li

konfigu rowanie button l , klikanego p rzez użytkownika .

this . button l . Location = new System . D rawing . Point ( 109 , 89 ) ; this . button l . Name = " button l " ; this . button l . Tabindex = 2 ; this . button l . Text = " Ente r" ; this . button l . Click += new System . EventHandle r ( th is . button l_Cl ick ) ;

6

C#

-

Księga przykładów

li

Konfigu rowanie f o rmula rza WelcomeFo rm i dodanie kont rolek .

this . ClientSize = new Sys tem . D rawing . Size ( 292 , 126 ) ; this . Cont rol s . Add ( this . button l ) ; this . Cont rol s . Add ( this . textBoxl ) ; this . Cont rols . Add ( this . label l ) ; t h is . Name = " f o rm l " ; t h is . Text = " C# P rog ramme r ' s Cookbook" ;

li

P rzywrócenie logiki układu dla f o rmula rza

this . ResumeLayout ( false ) ;

} li li

Punkt wej ścia aplikacj i , two rzy instan c j ę f o rmularza i u ru chamia standa rdową pętlę w bieżącym wątku .

public static void Main ( ) { Application . Ru n ( new WelcomeForm ( ) ) ;

} li

Obsł uga zda rzeń wywołana po kliknięciu p rz ycisku Ente r .

p rivate void button l_Click ( obj ect sende r , System . EventArgs e ) {

li

Wypisanie komu n ikat u debugowania na konsoli

System . Console . WriteLine ( " Us e r ente red :

li

"

+

textBoxl . Text ) ;

Wyświetlenie powitania j ako okna komunikatu

MessageBox . Show ( "Welcome to the C# P rog ramme r's Cookbook , " + textBoxl . Text , " C# P rog ramme r ' s Cookbook " ) ;

} } Aby wbudować klasę

WelcomeForm

(przechowywaną w pliku o nazwie WelcomeForm.cs)

w aplikację, należy wykorzystać polecenie

ltarget:winexe

csc

/target:winexe WełcomeFonn.cs.

Przełącznik

poinformuje kompilator, że aplikacja jest uruchamiana w oparciu o system

Windows. W rezultacie kompilator utworzy moduł wykonywalny w taki sposób, żeby w ogóle nie angażować konsoli podczas działania aplikacji. Jeśli do budowy aplikacji typu formularz Windows zostanie użyty przełącznik ltarget:exe zamiast ltarget:winexe, będzie ona również dzia­ łała prawidłowo, ale dodatkowo będzie widoczne okno konsoli. Przy produkcji oprogramowa­ nia finalnego jest to niepożądane, jednak okno konsoli jest pomocne przy pisaniu procedur usuwania błędów, rejestrowania podczas rozbudowy i testowania aplikacji Windows Forms. Można również pisać na konsoli, używając metod

Write i WriteLine klasy System. Conso l.e.

Rysunek 1-1 przedstawia aplikację WelcomeForm.exe, wykonującą pozdrowienie użytkow­ nika o imieniu Rupert. Ta wersja aplikacji została utworzona przy użyciu przełącznika kompi­ latora ltarget:exe, dzięki czemu powstało widoczne okno konsoli, w którym można obserwować dane wyjściowe instrukcji

Conso l.e. WriteLine programu obsługi zdarzeń button]_Click.

Rozdział

Rysunek 1 -1

1 : Tworzenie

aplikacji

7

Prosta aplikacja Formularzy Windows (Windows Forms) .

Uwaga Budowa dużych aplikacji w oparciu o GUI jest zajęciem czasochłonnym, które wymaga prawidłowego nadawania instancji, jak również konfigurowania i wiązania ze sobą wielu formularzy i kontrolek . Visual Studio .NET firmy Microsoft umożliwia automatyzację wielu prac związanych z budowaniem aplikacji graficznych . Tworzenie dużych aplikacji graficznych bez pomocy takich narzędzi, jak Visual Studio .NET, zajmuje o wiele więcej czasu, jest trudniejsze i zwiększa prawdopodobieństwo pojawienia się błędów w kodzie wynikowym .

1 .3 Utworzenie i wykorzystanie modułu kodu Problem Chciałbyś uzyskać jedną lub więcej z poniższych opcji: •

Poprawić sprawność aplikacji i wykorzystanie przez nią pamięci poprzez ładowanie przez moduł runtime rzadko używanych typów tylko wtedy, gdy zachodzi rzeczywista potrzeba ich obecności.



Skompilować typy zapisane w języku C# do formularza, umożliwiającego wbudowanie go do aplikacji napisanych w innych językach .NET.



Wykorzystać typy zaprojektowane w innych językach do potrzeb aplikacji w języku C#.

Rozwiązanie Zbuduj swój kod źródłowy w języku C# w postaci modułu przy użyciu przełącznika kom­ płiatora ltarget:module. Aby włączyć istniejące moduły do swego programu, użyj przełącznika kompilatora laddmodule.

8

C#

-

Księga przykładów

Omówienie Moduły stanowią elementy budowy asemblacji .NET. Każdy z nich składa się z pojedynczego pliku, który zawiera wyszczególnione niżej elementy: • Kod Microsoft lntermediate Language (MSIL), utworzony z kodu źródłowego w języku C#

podczas kompilacji. • Metadane, opisujące typy zawarte w module. • Zasoby, takie jak ikony i tablice łańcuchów, używane przez cypy w module.

Asemblacje składają się z jednego lub więcej modułów oraz z manifestu asemblacji. W przy­ padku pojedynczego modułu, moduł i manifest asemblacji są zazwyczaj dla wygody umiesz­ czone w pojedynczym pliku. Gdy modułów jest więcej, asemblacja stanowi logiczne połączenie więcej nii jednego pliku, które trzeba rozpowszechaniać jako kompletną jednostkę. W tej sytuacji manifest asemblacji zostanie umieszczony w osobnym pliku albo wbudowany w jeden z modułów. Tworzenie asemblacji z wielu modułów sprawia, ie zarządzanie staje się bardziej skompli­ kowane, a jej rozpowszechnianie trudniejsze, jednak w pewnych warunkach moduły oferują znaczne korzyści, takie jak: • Moduł zostanie załadowany przez moduł runtime cylko wtedy, gdy potrzebne są typy zde­

finiowane w cym module. Jeśli więc w grę wchodzi zestaw typów rzadko uiywanych przez aplikację, moina umieścić je w osobnym module, który zostanie załadowany cylko w razie konieczności. To rozwiązanie daje następujące korzyści: O Zwiększa funkcjonalność aplikacji, szczególnie przy pracy w sieci. O Pozwala na minimalizację wykorzystania pamięci. • Moiliwość wykorzystania wielu róinych języków przy pisaniu aplikacji, króra działa pod

kontrolą wspólnego modułu run time CLR (Common Language Runcime), jest wielką zaletą .NET Framework. Jednakże kompilator C# nie może skompilować kodu w Microsoft Visual Basic .NET lub COBOL .NET w celu jego dołączenia do asemblacji. Najpierw trzeba zastosować kompilator określonego języka, żeby przekształcić kod źródłowy w strukturę MSIL, którą kompilator C# może włączyć do całości, czyli w moduł. Jeśli inni programiści mają korzystać z tych typów, które zostały utworzone w języku C#, naleiy równiei włączyć je do modułu. Aby skompilować plik źródłowy o nazwie ConsoleUtils.cs do postaci modułu, naleiy uiyć pole­ cenia

csc /target:module ConsoleUtils.cs,

ConsoleUtils.netmodule. Rozszerzenie

w wyniku czego zostanie utworzony plik o nazwie

netmodule

jest domyślne dla wszystkich modułów,

a nazwa pliku taka sama, jak nazwa pliku źródłowego w języku C#. Moina równiei tworzyć moduły z wielu plików źródłowych, co spowoduje powstanie pojedyn­ czego pliku (modułu), zawierającego MSIL i metadane dla wszystkich typów, zawartych we wszyst­ kich plikach źródłowych. Polecenie

csc

/target:module ConsoleUtils.cs WindowsUtils.cs

kompiluje dwa pliki źródłowe o nazwach ConsoleUtils.cs i WindowsUtiłs.cs, aby utworzyć moduł o nazwie ConsoleUtiłs.netmodule. Moduł ten nosi nazwę pochodną od nazwy pierw­ szego wymienionego pliku tekstowego, o ile nie zostanie ona zastąpiona przy użyciu przełącz­ nika kompilatora /out. Na przykład polecenie csc /target:moclule /out:Utilities.netmodule ConsolcUtils.cs WindowsUtils.cs utworzy moduł o nazwie Utilicies.netmodule.

Rozdział 1 : Tworzenie aplikacji

9

Aby utworzyć asemblację składającą się z wielu modułów, należy posłużyć się przełącznikiem kompilatora laddmodule. Aby utworzyć plik wykonywalny o nazwie MyFirstApp.exe z dwóch modułów o nazwach WindowsUtils.netmodule i ConsoleUtils.netmodule oraz z dwóch plików źródłowych o nazwach SourceOne.cs i SourceTwo.cs, należy użyć polecenia csc /out:MyFust­

App.exe/target:exe/addmodule:WmdowsUtils.netmodule,ConsoleUtils.netmoduleSoW'CC­ One.es SourceTwo.cs. To polecenie utworzy asemblację składającą się z następujących plików: •

MyFirstApp.exe, który zawiera manifest asemblacji oraz MSIL dla typów zadeklarowanych w plikach źródłowych SourceOne.cs i SourceTwo.cs.



ConsoleUtils.netmodułe i WindowsUtils.netmodułe, które teraz są integralnymi składni­ kami złożonej z wielu plików asemblacji, ale nie zostały zmienione w tym procesie kompila­ cji (przy próbie uruchomienia MyFirstApp.exe bez obecności modułów netmodules, system zgłosi wyjątek System.IO.FileNotFoundException).

1 .4 Utworzenie biblioteki kodów i korzystanie z niej Problem Chcesz umieścić zestaw funkcjonalności w bibliotece kodów wielokrotnego użycia, dzięki czemu różne aplikacje mogą po wywołaniu z niego korzystać.

Rozwiązanie Aby utworzyć bibliotekę, użyj przełącznika ltarget:library dla kompilatora C# (csc.exe) pod­ czas kompilowania asemblacji. Aby się odnieść do biblioteki, użyj przełącznika kompilatora C# lreference podczas kompilowania aplikacji i określ nazwy żądanych bibliotek.

Omówienie W przepisie 1.1 pokazano, jak utworzyć aplikację o nazwie FirstApp.exe z dwóch plików źródłowych: ConsoleUtils.cs i HełloWorld.cs. Plik ConsoleUtils.cs zawiera klasę ConsoleUtils, która zapewnia metody upraszczające interakcję z konsolą Windows. Gdyby rozszerzyć zakres funkcjonalności klasy ConsoleUtils, mogłaby ona zawierać funkcjonalność użyteczną dla wielu aplikacji. Tak więc, zamiast włączać kod źródłowy dla ConsoleUtils do każdej aplikacji, można go umieścić w bibliotece i wykorzystywać niezależnie. Aby umieścić w bibliotece plik ConsoleUtils.cs, należy zastosować polecenie csc /target: library ConsoleUtils.cs. Spowoduje ono utworzenie biblioteki o nazwie ConsoleUtils.dłł. Aby utworzyć bibliotekę z wielu plików źródłowych, należy zapisać nazwę każdego pliku na końcu polecenia. Używając przełącznika kompilatora !out, można także określić nazwę biblioteki; w innym wypadku biblioteka będzie nosiła nazwę pierwszego pliku źródłowego. Na przykład, by utworzyć bibliotekę o nazwie MyFirstLibrary.dll dwóch plików źródłowych o nazwach ConsoleUtils.cs i WindowsUtils.cs, należy użyć polecenia csc /out:MyFirstLlbrary.dll /target:

library ConsoleUtils.cs WmdowsUtils.cs.

10

C# - Księga przykładów Przed rozpowszechnieniem biblioteki można zastosować silne nazwy, aby nikt oprócz autora nie mógł modyfikować asemblacji i przesyłać jej jako własnego oryginału. Silne nazewnictwo biblioteki pozwala także na instalowanie jej w globalnym buforze asemblacji (GAC - Global Assembly Cache), co ułatwia wielokrotne wykorzystywanie (w przepisie 1 .9 podano sposób zastosowania silnego nazewnictwa w asemblacji, a w przepisie 1 . 1 4 metodę instalacji asembla­ cji o silnej nazwie w GAC) . Można również podpisać bibliotekę podpisem elektronicznym Aur­ henticode; będzie to udokumentowaniem faktu, kro jest wydawcą danej asemblacji - metody podpisywania asemblacji przy pomocy Authenticode zawiera przepis 1 .1 2. W celu skompilowania asemblacji zależnej od typów zadeklarowanych w bibliotekach zewnętrz­ nych, trzeba poinformować kompilator, o które biblioteki chodzi - używając przdącznika kom­ pilatora !reference. Na przykład, aby skompilować plik źródłowy HelloWorld.es z przepisu 1 .1 , gdy klasa ConsoleUtils znajduje się w bibliotece ConsoleUtils.dll, należy zastosować polecenie csc /referenc.e:ConsoleUtils.dll HelloWorld.es. Nasuwają się trzy uwagi godne zapamiętania: -



Odwołując się do więcej niż jednej biblioteki, należy rozdzielić ich nazwy przecinkiem łub średnikiem, ale nie spacją. Na przykład: /referenc.e:ConsoleUtils.dll, W indowsUtils.dll.



Jeżeli kody źródłowe bibliotek nie znajdują się w rym samym katalogu, należy użyć prze­ łącznika kompilatora !lib dla określenia dodatkowych katalogów, które kompilator ma przeszukać w celu odnalezienia bibliotek. Na przykład: /lib:c:\CommonLibraries, c:\Dev\

ThirdPartyLibs. •

Jeżeli odniesienie dotyczy biblioteki, która stanowi asemblację wielu plików, należy sięgnąć do tego pliku, który zawiera manifest asemblacji. Informacje na temat asemblacji wielu plików podaje przepis 1 .3.

1 .5 Dostęp do argumentów linii poleceń Problem Chcesz mieć dostęp do argumentów określonych w linii poleceń w trakcie wykonywania aplikacji.

Rozwiązanie Użyj podpisu do metody Main, co pozwoli ułożyć argumenty w linii poleceń w formie tablicy łańcuchów. Alternatywnie możesz sięgnąć do argumentów w linii poleceń z dowolnego miejsca kodu przy użyciu pól typu static klasy System.Environments.

Omówienie Zadeklarowanie metody Main aplikacji z jednym z poniższych podpisów umożliwia dostęp do argumentów linii poleceń w postaci tablicy łańcuchów. public static void Main ( st ring ( J a rg s ) { } public static int Main ( st ring [ ) a rg s ) { }

Rozdział 1 : Tworzenie aplikacji

11

Podczas wykonywania programu, argument args b�zie zawierał łańcuch (string) dla każdej war­ tości wprowadzonej do linii poleceń po nazwie aplikacji. Metoda Main w poniższym przykładzie bada każdy przekazany argument linii poleceń i wyświetla je na konsoli. public class CmdLineArgExample { public static void Main ( st ring [ ] a rgs ) {

li

Kolej ne wykonywanie a rgumentów wie rsza polecenia

f o reach ( st ring s in a rgs ) { System . Console . WriteLine ( s ) ;

} } } Ważne jest zrozwnienie sposobu przekazywania argumentów do aplikacji. Jeśli wykonywany jest przykład CmdlineArgExamp/.e przy użyciu następującego polecenia: CmdLineArgExample "one \ " two\ "

t h ree " f o u r ' five

s ix '

to aplikacja wygeneruje na konsoli następujące dane wyjściowe: one " two "

t h ree

fou r ' five six '

Należy zauważyć, że inaczej niż w języku C lub C++, nazwa aplikacji nie jest umieszczona w tablicy argumentów. Użycie znaku podwójnego cudzysłowu (") sprawia, że jako pojedynczy argument traktowane jest więcej niż jedno słowo, podczas gdy cudzysłów pojedynczy (') tego nie powoduje. Można włączyć podwójny cudzysłów do argumentu poprzez ujęcie go lewym ukośnikiem (\). Wszystkie spacje zostaną z linii poleceń usunięte, o ile nie są ujęte w podwójny cudzysłów. Jeśli konieczny jest dostęp do argumentów linii poleceń - w innych miejscach kodu, niż metoda Main - należy przetworzyć argumenty linii poleceń w swojej metodzie Main i zapamiętać je do póź­ niejszego wykorzystania. Alternatywnie można użyć klasy System.Environment, która zawiera dwa pola static dostarczające informację o linii poleceń: Commandline i GetCommandlineArgs. Właś­ ciwość Commandline zwraca łańcuch zawierający całą linię poleceń, która uruchomiła aktualny proces. W zależności od systemu operacyjnego, w którym działa dana aplikacja, nazwa aplikacji może być poprzedzona informacją o ścieżce. Systemy Microsoft Windows NT 4.0, Windows 2000 i Windows XP nie dołączają informacji o ścieżce; systemy Windows 98 i Windows ME infor­ mację tę dołączają. Metoda GetCommandLineArgs przekazuje zwrotnie tablicę zawierającą argu­ menty linii poleceń. Tablica ta może być przetworzona w taki sam sposób, jak tablica przekazana do metody Main, omówiona na początku tego przepisu. W przeciwieństwie do tablicy przeka­ zywanej do metody Main pierwszym dementem w tablicy, przekazanej zwrotnie przez metodę GetCommandLineArgs, jest nazwa aplikacji.

12

C#

-

Księga przykładów

1 .6 Selektywne włączanie kodu podczas jego budowy Problem Chcesz selektywnie włączać i wyłączać fragmenty kodu źródłowego ze skompilowanej asemblacji.

Rozwiązanie Użyj dyrektyw preprocesora #if, #elif, #else i #endifw celu identyfikacji bloków kodu, które mają być warunkowo dołączane do asemblacji. Wykorzystaj atrybut System.Diagnostics. Conditional­ Attribute celem zdefiniowania metod, które mają być wywoływane wyłącznie warunkowo. Możesz kontrolować dołączanie warunkowego kodu dzięki zastosowaniu w kodzie dyrektyw #dejine i #umie/ lub poprn:z użycie przełącznika !de.fine przy uruchomieniu kompilatora C#.

Omówienie Jeśli aplikacja ma działać w sposób zależny od takich czynników, jak platforma czy środowisko, w którym się znajduje, należy wbudować w logikę kodu mechanizmy sprawdzające modułu runtime, które będą przełączać wykonywanie poszczególnych wariantów. Jednakie takie roz­ wiązanie może zaowocować zwięk."7.oną objętością kodu i obniżyć efektywność wykonania, zwłaszcza gdy chodzi o wicie wariantów lub wicie lokalizacji. Alternatywnym rozwiązaniem jest utworzenie różnych wersji danej aplikacji, działających na różnych docelowych platformach i w różnych środowiskach. Taka realizacja nie wyklucza ani zwiększenia objętości kodu, ani pogorszenia efektywności, może się okazać jednak optymalnym rozwiązaniem, jeśli dla każdej wersj i musi zadziałać inny kod źródłowy. Tak więc język C# umożliwia dostosowanie do potrzeb klienta wersj i aplikacji od podstaw, od pojedynczego kodu. Dyrektywy preprocesora #if, #elif, #else i #endifumożliwiają identyfikację tych bloków kodu, które kompilator może włączyć do asemblacji tylko wtedy, gdy podczas kompilacji wstaną zdefiniowane specjalne symbole. Te symbole funkcjonują jako przełączniki „on/off' i nie posia­ dają wartości: symbol jest po prostu zdefiniowany lub nie. Aby zdefiniować symbol, można użyć albo dyrektywy #de.fine, albo wykorzystać przełącznik kompilatora /de.fine. Symbole zde­ finiowane przy pomocy #de.fine pozostają aktywne do końca pliku, w którym je zdefiniowano. Symbole zdefiniowane przy pomocy przełącznika kompilatora !de.fine są aktywne we wszystkich plikach źródłowych, które mają być kompilowane. Aby symbol utworzony przy użyciu prze­ łącznika kompilatora /de.fine uczynić z powrotem niezdefiniowanym, przewidziana jest w języku C# dyrektywa #undef, zapewniająca brak zdefiniowania symbolu w określonych plikach źródło­ wych. Wszystkie dyrektywy #de.fine i #undefmuszą być umieszczone przed kodem, na początku pliku źródłowego, a także przed wszystkimi dyrektywami using. Dla symboli rozróżniana jest wielkość liter. W poniższym przykładzie kod przypisuje różne wartości lokalnej zmiennej platformName, zależnie od tego, czy symbole winXP, win2000, winNTlub win98 są zdefiniowane. Nagłówek kodu definiuje symbole win2000 i release (w tym przykładzie nie wykorzystany) i anuluje defi­ nicję symbolu win98 w przypadku, gdy był on zdefiniowany w linii poleceń kompilatora.

Rozdział 1 : Tworzenie aplikacji

13

#define win2000 #define release #undef

win98

using System ; public class ConditionalExample { public s tatic void Main ( ) {

li

Dekl a racj a łańcucha

st ring plat f o rmName ;

li

#if winXP

Kompilacj a dla Windows XP

platformName = " Mic rosoft Windows XP" ;

li

#el i f win2000 platformName

=

li

#el if winNT plat f o rmName

=

li

#el i f win98 platfo rmName #el se platfo rmName

=

li =

Kompilacj a dla Windows 2000 " Mic rosoft Windows 2000 " ; Kompilacj a dla Windows NT " Mic rosoft Windows NT" ; Kompilacj a dla Windows 98 " M i c rosoft Windows 98" ; Nieznana plat f o rma " Un known " ;

#endif Console . WriteLine ( pl a t f o rmName ) ;

} } Aby utworzyć klasę

ConditionalExample (zawartą w pliku o nazwie ConditionalExample.cs) winXP i DEBUG (nie wykorzystany w niniejszym przykładzie), należy

i zdefiniować symbole użyć polecenia csc

/define:winXP;DEBUG ConditionalExample.cs.

Konstrukcja #if .. #endif pozwoli oszacować zdania #ifi #elif tylko wtedy, gdy stwierdzi, że jedna z wartości jest prawdziwa. Oznacza to, że jeśli wstanie zdefiniowanych wiele symboli (na przykład tylko kod

ze

winXP i win2000),

ważna jest kolejność ucworwnych zdań. Kompilator dołączy

zdania, które oceni jako prawdziwe. Jeśli żadne zdanie nie jest prawdziwe, kompi­

lator umieści kod w zdaniu #else. Można również użyć operatorów logicznych, aby oprzeć kompilację warunkową na więcej niż jednym symbolu. Tabela 1 - 1 podsumowuje operatory posiłkujące się dyrektywą #if.. #endif. Tabela

1 -1

Operator

Operatory logiczne posiłkujące się dyrektywą #if.. #endif

Przykład #ifwinXP

!=

Opis

= = true

#ifwin.XP != true

Równość. Przypisuje wartość true, jeśli symbol

winXP

jest zdefiniowany. Równoważny z #ifwinXP. Nierówność. Przypisuje wartość true, jeśli symbol winXP nie jest zdefiniowany. Równoważny z #if!winXP.

&&

#ifwinXP &&

release

Logiczne AND. Przypisuje wartość true, jeśli obydwa symbole

winXP i release są zdefiniowane.

Ciq.g dalszy na nastrpnej stronie

14

C#

-

Księga przykładów

Tabela

1 -1

Operatory logiczne posiłkujące się dyrektywą #if..#endif

Ciqg da/szy

poprzedniej strony

z

Operator

Przykład

Opis

li

#ifwinXP li release

Logiczne OR. Przypisuje wartość true, jeśli któryś z symboli winXP lub release jest zdefiniowany.

o

#if( winXP l i win2000) Nawiasy umożliwiają grupowanie wyrażeń. Przypisuje && release wartość true, jeśli zdefiniowany jest symbol winXP lub

win2000 oraz symbol release jest zdefiniowany.

Ostrzeżenie Należy zwracać uwagę, aby nie nadużywać dyrektyw kompilacji warunko­ wej i nie przesadzać ze złożonością wyrażeń warunkowych. W przeciwnym wypadku kod może się stać nieczytelny i trudny w zarządzaniu, zwłaszcza w wypadku rozbudowy.

Mniej elastyczną, ale bardziej elegancką alternatywą dyrektywy preprocesora #if jest atry­ but System.Diagnostics. Conditiona!Attribute. Po zastosowaniu atrybutu Conditiona!Attribute do metody, kompilator zignoruje wszystkie wywołania tej metody, jeśli symbol określony przez Conditiona/Attribute nie jest zdefiniowany w punkcie wywołania. W podanym niżej przykładzie Conditiona/Attribute określa, że wywołania metody DumpState powinny być zawarte w skompilo­ wanej asemblacji tylko wtedy, kiedy symbol DEBUG został zdefiniowany podczas kompilacji. [ System . Diagnostics . Conditional ( " DEBUG " ) J public static void DumpState ( ) {//-}

Użycie Conditiona/Attribute centralizuje logikę warunkowej kompilacji w deklaracji metody, co oznacza możliwość swobodnego umieszczania wywołań do metod warunkowych, bez potrzeby przepełniania kodu dyrektywami #if. Ponieważ kompilator po prostu usuwa wywo­ łania metody warunkowej z kodu, nie może on zawierać zależności od wartości zwracanych z metody warunkowej . Oznacza to, że Conditiona/Attribute można stosować tylko do metod, które zwracają stan pusty (void). Można zastosować wielokrotne instancje Conditiona/Attribute do metody, aby utworzyć zachowanie logiczne OR. Wywołania podanej niżej wersji metody DumpState będą skompilo­ wane tylko wtedy, gdy symbole DEBUG OR TEST są zdefiniowane. [ System . Diagnost ic s . Condit iona l ( " DEBUG " ) J [ System . Diagnost ic s . Conditional ( " TEST" ) J public static void DumpStat e ( ) {//-}

Osiągnięcie zachowania logicznego AND nie jest tak proste i wymaga użycia pośredniej metody warunkowej, co szybko prowadzi do nakładania się zapisów i w efekcie do złożonego, trudnego do zrozumienia i utrzymania kodu. Oto szybki przykład, wymagający zdefiniowania symboli DEBUG oraz TEST dla wywoływania funkcjonalności DumpState (zawartej w DumpState2). [ System . Diagnostic s . Conditiona l ( " DEBUG " ) J public static void DumpState ( ) { DumpState2 { ) ; }

Rozdział 1 : Tworzenie aplikacji

15

[ Sys tem . Diag nostic s . Conditional ( "TEST" ) J public static void DumpState2 ( )

{li- }

Uwaga Klasy Debug i Trace, z przestrzeni nazw System.Diagnostics, używają Conditional­ Attribute dla różnych metod . Metody klasy Debug są warunkowe na podstawie definicji sym­ bolu DEBUG, a metody klasy Trace są warunkowe na podstawie definicji symbolu TRACE.

1 .7 Dostęp do elementu programu o tej samej nazwie, co słowo kluczowe Problem Chcesz mieć dostęp do pola typu, ale nazwa typu lub nazwa pola jest taka sama, jak słowo kluczowe C# .

Rozwiązanie Dołącz znak (@) jako prefiks do wszystkich instancji nazwy identyfikatora w swoim kodzie.

Omówienie Platforma .Net Framework umożliwia użycie składowych oprogramowania, pochodzących z aplikacji języka C#, napisanych w innych językach .NET. Każdy język zawiera własny zestaw słów kluczowych (lub słów zarezerwowanych) , co nakłada na programistę określone obostrzenia dotyczące nazw, które może przypisywać elementom programu, takim jak typy, pola i zmienne. Może się zdarzyć, że programista uruchamiający składową programu w innym języku, użyje niechcący słowa kluczowego języka C# jako nazwy elementu programu. Symbol @ umożliwia uniknięcie ewentualnego konfliktu, dotyczącego użycia słowa kluczowego C# jako identyfika­ tora. Poniższy fragment kodu nadaje instancję obiektowi typu operator (być może telefoniczny) i ustawia jego właściwość volatile (ulotność) na wartość true, przy czym zarówno operator jak volatile są słowami kluczowymi języka C#.

li

Two rzenie instancj i obiektu ope rato ra

@ope rat o r Operatorl = new @operato r ( ) ;

li

Określanie wła ściwości volatile dla ope rat o ra

Operatorl . @volatile = t ru e ;

16

C#

Księga przykładów

-

1 .8 Tworzenie i zarządzanie parami kluczy silnej nazwy Problem Chcesz utworzyć klucze publiczny i prywatny (parę kluczy) , aby przy ich pomocy można było przypisać asemblacjom silne nazwy.

Rozwiązanie Użyj narzędzia Strong Name (sn.exe) do wygenerowania pary kluczy i zapamiętaj je w pliku lub w zasobniku kluczy programu usług kryptograficznych CSP (Cryptographic Service Provider).

Uwaga Program CSP jest elementem programu Win32 CryptoAPI , który umożliwia usługi w zakresie utajniania i odtajniania informacji oraz generowania podpisu cyfrowego. CSP oferuje również możliwości przechowywania kluczy, przy zastosowaniu mocnych narzędzi szyfrujących i możliwości zasobów systemu operacyjnego do zabezpieczenia jego zawartości. Dokładniejsze omówienie CSP i CryptoAPI wykracza poza zakres niniej­ szej książki. Po odnośne szczegółowe informacje na temat CryptoAPI należy sięgnąć do dokumentacji SOK.

Omówienie Aby wygenerować nową parę kluczy i zapamiętać je w pliku o nazwie MyKeys.snk, należy wyko­ nać polecenie sn -k MyKeys.snk (.snk jest zwykle używanym rozszerzeniem dla plików zawiera­ jących klucze silnej nazwy) . Wygenerowany plik zawiera obydwa klucze: publiczny i prywatny. Można obejrzeć klucz publiczny przy użyciu polecenia

sn -tp MyKeys.snk,

które wygeneruje

informację wyjściową, podobną do przedstawionej niżej (w skróconej formie) :

Mic rosoft ( R ) . NET F ramewo rk S t rong Name Ut ilit y

Vers ion 1 . 1 . 4322 . 573

Copyright ( C ) Mic rosoft Co rpo rat ion 1998 - 2002 . All rig h t s rese rved . Public key i s 07020000002400005253413200040000010001002b4ef3c2bbd6478802b64d0dd3 f2e7c65ee ; 6478802b63cb894a782f3aladbb46d3ee5ec5577e7dccc8 18937e964cbe997c 12076c 19f2d7ad 179f15f7dccca6c6b72a Public key token is 2a ld3326445fc02a Token klucza publicznego, przedstawiony przy końcu wydruku, to co najmniej 8-bajtowy skrót kryptograficzny (hash), obliczony na podstawie klucza publicznego. Ponieważ klucz publiczny jest tak długi, moduł .NET wykorzystuje token ze względu na jego spakowaną formę - zarówno przy wyświetlaniu nazwy, jak i w wypadku odwołań do danego klucza w innych asemblacjach. Ogólne omówienie skrótów kryptograficznych zawiera rozdział 1 4 .

Rozdział 1 : Tworzenie aplikacji

17

Jak sugeruje sama nazwa, utrzymywanie klucza publicznego (ani jego skrótu) w tajemnicy nie jest konieczne. Po nadaniu asemblacji silnej nazwy (co zostanie omówione w przepisie 1 .9), kompilator wykorzysta klucz prywatny do wygenerowania podpisu cyfrowego (zaszyfrowanego skrótu kodu) dla manifestu asemblacji. Kompilator wbuduje podpis cyfrowy i klucz publiczny w asemblację, dzięki czemu każdy użytkownik będzie mógł zweryfikować dany podpis cyfrowy. Niezbędne jest za to utrzymywanie w tajemnicy klucza prywatnego. Ludzie, który mają dostęp do klucza prywatnego, mogą zmienić posługującą się nim asemblacjc; i utworzyć nową silną nazwę. Klienci z niej korzystający w ogóle nie będą świadomi, że używają zmodyfiko­ wanego kodu! Nie istnieje mechanizm odrzucania skompromitowanych kluczy silnych nazw. Jeśli klucz prywatny zostanie skompromitowany, należy wygenerować nowe klucze i porozsyłać nowe wersje asemblacji, używające nowych kluczy. Trzeba także powiadomić klientów o tym fakcie i dokładnie wyjaśnić, które wersje klucza publicznego są wiarygodne. Jest to więc bar­ dzo kosztowna lekcja ze względów finansowych, jak i wiarygodności. Istnieje wiele możliwości ochrony klucza prywatnego; ich wybór zależy od takich czynników, jak: •

struktura i rozmiar organizacji,



proces projektowania i rozpowszechniania,



dostępne zasoby oprogramowania i sprzętu,



wymagania użytkownika.

Wskazówka Zazwyczaj mała grupa zaufanych osób (osoby poręczające podpisem) jest odpowiedzialna za bezpieczeństwo kluczy podpisu silnych nazw w danej instytucji i za podpisywanie wszystkich asemblacji, bezpośrednio przed ich ostateczną dystrybu­ cją. Możliwość opóźnienia tego podpisu (omówiona w przepisie 1 . 1 1 ) ułatwia to zadanie i pozwala uniknąć potrzeby dystrybuowania kluczy prywatnych dla wszystkich uczestni­ ków zespołu projektowego.

Jedną z właściwości dostarczanych przez narzędzie Strong Name, pomocną przy uproszczeniu zabezpieczania kluczy silnych nazw, jest wykorzystanie zasobników CSP. Po wygenerowaniu pary kluczy do pliku można zainstalować je w zasobniku kluczy i usunąć plik. Na przykład, aby prze­ nieść parę kluczy zapamiętanych w pliku MyKeys.snk do zasobnika CSP o nazwie StrongName­ Keys, należy użyć polecenia sn -i MyKeys.snk StrongNameKeys (w przepisie 1 .9 wyjaśniono, jak używać kluczy silnych nazw, zapamiętanych w zasobniku CSP). Ważną cechą zasobników kluczy CSP jest ich podział na zasobniki tworzone pod kątem użytkownika (user-based) i zasobniki tworzone pod kątem systemu komputerowego (machine­ based). System bezpieczeństwa Windows pozwala na dostęp każdego użytkownika tylko do jego własnych zasobników typu user-based. Jednocześnie każdy użytkownik danego systemu kompu­ terowego ma dostęp do zasobnika systemowego typu machine-based. W trybie domyślnym narzędzie Strong Name użyje zasobników kluczy systemowych (machine­ based). Oznacza to, że każdy, kto może sic; zalogować na danym komputerze i zna nazwę czyjegoś zasobnika kluczy, może podpisać asemblacjc; jego kluczami silnych nazw. Aby zapewnić korzystanie tylko z zasobników typu user-based przez narzędzie Strong Name, należy użyć polecenia sn -m n. Dla przywrócenia poprzedniego stanu (zasobniki typu machine-based), należy zastosować polecenie sn -m y. Polecenie sn -m wyświetla bieżący stan użycia zasobników machine-based i user-based.

18

C#

Księga przykładów

-

Aby usunąć klucze silnych nazw z zasobnika StrongNarneKeys (podobnie jak i sam zasobnik), należy użyć polecenia sn -d StrongNameKeys.

1 .9 Nadawanie asemblacji silnej nazwy Problem Chcesz nadać asemblacji silną nazwę w taki sposób, żeby: •

Posiadała unikalną tożsamość, pozwalającą na przypisanie do niej określonych zezwoleń przy konfigurowaniu zasad zabezpieczeć dostępu do kodu.



Nie mogła podlegać modyfikacjom i przemieszczeniom, jak twoja oryginalna asemblacja.



Wspomagała wymianę wersji i zasad doboru wersji.



Mogła być dzielona przez liczne aplikacje i była zainstalowana w globalnej pamięci asembla­ cji cache (GAC - Global Assembly Cache).

Rozwiązanie Z poziomu asemblera wykorzystaj atrybuty do określenia lokalizacji twojej pary kluczy silnych

nazw i (opcjonalnie) numeru wersji i kultury dla twojej asemblacji. Nadanie przez kompilator twojej asemblacji silnej nazwy będzie stanowiło etap procesu jej budowy.

Omówienie Aby zaopatrzyć asemblację w silną nazwę przy użyciu kompilatora C#, należy: •

Posiadać parę kluczy silnych nazw, znajdującą się w pliku albo w zasobniku kluczy CSP (przepis 1 .8 zawiera omówienie tworzenia pary kluczy silnych nazw) .



Mieć możność wykorzystania atrybutów na poziomie asemblera, w celu określenia lokaliza­ cji danej pary kluczy silnych nazw, dostępnej dla kompilatora: O

Jeśli ta para kluczy znajduje się w pliku, należy zastosować do swojej asemblacji atrybut System.Refoction.AssemblyKeyFileAttribute pliku zawierającego klucze.

O

Jeśli para kluczy znajduje się w zasobniku CSP, należy zastosować do asemblacji atrybut System.Refoction.AssemblyKeyNameAttribute i określić nazwę zasobnika, w którym klu­ cze są zapamiętane.

Opcjonalnie można także: •

Określić kulturę, która wspomaga daną asemblację, poprzez zastosowanie atrybutu System. Refoction.AssemblyCuitureAttribute (nie można określić kultury dla asemblacji wykonywal­



Określić wersję asemblacji poprzez zastosowanie do niej atrybutu System.Refoction.Assembly­

nych, ponieważ wspomagają one rylko kulturę neutralną) .

VersionAttribute.

Rozdział 1 : Tworzenie aplikacji

19

Niżej przedstawiony kod (pochodzący z pliku o nazwie HelloWorld.es) prezentuje użycie atry­ butów {zapisanych wytłuszcwnym drukiem) do określenia kluczy, kultury i wersji dla celów asemblacji. using System ; using System . Reflection ; [assembty : Asse11btyKeyName ( "MyKeys " ) J [asse11bty : Asse•blyCutt u re ( " " ) ] [asse11bty : As se11btyVersion ( " l . 8 . 8 . 8" ) J

public class HelloWo rld { public static void Main ( ) { Console . WriteLine ( " Hello , wo rld" ) ;

} } Aby utworzyć asemblację o silnej nazwie z kodu, podanego w przykładzie, należy utwo­ rzyć klucze silnych nazw i zapamiętać je w pliku o nazwie MyKeyFiłe, używając polecenia sn -k MyKeyFtłe.snk. Następnie należy zainstalować klucze w zasobniku CSP o nazwie MyKeys poleceniem sn -i MyKeyFile.snk. MyKeys. Teraz już można skompilować plik HelloWorld.es w asemblację o silnej nazwie przy użyciu polecenia csc HelloWorld.es.

Uwaga Można również tworzyć asemblacje o silnej nazwie przy użyciu programu Assembly Linker (al.exe), co umożliwi określenie informacji dotyczącej silnej nazwy w linii poleceń, zamiast umieszczania w kodzie atrybutów. Jest to dobre rozwiązanie, jeśli nie zachodzi potrzeba włączenia atrybutów silnej nazwy do pliku źródłowego, a do budowy dużych źródłowych struktur drzewiastych używa się skryptów. Dokumentacja SOK .NET Framework zawiera szczegółowe dane, dotyczące programu Assembly Linker.

1 .1 O Sprawdzenie, czy asemblacja o silnej nazwie nie została zmodyfikowana Problem Chcesz zweryfikować fakt ewentualnej modyfikacji asemblacji o silnej nazwie po jej utworzeniu.

Rozwiązanie Użyj narzędzia Strong Name (sn.exe) do weryfikacji silnej nazwy asemblacji.

20

C#

-

Księga przykładów

Omówienie Kiedy moduł runtime .NET ładuje asemblację o silnej nazwie, wycina zaszyfrowany skrót wbu­ dowany w asemblację, a następnie odtwarza go przy pomocy klucza publicznego, który rów­ nież jest wbudowany w asemblację. Następnie moduł runtime oblicza skrócony kod manifestu asemblacji i porównuje go z odtajnionym skróconym kodem. Proces ten służy do weryfikacji, czy po kompilacji asemblacja nie uległa zmianie. Jeżeli weryfikacja asemblacji wykonywalnej wykaże niezgodność silnej nazwy, moduł run­ time wyświetli okno dialogowe przedstawione na rysunku 1 -2. Jeśli kod będzie próbował załadować asemblację po nieudanej weryfikacji, moduł runtime przekaże informację

System.

10.FikloadException wraz z komunikatem „Strong name validation failed."

Rysunek 1 -2

Błąd wyświetlony po próbie uruchomienia zmodyfikowanej asemblacji o silnej nazwie .

Oprócz generowania i zarządzania kluczami silnych nazw (omówionego w przepisie 1 .8), narzędzie Strong Name umożliwia weryfikację asemblacji o silnej nazwie. Do stwierdzenia, czy asemblacja o silnej nazwie HelloWorld.exe jest niezmieniona, można użyć polecenia sn -vf

HelloWorłd.exe.

Przełącznik

v

zgłasza żądanie weryfikacji silnej nazwy określonej asemblacji

do narzędzia Strong Name, a przełącznik -fnarzuca weryfikację silnej nazwy nawet w przypadku, gdy była ona wzbroniona dla określonej asemblacji (można zabronić weryfikacji silnej nazwy dla określonych asemblacji poprzez użycie przełącznika - Vr , np. sn -Vr HelloWorld.exe). Szczegó­ łowe informacje o zabezpieczaniu się przed weryfikacją silnej nazwy zawiera przepis 1 . 1 1 . Jeśli asemblacja zostanie poddana weryfikacji silnej nazwy, zostaną wyświetlone następujące dane wyjściowe:

Mic rosoft ( R ) . NET F ramewo rk St rong Name Utility

Vers ion 1 . 1 . 4322 . 573

Copy right ( C ) Mic rosoft C o rpo ration 1998 - 2002 . All rig h t s rese rved . As sembly ' HelloWo rld . exe ' is valid Jeśli jednak asemblacja została zmodyfikowana, pokaże się zapis:

Mic rosoft ( R ) . NET F ramewo rk St rong Name Utility Ve rsion 1 . 1 . 4322 . 573 Copyright ( C ) Mic rosoft Corporation 1998 - 2002 . All rig h t s rese rved . Failed to ve rify a s sembly - - Unable to fo rmat e r ror message 8013 141A

1 .1 1 Opóźnienie podpisu asemblacji Problem Chcesz utworzyć asemblację o silnej nazwie, ale nie chcesz, żeby wszyscy członkowie zespołu projektowego mieli dostęp do składowej klucza prywatnego pary kluczy.

Rozdział 1 : Tworzenie aplikacji

21

Rozwiązanie Wytnij i rozpowszechnij składową klucza publicznego pary kluczy silnych nazw. Zastosuj instrukcje z przepisu 1 .9, opisujące sposób nadania twojej asemblacji silnej nazwy. Dodatkowo zastosuj do asemblacji atrybut System.Reflection.AssemblyDelaySignAftribute w celu zidentyfiko­ wania jej jako asemblacji podpisanej z opóźnieniem. Weryfikacji silnej nazwy asemblacji można zabronić przy użyciu przełącznika -Vr narzędzia Strong Name (sn.exe).

Omówienie Asemblacje odwołujące się do asemblacji o silnej nazwie zawierają token klucza publicznego asembłacji nadrzędnych. Oznacza to, że asemblacj i, do której ma nastąpić odwołanie, należy nadać silną nazwę przed jej wywołaniem. W środowisku projektowym, w którym asemblacje są regularnie przebudowywane, będzie to wymagało możliwości dostępu przez wszystkich projek­ tantów i testujących do danej pary kluczy silnych nazw, co stanowi główne źródło zagrożenia. Zamiast dystrybucji prywatnej składowej klucza danej pary kluczy silnych nazw do wszyst­ kich członków zespołu projektowego, .NET Framework udostępnia mechanizm o nazwie delay signing (opóźnienie podpisu), dzięki któremu można nadać swojej asemblacji silną nazwę częś­ ciowo. Asemblacja o nadanej częściowo silnej nazwie zawiera klucz publiczny i token klucza publicznego (wymagany przez wywoływane asemblacje), jednocześnie zawierając tylko puste miejsce na podpis, który jest generowany przy użyciu klucza prywatnego. Kiedy projekt jest gotowy, osoba odpowiedzialna za wykorzystanie i bezpieczeństwo danej pary kluczy silnych nazw podpisuje ponownie asemblację z opóźnionym podpisem, aby skom­ pletować jej silną nazwę. Podpis ten jest obliczany przy użyciu klucza prywatnego i włączany do asemblacji, gotowych do dystrybucji . Opóźnienie podpisu asemblacji wymaga dostępu tylko do składowej klucza publicznego danej pary kluczy silnych nazw. Dystrybucja klucza publicznego nie pociąga za sobą żadnego ryzyka, kierownik podpisujący projekt może zatem uczynić klucz publiczny powszechnie dostępnym dla wszystkich projektantów. Aby wyciąć składową klucza publicznego z pliku klucza o silnej nazwie MyKeys.snk i zapisać go do pliku o nazwie MyPublicKey.snk, należy użyć polecenia sn -p MyKeys.snk MyPublicKey.snk. Po zapamiętaniu swojej pary kluczy silnych nazw w zasobniku kluczy CSP o nazwie MyKeys, należy wyciąć klucz publiczny i przenieść go do pliku o nazwie MyPublicKey.snk, przy użyciu polecenia sn -pe MyKeys MyPublicKey.snk. Atrybuty z przepisu 1 .9 są używane do zadeklarowania wersji i kultury asemblacji oraz lokalizacji klucza publicznego. Należy także zastosować atrybut AssemblyDelaySign(true) do asemblacji - powiadomi on kompilator o postulowanym opóźnieniu podpisania asemblacji. W poniższym kodzie wytłuszczono atrybuty, które powinny być użyte do opóźnienia podpisu asemblacji, gdy klucz publiczny znajduje się w pliku o nazwie MyPublicKey.snk. using System ; using System . Reflection ; [assembly : AssemblyKeyfile ( "MyPublicKey . snk" ) ] [assembly : Asse11blyCult u re ( '"' ) ] [assembly : AssemblyVe rsion ( "l . 8 . 8 . 8" ) ) [assembly : AssemblyDelaySign ( t rue ) J

public c l a s s HelloWo rld {

22

C#

Księga przykładów

-

public static void Main ( ) { Console . W riteline ( " Hello , world " ) ;

} } Kiedy moduł runtime spróbuje załadować asemblację z opóźnionym podpisem, zidentyfikuje ją jako asemblację o silnej nazwie i spróbuje ją zweryfikować (prrepis 1 . 1 O). Ponieważ brak w niej teraz podpisu cyfrowego, wykonywanie pl'7.CZ moduł runcime weryfikacji silnej nazwy asembłacji powinno być zabronione przy użyciu polecenia sn -Vr HelloWorld.exe. Kiedy projekt jest gotowy, należy powtórnie podpisać asemblację, aby skompletować jej silną nazwę. Narzędzie Strong Name umożliwia wykonanie tej czynności bez zmiany kodu źródło­ wego lub powtórnej kompilacji asemblacji, jednak konieczny jest w tym przypadku dostęp do klucza prywatnego pary kluczy silnych nazw. Aby powtórnie podpisać asemblację o nazwie HelłoWorld.exe parą kluczy prrechowywaną w pliku MyKeys.snk, należy użyć polecenia sn -R HelloWorld.exe MyKeys.snk Jeśli klucze są zapamiętane w zasobniku kluczy CSP o nazwie MyKeys, właściwe polecenie będzie miało postać sn -Re HelloWorld.exe MyKeys. Po powtórnym podpisaniu asemblacji należy przywrócić weryfikację silnej nazwy przy uży­ ciu przełącznika narzędzia Strong Narne -Vu, jak w sn -Vu HelloWorld.exe. Aby nie dopuścić do weryfikacji wszystkich asemblacji, dla których weryfikacja silnej nazwy jest zabroniona, należy użyć polecenia sn -Vx. Można również sporządzić wykaz asemblacji, dla których weryfikacja jest zabroniona, używając polecenia sn -VI.

Uwaga Przy używaniu asemblacji z opóźnionym podpisem można porównać różne wdrożenia tej samej asemblacji, by mieć pewność, że jedyną ich różnicę stanowią pod­ pisy. Jest to możliwe tylko wtedy, gdy asembłacja z opóźnionym podpisem została pod­ pisana ponownie przy użyciu przełącznika narzędzia Strong Name -R. Do porównania dwóch asembłacji należy użyć polecenia sn -D assembly1 assembly2.

1 .1 2 Podpisywanie asemblacji podpisem cyfrowym Authenticode Problem Chciałbyś podpisać asemblację przy pomocy Authemicode, aby jej użytkownicy mieli pewność, że to ty jesteś jej wydawcą i że asernbłacja nie uległa zmianie po podpisaniu.

Rozwiązanie Użyj narzędzia File Signing (signcode.exe) do podpisania asemblacji za pomocą certyfikatu SPC (Software Publisher Certificare - certyfikat wydawcy oprogramowania) .

Rozdział

1 : Tworzenie

aplikacji

23

Omówienie Silne nazwy zapewniają unikalną tożsamość asemblacji i dowodzą niepodważalnie jej integral­ ności, ale nie pozwalają udowodnić, kto jest wydawcą danej asemblacji. Platforma .NET Frame­ work pozwala na wykorzystanie techniki Authenticode do podpisywania asemblacji. Umożliwia to użytkownikom danej asemblacji zarówno możliwość stwierdzenia, kto jest ich wydawcą, jak i potwierdzenie ich integralności. Podpisy w Authenticode stanowią również dla podpisanej asemblacji dokumentację, która może być wykorzystana do konfigurowania zasad zabezpieczeń dostępu do kodu (dokumentacja asembłacji została omówiona w przepisach 1 3.9 i 1 3 . 1 O). Aby podpisać asemblację podpisem Authenticode, trzeba mieć SPC opublikowany przez znany urząd certyfikacji (certificate authority - CA) . CA jest instytucją upoważnioną do wyda­ wania certyfikatów SPC Qak i wielu innych ich typów) do użytku pojedynczych osób lub insty­ tucji. Przed wydaniem certyfikatu CA powinno potwierdzić tożsamość występującego i zawrzeć z nim kontrakt gwarantujący, że nie nadużyje on otrzymanego certyfikatu. Aby otrzymać SPC, należy przejrzeć listę Microsoft Root Cercificate, dostępną pod adresem

http://msdn. microsoft. com/libraryldefouit.asp?url=llibrarylen-us/dnsecurelhtm/Jrootcertprog.asp. Znajduje się tam również lista instytucji CA, które mogą wydawać SPC. Dla celów testowych można utworzyć testowy SPC, posługując się procesem opisanym w przepisie 1 . 1 3, jednak nie można rozpowszechniać oprogramowania podpisanego takim certyfikatem. Większość odpo­ wiedzialnych użytkowników nie uzna podpisanych w ten sposób asemblacji, ponieważ certyfi­ kat nie wstał wydany przez wiarygodny CA. Po uzyskaniu SPC można wykorzystać narzędzie File Signing do podpisania asemblacji. Narzędzie to utworzy podpis cyfrowy używając klucza prywatnego związanego z SPC, a następ­ nie włączy podpis oraz � publiczną SPC do asemblacji (w tym odpowiedni klucz publiczny) . Podczas weryfikacji danej asemblacji klient rozszyfrowuje zaszyfrowany skrót kodu, posługując się kluczem publicznym tej asemblacji, wylicza ponownie skrót asemblacji i porównuje obydwa wyniki, żeby sprawdzić ich identyczność. Jeśli skrócone kody się zgadzają, użytkownik będzie miał pewność, kto podpisał asemblację i że nie zmieniła się ona od momentu podpisu. Aby podpisać podpisem Authenticode asemblację o nazwie MyAssembly.exe używając SPC, znajdującego się w pliku o nazwie MyCert.spc oraz klucza prywatnego, znajdującego się w pliku o nazwie MyPrivateKey.pvk, należy użyć polecenia signcocłe -spe MyCert.spc -v MyPrivat� Key.pvk MyAssembly.exe. Narzędzie File Signing wyświetli okno dialogowe przedstawione na rysunku 1 -3, monitując o podanie hasła zabezpieczającego klucz prywatny, zapamiętany w pliku MyPrivateKey.pvk. l't1°dh'

Private Key Pussword

Key.

Subjoct Key

.(;onlim Paisword:

. i.•••••oo•

Rysunek 1 -3

l")f,

------.

--------------·--'

Narzędzie File Signing żąda hasła przy dostępie do kluczy prywatnych, przechowywanych jako file based.

24

C# - Księga przykładów Można również mieć dostęp do kluczy i certyfikatów przechowywanych w magazynach klu­ czy i cerryfikarów. Tabela 1 -2 podaje najczęściej używane przełączniki narzędzia File Signing. Ich pełna lista znajduje się w dokumentacji NET Framework SOK.

Tabela 1 -2

Powszechnie używane przełączniki narzędzia File Signing

Przełącznik Opis

-k

Określa nazwę zasobnika kluczy CSP, w którym przechowywany jest dany klucz prywatny SPC.

-s

Określa nazwę magazynu certyfikatów, w której zapisany jest dany SPC.

-spe

Określa nazwę pliku, w którym przechowywany jest dany SPC.

-v

Określa nazwę pliku, w którym przechowywany jest prywatny klucz SPC.

Przy podpisywaniu asemblacji złożonej z wielu plików należy określić nazwę pliku zawierają­ cego manifest asemblacji. Przy jednoczesnym zastosowaniu sil nej nazwy i podpisaniu asemblacji za pomocą Auchenricode, należy najpierw nadać swojej asemblacji silną nazwę (przepis 1 .9). Aby sprawdzić ważność pliku opatrzonego podpisem Authenticode, należy użyć narzędzia Certificate Verification (chktrusc.exe) . Na przykład, żeby przetestować MyAssembly.exe, można wykorzystać polecenie chktrust MyAssembly.exe. Jeżeli system komputerowy nie został jeszcze skonfigurowany rak, aby respektował SPC używany do podpisywania asemblacji, pojawi się w rym momencie okno dialogowe podobne do przedstawionego rysunku 1 -4 . Zawiera ono informację o wydawcy danej asemblacji i umożliwia jego uwiarygodnienie. W rym przypadku przedstawiony certyfikat jest certyfikatem testowym, utworzonym przy użyciu procesu opisa­ nego w przepisie 1 - 1 3. Secu1 lty

\".'.imin�

�·

IłSI

Cl ll 1 11 U A I ł

r8:J

....

.,, unknown doteJline and cillrłUed by.

Do l/OU wn to instal and M ''M�.e.e" s9>ed

al

� WAANINGI!boW!eJboabęcnyMdg!ec!uWqaTEST CEATIFICATE

Ca­ BHV: eneodif"19Dtyl� •http: I I aeho• • • · >Oal. o o e p . org/ soep/ oneodinig/ '"').

xal n• : xed

BNC.a "http: // ecih„ a a . xa.l. et o a p . orq/ eoap/oncoding / "

< BOAP- l!UłV: !lody> < e l : A r r ayLiot id2 '" r e f- 1 •

xal n•1 al:s"http: //•eh-••· aiero•oft. eoa/elr/ne/Byetea. Colloction:ii ">

< -.a.t••• lu . e t• '"#r e f-2 "/ > <

o.i.te>)< /

< v•c a i o <

•i-ze>

n>:i'< / verei on> 7a 1 1 ArrayLio't>

>..nd.y

. . .

Gr

3 Domeny aplikacji, odbicie i metadane Możliwość sprawdzenia metadanych i rypów, a takie manipulowania nimi podczas wykonania programu, zwiększa moc i elasryczność systemu Microsoft .NET Framework. Przepisy zamiesz­ czone w rym rozdziale doryczą popularnych aspektów aplikacji domen, odbicia i metadanych, takich jak: •

Tworzenie i zwalnianie domen aplikacji (przepisy 3 . 1 i 3.9).



Praca z typami i obiektami przy użyciu wielu domen aplikacji (przepisy 3.2, 3.3, 3.4 i 3.8).



Praca z informacją Tjpe (przepisy 3. 1 0 i 3. 1 1 ) .



Dynamicznie ładowanie asemblacji i tworzenie obiektów podczas wykonania programu (przepisy 3.5, 3.6, 3.7 i 3. 1 2).



Tworzenie i sprawdzanie atrybutów użytkownika (przepisy 3. 1 3 i 3 . 1 4).

3.1 Tworzenie domeny aplikacji Problem Chcesz utworzyć nową domenę aplikacji.

Rozwiązanie Użyj starycznej metody CreateDomain klasy System.AppDomain.

54

C#

-

Księga przykładów

Omówienie Najprostsza przeciążona metoda CreateDomain przyjmuje jako argument pojedynczy łańcuch, określający przyjazną dla użytkownika nazwę dla nowej domeny aplikacji. Inne przeciążenia pozwalają określić ustawienia ewidencji i konfiguracji dla nowej domeny aplikacji. Ewidencję określa się przy użyciu obiektu System.Security.Policy.Evidmce, w przepisie 1 3. 1 1 zostały omó­ wione wyniki zastosowanej ewidencji przy tworzeniu domeny aplikacji. Ustawienia konfigura­ cji zostały wyspecyfikowane przy użyciu obiektu System.AppDomainSetup. Klasa AppDomainSetup stanowi dla domeny aplikacji zasobnik informacji o konfiguracji. Tabela 3- 1 pokazuje niektóre właściwości klasy AppDomainSetup, używane najczęściej przy tworzeniu domen aplikacji. Po utworzeniu domeny aplikacji właściwości te są dostępne poprzez poła obiektu AppDomain, a niektóre z nich są podatne na modyfikację podczas wykonania programu (wyczerpujące omówienie tego tematu znajduje się w dokumentacji SOK .NET Fra­ meworks, dotyczącej klasy AppDomain).

Tabela 3-1

Używane powszechnie właściwości AppDomainSetup

WłaściwoU

Opis

ApplicationBase

Katalog, który moduł CLR będzie wstępnie przeszukiwał, aby rozwiązać asemblacje prywatne, omówione w przepisie 3.5. ApplicationBase jest de facto katalogiem najwyższego poziomu (root) dla wykonanych aplikacji. Domyślnie jest to katalog zawierający asembłację. Po utworzeniu daje się odczytać przy użyciu właściwości AppDomain.

BaseDirectory. ConftgurationFik

Nazwa pliku konfiguracji, używana przez kod załadowany do domeny aplikacji. Po utworzeniu daje się odczytać przy użyciu metody AppDomain. GetData z kluczem APP CONFIG FILE _

_

.

Disa/JqwPublisher­ Sprawdza, czy sekcja zasad wydawcy w pliku konfiguracji aplikacji jest brana pod uwagę podczas decyzji, którą wersję asemblacji o silnej nazwie Policy należy włączyć. Zasady wydawcy wstały omówione w przepisie 3 . 5 .

PrivateBinPath

Lista rozdzielonych średnikami katalogów, używanych przez moduł run­ time przy wyszukiwaniu prywatnych asemblacji . Katalogi te są względne w stosunku do katalogu wyspecyfikowanego w ApplicationBase. Do odczytu po utworzeniu domeny aplikacji przy użyciu AppDomain. RelativeSearchPath property. Opcja dająca się modyfikować podczas wykonania programu przy użyciu metod AppendPrivatePath i Ckar­

PrivatePath. Poniższy przykład demonstruje tworzenie i konfigurowanie domeny aplikacj i. li Two rzenie instancj i obiektu AppDomainSetup . AppDomainSet up setupinfo = new AppDomainSet u p ( ) ; li Kon figu rowanie info rma c j i wstępnych domeny aplika c j i . setupinfo . Appl icationBase = @ " C : \MyRootD i recto ry " ; setupinfo . Configu rationFile = " MyApp . config " ;

Rozdział 3: Domeny aplikacji, odbicie i metadane setuplnfo . P rivateBinPath

=

55

" bin ; plugin s ; exte rnal " ;

li Two rzenie nowej domeny aplikacj i p rzekazuj ącej n u l l j a ko a rgument li dowodowy . Należy zapamięta ć odnośnik do nowego AppDomain , gdyż n ie można li go odzyskać w żaden inny sposób . AppDomain newDomain

=

AppDomain . C reateDomain (

"My New AppDoma in " , new System . Secu rity . Policy . Evidence ( ) , setuplnfo ) ;

Wskazówka Powinno zostać zachowane odniesienie do obiektu AppDomain podczas jego tworzenia, ponieważ nie istnieje mechanizm przyporządkowania istniejących domen aplikacji z poziomu kodu, który jest aktualnie używany.

3.2 Przekazanie obiektów poza granice domeny aplikacji Problem Chcesz przekazać obiekty poza granice domeny aplikacji jako argumenty lub wartości zwrotne.

Rozwiązanie Użyj obiektów przekazywanych przez wartość lub poprzez odniesienie (marshal-by-value i mar­ shal-by-reference) .

Omówienie Zdalny system .NET Remoting (omówiony w rozdziale 12) pozwala na bezpośrednie przeka­ zywanie obiektów poza granice domen aplikacji. Jednakie dla osób nie znających .NET Remo­ ting wyniki mogą się okazać zupełnie różne od spodziewanych. Najbardziej mylący aspekt przy korzystaniu z wieloaplikacyjnych domen stanowi interakcja z systemem .NET Remoting i spo­ sób, w jaki obiekty pokonują granice domen aplikacji. Wszystkie typy dzielą się na trzy kategorie: nie-zdalne

(nonremotabk), zawiadywane poprzez

wartość ( marshai-by-value M BV) i zawiadywane przez odniesienie { marshai-by-reference- MBR). -

Typy nie-zdalne nie mogą pokonywać granic domeny aplikacji i nie mogą być użyte jako argumenty lub wartości, zwracane przez granice aplikacji w wywołaniach domen. Typy te zostały omówione w przepisie 3.4. Typy MBV są serializowalne. Kiedy obiekt MBV zostaje przekazany przez granicę domeny aplikacji jako argument lub wartość zwrotna, system .NET Remoting serializuje aktualny stan

56

C#

-

Księga przykładów

obiektu, przekazuje go do docelowej domeny aplikacji i tworzy nową kopię obiektu o takim samym scanie jak oryginalny. Powoduje co utworzenie kopii obiektu M BV, istniejącego teraz w obydwu domenach aplikacji. Obie ce instancje są początkowo identyczne, chociaż są nie­ zależne: zmiany dokonane w jednym z nich nie powodują zmian drugiego. Niżej podano przykład serialiwwalnego typu o nazwie Employee, którego wartość jest przekazywana poprzez granice domeny aplikacji (w przepisie 1 6. 1 podane są szczegóły dotyczące tworzenia typów serializowal nych) . [ System . Se rializable ) public class Employee {

} Typy MBR tworzą klasy pochodzące od System.MarshalByRejObject. Kiedy obiekt MBR zostaje przekazany przez granice domeny aplikacji jako argument lub wartość zwrotna, system .NET Remoting utworzy wartość proxy w docelowej domenie aplikacji - wartość reprezentującą obiekt M BR. Dla każdej klasy w docelowej domenie aplikacji, proxy wygląda i zachowuje sic; jak zdalny obiekt MBR, który reprezentuje. W rzeczywistości, kiedy wstaje wywołane, system .NET Remoting przekazuje wywołanie i jego argumenty w sposób niewidoczny do zdal nej domeny aplikacji i wykonuje wywołanie oryginalnego obiektu. Wszystkie wyniki są zwra­ cane do podmiotu wywołującego za pośrednictwem proxy. Niżej przedstawiono wersję klasy Employee, która wstała przekazana przez wywołanie, a nie poprzez wartość (przepis 1 2.7 podaje szczegóły dotyczące utworzenia typów M BR). public class Employee : System . MarshalByRefObj ect {

}

3.3 Unikanie ładowania zbędnych asemblacji do domen aplikacji Problem Chcesz przekazać wywołanie obiektu z kodu do kodu, pracującego w różnych domenach apli­ kacji, ale nie chcesz, żeby moduł run time CLR ładował metadane typu obiektu do pośredniczą­ cych domen aplikacji.

Rozwiązanie Opakuj odniesienie obiektu w klasę System.Runtime.&moting. ObjectHandle i rozpakuj je dopiero wtedy, gdy masz uzyskać dostęp do obiektu.

Rozdział 3: Domeny aplikacji, odbicie i metadane

57

Omówienie Kiedy obiekt MBV wstaje przekazany przez granice domeny aplikacji, moduł runtime two­ rzy nową instancję tego obiektu w domenie aplikacji. Oznacza to, że musi on przeładować asemblację zawierającą metadane tego typu do domeny aplikacji. Przekazywanie odniesień MBV przez granice pośrednich domen aplikacji może spowodować ładowanie do domen apli­ kacji zbędnych asemblacji przez moduł runtime. Po załadowaniu asemblacje te nie mogą być zwolnione bez zwolnienia zawartości domeny aplikacji (przepis 3.9). Klasa ObjectHandle umożliwia opakowanie odniesienia obiektu, by można było przekazywać je między domenami aplikacji bez ładowania przez moduł runtime dodatkowych asemblacji. Kiedy obiekt osiągnie docelową domenę aplikacji, można rozpakować odniesienie obiektu, powodując załadowanie przez moduł runcime wymaganej asemblacji, co pozwoli na normalny dostęp do obiektu. Aby opakować obiekt Qak w System.Data.DataSet), można użyć następującej instrukcji: li Two rzenie nowego DataSet . System . Data . DataSet datal = new System . Data . DataSet ( ) ; li Konfigu rowanie/wypełnianie DataSet .

li Opakowywanie DataSet . System . Runt ime . Remoting . Obj ectHandle obj Handle

=

new System . Runt ime . Remoting . Obj ectHandle (data l ) ;

Aby rozpakować obiekt, należy użyć metody ObjectHandle. Unwrap i dopasować zwrócony obiekt do właściwego typu, jak to pokazano niżej. li Rozpakowywanie DataSet System . Data . DataSet data2 = ( System . Data . DataSet ) obj Handle . Unwra p ( ) ;

3.4 Utworzenie typu, który nie może przekroczyć granic domeny aplikacji Problem Chcesz utworzyć typ w taki sposób, żeby jego instancje były niedostępne dla kodu z innych domen aplikacji.

Rozwiązanie Upewnij się, że typ nie jest zdalny poprzez stwierdzenie, że nie jest on serializowalny i nie wywo­ dzi się z klasy MarshalBy&JObject.

58

C#

-

Księga przykładów

Omówienie Czasami warto się upewnić, czy instancje danego typu nie mogą wykraczać poza granice domen aplikacj i. Aby utworzyć typ nie-zdalny, należy sprawdzić, czy nie jest on serializowalny i czy nie pochodzi (wprost lub nie wprost) od klasy MarshalBy&JObject. Te czynności zapewniają, że stan obiektu nie będzie nigdy dostępny spoza domeny aplikacji, w której obiekt został utworzony. Takie obiekty nie mogą być używane ani jako argumenty, ani wartości zwracane w wywołaniach metod przekraczających domeny aplikacji. Sprawdzenie, czy typ nie jest serializowalny nie jest trudne, ponieważ klasa ta nie dziedziczy zdolności klasy macierzystej. Można się o tym przekonać sprawdzając, czy ma ona dołączony atrybut System.Serializabl.eAttribute do typu deklaracj i. Upewnienie się, że klasa nie może być przekazywana w odniesieniu, wymaga nieco więcej uwagi. Wiele klas z biblioteki klas .NET pochodzi bezpośrednio lub pośrednio od Marshal­ ByRejObject, konieczne jest więc upewnienie się, czy przez nieuwagę dana klasa nie została wyprowadzona od jednej z nich. Zwykle używane podstawowe klasy pochodzą od MarshalBy­ &JObject, w tym System. ComponentModeL Component, System.JO.Stream, System.IO. TextReader, System.IO. TextWriter, System.NET.WebRequest i System.Net. Web&sponse (w dokumentacji SOK . NET Framework należy wyszukać listę klas wyprowadzonych z Marsha/ByRejObject).

3.5 Ładowanie asemblacji do aktualnej domeny aplikacji Problem Chcesz załadować asemblację podczas wykonania programu do aktualnej domeny aplikacji.

Rozwiązanie Użyj metod statycznych Load lub LoadFrom klasy System.Reflection.Assembly.

Omówienie Moduł CLR automatycznie załaduje asemblacje zidentyfikowane w czasie tworzenia, jako wywołane przez utworzoną asemblację. Można bezpośrednio poinstruować moduł runtime, żeby załadował asemblacje. Obie te metody: Load i LoadFrom powodują przeładowanie asem­ blacji przez moduł runtime reprezentujący nowo załadowaną asemblację do aktualnej domeny aplikacj i i zwracają instancję Assembfy. Różnicę między tymi metodami stanowią argumenty, które muszą być dostarczone w celu zidentyfikowania asemblacji, która ma być załadowana, a także proces używany przez moduł runtime w celu ulokowania określonej asemblacji.

Rozdział 3: Domeny aplikacji, odbicie i metadane

59

Metoda Load zapewnia przeciążenie, pozwalające określić załadowaną asemblację przy uży­ ciu jednej z następujących opcji: •

łańcucha (string), zawierającego wyświetlaną nazwę assemblacji display name, w całości lub w części kwalifikowaną,



nazwy System.Ref/ection.AssemblyName, zawierającej szczegółowe dane dotyczące asemblacji,



tablicy bajtowej byte, zawierającej pierwotne bajty tworzące asemblację.

Najczęściej do załadowania asemblacji używa się nazwy wyświetlanej. W pełni kwalifikowana wyświetlana nazwa zawiera tekst, wersję, kulturę i token asemblacji, rozdzielone przecin­ kami (na przykład System . Data , Ve r s ion=l . 0 . 5000 . 0 , Culture=neutral , PublicKeyToken= b77a5c561934e089) . Aby wyspecyfikować asemblację, która nie ma silnej nazwy, należy użyć PublicKeyToken=null. Można również wyspecyfikować częściową nazwę wyświetlaną, ale jako minimum należy wtedy określić nazwę asemblacj i (bez rozszerzenia pliku). Podany niżej kod pokazuje różne sposoby użycia metody Load. li Ładowanie a semblacj i System . Data p rzy użyciu w pełn i kwalifikowanej li nazwy wyświetlanej . st ring namel = "System . Data , Version= l . 0 . 5000 . 0 , "

+

" C u l t u re=neut ral , Publ icKeyToken=b77a5c561934e089 " ; Assembly a l = Assembly . Load ( namel ) ; li Ładowanie asemblacj i System . Xml p rzy u życiu AssemblyName . AssemblyName name2 = new AssemblyName ( ) ; name2 . Name = " System . Xml " ; name2 . Ve rs ion = new Ve rsion ( l , 0 , 5000 , 0 ) ; name2 . Cu l t u reinfo = new Cult u reinfo ( " " ) ; name2 . SetPublicKeyTo ken ( new byte [ J {0xb7 , 0x7a , 0x5c , 0x56 , 0x l9 , 0x34 , 0xe0 , 0x89} ) ; Assembly a2 = As sembly . Load ( name2 ) ; li Ładowanie a semblacj i SomeAssembly p rzy użyciu częściowej nazwy Assembly a3 = As sembly . Load ( " SomeAs sembly " ) ;

W odpowiedzi na wywołanie Load, moduł runtime rozpoczyna ekstensywny proces lokalizacji i ładowania określonej asemblacji . Więcej szczegółów zawiera dokumentacja SDK .NET Fra­ mework. W skrócie:

1 . Przy wyspecyfikowaniu asemblacji o silnej nazwie, metoda Load zastosuje zasady wersji i zasady wydawcy, aby zapewnić wywołanie takiej wersji asemblacji, która będzie odpowied­ nia dla innej wersji. Zasady wersji są określone w danym komputerze lub w pliku konfi­ guracji apłikacj przy użyciu elementów . Zasady wydawcy są określone w asemblacjach specjalnych zasobów, zainstalowanych w globalnym buforze asemblacji (GAC) .

2. Kiedy moduł runtime ustali prawidłową wersję asemblacji, która ma być użyta, spróbuje ·

załadować asemblację o silnej nazwie z GAC.

3. Jeśli asemblacja nie ma silnej nazwy albo nie znajduje się w GAC, moduł runtime wyszukuje pasujące elementy w plikach konfiguracyjnych danego komputera i aplikacji.

60

C#

Księga przykładów

-

mapuje nazwę asembłacji na plik lub URL . Jeśli asembłacja ma silną może odwoływać się do dowolnego miejsca, łącznie z internetowymi adre­ sami URL; jeśli nie, musi wskazywać katalog względny do katalogu aplikacji. Jeśli asemblacja nie występuje pod określonym w ten sposób adresem, metoda Load wywoła wyjątek System.10.FileNotFoundException. Element

nazwę,

4. Jeśli brak jest elementów odpowiednich dla żądanej asemblacji, moduł runtime próbuje zlokaliwwać asemblację przy użyciu funkcji probing. Funkcja ta szuka pierwszego pliku z nazwą asembłacji (z rozszerzeniem .dłl albo .exe) w następujących lokalizacjach: O Katalog aplikacji najwyższego poziomu (root). O Katalogi poniżej najwyższego poziomu, zgodne z nazwą i kulturą asemblacji. O Katalogi poniżej najwyższego poziomu, wyspecyfikowane na prywatnej ścieżce binarnej

binpath. Metoda

Load jest

najłatwiejszym sposobem lokalizacji i załadowania asemblacji, jednak może

być czasochłonna, jeśli moduł runtime musi przed startem przeszukiwać różne katalogi asem­ błacji o słabych nazwach. Metoda

LoadFrom

pozwala na przeładowanie asemblacji z określo­

nego położenia. Jeśli określony plik nie wstaje znaleziony, moduł runtime wywoła wyjątek

FileNotFoundException.

Moduł runcime nie będzie próbował zlokalizować asembłacji w ten

Load - LoadFrom nie przewiduje wsparcia dla posługiwania się ani wstępnym wyszukiwaniem. Poniższy kod demonstruje użycie metody LoadFrom do załadowania asemblacji o nazwie c:\shared\MySha­ redAssembly.dłl. W przeciwieństwie do metody Load, LoadFrom wymaga podania rozszerzenia

sam sposób, jak przy metodzie

pamięcią GAC, zasadami, elemenami

pliku asemblacji.

li Ładowanie a semblacj i c : \ s ha red\MySha redAssembly . dll As sembly a4

=

As sembl y . LoadF rom ( @ " c : \ s h a red\MySha redAs sembly . dll " ) ;

3.6 Wykonanie asemblacji w innej domenie aplikacji Problem Chcesz wykonać asemblację w domenie aplikacji innej, niż aktualna.

Rozwiązanie Wywołaj metodę

ExecuteAssembly

obiektu

i określi nazwę wykonalnej asembłacj i.

AppDomain,

który reprezentuje domenę aplikacji

Rozdział 3: Domeny aplikacji, odbicie i metadane

61

Omówienie Najłatwiejsze rozwiązanie zadania załadowania i uruchomienia istniejącej już asemblacji wyko­ nywalnej w domenie aplikacji stanowi metoda

ExecuuAssembly. Ma ona (string), zawierający

odmiany. Najprostsza z nich pobiera tylko łańcuch

cztery przeciążone nazwę wykonalnej

asemblacji, która ma być uruchomiona; argumentem może być nazwa lokalnego pliku albo

URL. Pozostałe przeciążenia ExecuteAssembly umożliwiają wyspecyfikowanie ewidencji dla tej asemblacji (przepis 1 3 . 1 0) oraz argumentów przekazywanych do punktu wejścia asemblacji

(entry point), co stanowi ekwiwalent przekazania argumentów z linii poleceń. Metoda ExecuteAssembly załadowuje określoną asemblację i wykonuje metodę zdefiniowaną w metadanych jako punkt wejścia asemblacji (zazwyczaj metodę Main). Jeśli dana asemblacja nie jest wykonalna, ExecuteAssem bly wstrzyma wykonanie programu, wywołując wyjątek Sytem. Runtime.lnteropServices. COMException. Moduł CLR nie rozpoczyna wykonania asemblacji jako nowego wątku, scerowanie nie może więc być przekazane z powrotem, dopóki istnieje nowo wykonana asemblacja. Ponieważ metoda ExecuteAssembly załadowuje asemblację posługując się informacją częściową (tylko nazwą pliku), moduł CLR nie może użyć pamięci GAC, ani metod przeszukiwania do zlokalizowania asemblacji (dodatkowe informacje podaje przepis 3.5).

ExecuteAssembly do załadowania ExecuteAssemblyExample tworzy obiekt AppDomain i wyko­ nuje się w nim przy użyciu metody ExecuteAssembly. W efekcie tego działania powstaną dwa egzemplarze asemblacji ExecuteAssemblyExample, które zostaną załadowane do dwóch różnych Niżej podany przykład pokazuje wykorzystanie metody

i uruchomienia asemblacji. Klasa

domen aplikacji.

using System ; public c l a s s ExecuteAssemblyExample { public static void Main ( s t ring [ ] a rg s ) { li Na użytek tego p rzykładu , j eżeli asemblacj a wykonywana j e st li w domenie apl i kacj i o nazwie " NewAppDomain " , nie j est li tworzona nowa domena . Pozwala to un iknąć nies kończonej pętli li two rzenia domen apl ikacj i . i f ( AppDomain . Cu r rentDomain . F riendlyName ! = " NewAppDomain " ) { li Two rzenie nowej domeny aplikacj i AppDomain domain = AppDomain . C reateDoma in ( " NewAppDomain " ) ; li Wykonanie asemblacj i w nowej domenie apl ikacj i li i p rzekazanie tabeli a rg umentów wie rsza polecenia . domain . ExecuteAs sembly ( " ExecuteAssemblyExample . exe" , nul l , a rg s ) ;

} li Wyświetlenie a rgumentów wie rsza polecenie na e k ra n i e , pop rzedzonych li nazwą AppDomain . f o reach ( s t ring s in a rg s ) { Console . Writeline ( AppDomain . Cu rrentDomain . F riendlyName

} } }

+

"

" + s);

62

C#

Księga przykładów

-

3. 7 Utworzenie instancji typu w innej domenie aplikacji Problem Chcesz utworzyć instancję typu w domenie aplikacji innej niż aktualna.

Rozwiązanie Creatdnstance

Wywołaj jedną z metod:

lub

Create!nstanceFrom

obiektu

AppDomain,

który

reprezentuje docelową domenę aplikacji.

Omówienie Metoda

ExecuteAssembly,

omówiona w przepisie 3.6, jest prosta w użyciu, ale przy projekto­

waniu złożonych rozwiązań korzystających z domen aplikacji potrzebna jest większa kontrola nad ładowaniem asemblacj i, tworzeniem instancji typów i inicjowaniem pół obiektu w obrębie domeny aplikacji. Metody

Create!nstance i Create!nstanceFrom

mają rozmaite przeciążone odmiany, zapewnia­

jące szczegółową kontrolę procesu tworzenia instancji obiektu . Najprostsze przeciążenia zakładają użycie domyślnego konstruktora typu, ale obie te metody implementują przeciążenia umożliwia­ jące dostarczenie argumentów, pozwalających na wykorzystanie innego konstruktora. Metoda

Create!nstance załadowuje asemblację o określonej nazwie do domeny aplikacj i przy

użyciu procesu opisanego dla metody Assembly.Load, w przepisie 3.5. Następnie metoda ta two­

Object­ Create!nstanceFrom również tworzy nazwie i zwraca opakowane odniesienie obiektu ObjectHandle, jed­

rzy instancję typu o danej nazwie i zwraca odsyłacz do nowego obiektu, opakowane w

Handle

(co zostało omówione w przepisie 3.3). Metoda

instancję typu o określonej nakże

Create!nstanceFrom

załadowuje określoną asembłację do domeny aplikacji przy użyciu

procesu podanego w przepisie 3.5 dla metody Assembly.LoadFrom.

Wskazówka

Metoda AppDomain dostarcza również wygodnych metod Create­ lnstanceAndUnwrap i CreatelnstanceFromAndUnwrap, które automatycznie wydobywają odniesienie do obiektu z utworzonymi instancjami ze zwróconego obiektu ObjectHandle, należy jednak zwrócony obiekt dopasować do właściwego typu.

Użycie metody

Createlnstance lub CreatelnstanceFrom w celu utworzenia instancji rypów MBV

w innej domenie aplikacji spowoduje wprawdzie stworzenie obiektu, ale zwrócone odniesie­ nie

object nie będzie się odnosiło do tego obiektu. Ze względu

na sposób przkraczania granic

domeny aplikacji przez obiekt MBV, odniesienie to będzie dotyczyło kopii obiektu, utworzo­ nej automatycznie w lokalnej domenie aplikacji . Zwrócone odniesienie będzie się odnosiło do obiektu w innej domenie aplikacji tylko wtedy, gdy został utworzony typ MBR (przepis 3.2 podaje więcej informacji na temat typów MBV i MBR).

Rozdział 3: Domeny aplikacji, odbicie i metadane

63

Popularną techniką upraszczającą zarządzanie domenami aplikacji jest zastosowanie klasy kontrolera. Ta klasa stanowi niestandardowy typ MBR Po utworzeniu domeny aplikacji, należy utworzyć i nstancje swojej klasy kontrolera w domenie aplikacji przy użyciu

Createlnstance. Klasa

kontrolera zaimplementuje wymaganą przez ucworwną aplikację funkcjonalność, potrzebną do manipulowania aplikacją i jej zawartością. Może to obejmować przeładowanie asemblacji, tworzenie dalszych domen aplikacji, czyszczenie przed usunięciem domeny aplikacji lub wyli­ czanie elementów programu (czego zazwyczaj nie można wykonać spoza domeny aplikacji). Poniższy przykład demonstruje zastosowanie uproszczonej klasy kontrolera o nazwie Plugin­

Manager.

PluginManager umożliwia

tworzenie

włączanie i wyłączanie wtyczek

(plug-im)

Po utworzeniu instancji w domenie aplikacji,

instancji klas implementujących interfejs

!Plugin,

i zwrócenie bieżącej listy załadowanych elementów.

using System ; u sing System . Reflection ; us ing System . Collections ; using System . Collection s . Specialized ; li Wspólny interfej s , któ ry musi być zaimplementowany w każdej wtycz ce . public inte rface !Plugin { void S t a rt { ) ; void Stop ( ) ;

} li P rosta implementacj a !Plugin do demonst racj i klasy kont role ra PluginManager public class SimplePlugin : !Plugin

{

public void Start ( ) { Console . WriteLine ( AppDomain . Cu r rentDomain . F riendlyName + " : SimplePlugin s t a rting . . . ) ; "

} public void Stop ( ) { Console . W riteLine ( AppDomain . Cu r rentDomain . F riendlyName + "

: SimplePlugin stopping . . . " ) ;

} } li Klasa kont rol e ra za rządzaj ąca ładowaniem i manipulowaniem wtyczkami li w domenie aplikacj i . public class PluginManager : Ma rshalByRefDbj ect { p rivate ListDictio n a ry plugins = new ListDict io n a ry ( ) ; li domyślny kon s t rukto r . public Pl uginManage r ( )

{}

li Konst rukto r ładuj ący określon y z bió r wtyczek podczas two rzenia . public Plug inManage r ( ListDictio n a ry pluginList )

{

li Ładowanie wtyczek . f o reach ( st ring plugin in pluginList . Keys ) { t h i s . LoadPlugin ( ( st ring ) pluginList [ plugin ] , plugin ) ;

}

64

C#

-

Księga przykładów } li Ładowanie wskazanej a semblacj i i two rzenie instanc j i ! Plugin . public bool LoadPlugin ( st ring as sembl yName , st ring pl uginName ) { t ry { li Ładowanie a semblacj i p rywatnej . As sembly a s sembly

=

As sembl y . Load ( a s semblyName ) ;

li Two rzenie instancj i !Plugin . !Plugin plugin = ( IPlugin ) a s sembly . C reatein s t a n c e ( pluginName , t rue ) ; if ( pl ugin ! = null ) { li dodanie nowego obiektu ! Pl ugin do ListDictiona ry plugin s [ pluginName ]

=

plugin ;

ret u rn t ru e ; } else { retu rn false ; } } catch { ret u rn false ; } } public void StartPlug in ( st ring plugin ) { ( ( I Plugin ) plugins [ plugin ] ) . St a rt ( ) ; } public void StopPlugin ( s t ring plugin ) { ( ( IPlugin ) plugin s [ plugin ] ) . Stop( ) ; } public A r raylist Get Plugin list ( ) { li Zwrócenie listy nazw wtyczek . ret u rn new A r raylist ( plugins . Keys ) ; } } public class C reateinstan ceExample { public static void Main ( ) { li Two rzenie nowej domeny aplikac j i . AppDomain doma i n l = AppDomain . C reateDoma in ( " NewAppDomain l " ) ; li Two rzenie PluginManage r w nowej domenie p rzy użyciu li domyślnego kon st rukto ra . PluginManage r manage r l

=

( PluginManage r ) domain l . C reateinstanceAndUnwrap ( " C reateinstanceExampl e " , " PluginManage r" ) ; li Ładowanie nowej wtyczki do NewAppDomainl . manage r l . LoadPlug in ( " C reatei nstanceExample " , " SimplePlugin " ) ;

Rozdział 3: Domeny aplikacji, odbicie i metadane

65

li u ru chomienie i zat rzymanie wtyczki . managerl . Sta rtPlugin ( " SimplePlugin " ) ; managerl . StopPlugin ( " SimplePlugi n " ) ; li Two rzenie nowej domeny apl ikacj i . AppDomain domain2

=

AppDomain . C reateDoma in ( " NewAppDomain2 " ) ;

li Two rzenie ListDictiona ry zawiera j ącego l i s tę wtyczek do utwo rzenia . ListDictiona ry pluginList = new ListDictiona ry ( ) ; pluginlist [ " SimplePlugin " l = " C reateinstanceExample" ; li Two rzenie PluginManager w nowej domenie i o k reślenie domyślnej li listy wtyczek do utworzen ia . PluginManag e r manager2 = ( PluginManag e r ) domain l . C reateinstanceAndUnwra p ( " C reateinstanceExample " , " Pl uginManage r " , t rue , 0 , n u l l , new obj ect [ ] { pluginList } , nul l , n u l l , n u l l ) ; li Wyświet lenie l i s t y załadowanych wtyczek . Console . WriteLine ( " Plugins in NewAppDomain2 : " ) ; foreach ( s t ring s in manage r2 . GetPluginLis t ( ) ) { Console . WriteLine ( "

-

"

+

s);

} Console . ReadLine ( ) ; } }

3.8 Przekazywanie danych między domenami aplikacji Problem Potrzebujesz prostego mechanizmu do przekazywania zasadniczej konfiguracji lub danych stanu między domenami aplikacj i .

Rozwiązanie Użyj metod

SetData i GetData klasy AppDomain.

Omówienie Można przekazywać dane między domenami aplikacji jako argumenty i wartości zwrócone po wywołaniu pól obiektów występujących w innych domenach aplikacji. Jednakże czasami wygodne jest przekazywanie danych między domenami aplikacji w taki sposób, żeby dane te były łatwo dostępne w obrębie całego kodu w domenie aplikacji.

66

C#

-

Księga przykładów

Kaida domena aplikacji utrzymuje bufor danych, zawierający zestaw par nazw/wartości. Większość zawartości tego bufora odzwierciedla ustawienia konfiguracji domeny aplikacji, takie jak wartości dotyczące obiektu AppDomainSetup, dostarczone podczas tworzenia domeny apli­ kacji (przepis 3 . 1 ) . Można także zastosować bufor danych jako mechanizm wymiany danych między domenami aplikacji albo jako prosty mechanizm zapamiętywania stanu dla kodu dzia­ łającego w obrębie domeny aplikacji. Metoda SetData umożliwia powiązanie klucza typu łańcuch (string) z obiektem i zapamię­ tanie go w pamięci danych domeny aplikacji. Metoda GetData umożliwia wyszukanie obiektu w pamięci danych przy użyciu klucza. Jeśli kod w jednej domenie aplikacji wywoh1je metodę SetData lub GetData w celu realizacji dostępu do pamięci danych w innej domenie aplikacji, obiekt danych musi spełniać wymagania składni MBV lub MBR (marshal-by-value lub mar­ shal-by-reforence); w innym wypadku wykonanie programu zostanie wstrzymane z wywołaniem wyjątku System. Runtime.Serialization.SerializationException (przepis 3.3 podaje więcej informa­ cji o wymaganych charakterystykach, umożliwiających przekazywanie obiektów przez granice domen aplikacji). Poniższy kod pokazuje użycie mecod SetData i GetData przy przekazywaniu listy System. Collections.Arraylist między dwiema domenan1i aplikacj i. u s ing System ; u s ing System . Reflection ; u sing System . Collection s ; public class ListModifie r { public ListModifier ( ) { li Pobranie listy z pamięci danych . A r raylist list = ( A r raylist ) AppDomain . Cu r rentDomain . GetData ( " Pet s " ) ; li Modyfikac j a l i s t y . list . Add ( " t u rtle " ) ;

} } public c l a s s PassDataExample { public static void Main ( ) { li Two rzenie nowej domeny apl ikac j i . AppDomain domain = AppDomain . C reateDomain ( " Test " ) ; li Two rzenie obiektu A r raylist i wypełnianie go . A r raylist list = new A r raylist ( ) ; list . Add ( " dog " ) ; list . Add ( " cat " ) ; list . Add ( " fish " ) ; li Umieszczenie l isty w pamięci danych nowej domeny aplikacj i . domain . SetData ( " Pe t s " , l i st ) ; li Two rzenie instan c j i ListModifier w nowej domenie apl ikacj i . domain . C reateinstance ( " P rzepis93 - 98 " , " ListModifie r " ) ; li Pob ranie listy i wyświetlenie j ej zawa rtośc i .

Rozdział 3: Domeny aplikacji, odbicie i metadane

67

foreach ( st ring s in ( A r raylist ) domain . GetData ( " Pet s " ) ) { Console . Writelin e ( s ) ; } li Oczekiwan ie na kontynuacj ę Console . Readline ( ) ;

} }

3.9 Zwalnianie asemblacji i domen aplikacji Problem Chcesz zwolnić asemblacje lub domeny aplikacji podczas wykonania programu.

Rozwiązanie Nie ma możliwości zwolnienia indywidualnych asemblacji. Można zwolnić całą domenę aplika­ cji przy użyciu metody statycmej AppDomain. Unlnad, co w efekcie daje zwolnienie wszystkich asemblacji załadowanych do domeny aplikacji.

Omówienie Jedynym sposobem zwolnienia asemblacji jest zwolnienie domeny aplikacji, do której asem­ blacja jest załadowana. Jednakże zwolnienie domeny aplikacji powoduje usunięcie z pamięci wszystkich asemblacji, które były do niej załadowane. Takie rozwiązanie może się wydawać nie­ zręczne i nieelastyczne, ale przy właściwym zaplanowaniu domeny aplikacji i struktury ładowa­ nia asemblacji nie będzie groziło nakładkowaniem (to ograniczenie zostanie prawdopodobnie wyeliminowane w przyszłej wersji .NET Framework). Można zwolnić domenę aplikacji używając metody statycznej AppDomain. Unlnad i przeka­ zując jej odniesienie AppDomain do domeny aplikacji, która ma być zwolniona. Nie można jed­ nak zwolnić domyślnej domeny aplikacji utworzonej przez moduł CLR podczas uruchamiania. Poniższy fragment kodu demonstruje użycie metody Unlnad. li Two rzenie nowej domeny aplikacj i . AppDomain newOomain

=

AppDomain . C reateDomain ( " New Domain " ) ;

li Ładowan ie a semblacj i do domeny aplikacj i

li Zwolnienie domeny aplikacj i AppDomain . Unload ( newDomain ) ;

Metoda Unlnad powstrzymuje dostęp wszystkich nowych wątków do określonej domeny apli­ kacji i wywołuje metodę 7hread.Abort na wszystkich aktualnie aktywnych wątkach w tej dome­ nie. Jeżeli wątek wywołujący metodę Unlnad jest aktualnie uruchomiony w określonej domenie

68

C#

-

Księga przykładów

aplikacj i (co czyni go automatycznie celem wywołania Ihread.Abon), operację zwalniania wykona nowy wątek. W przypadku problemu ze zwolnieniem domeny aplikacj i wątek reali zu­ jący operację zwalniania wywoła wyjątek System. CannotUn/,oadAppDomainException. Podczas zwalniania domeny aplikacji moduł CLR wywołuje metodę finalizacyjną dla wszyst­ kich obiektów w domenie aplikacji. Proces ten, zależnie od ilości obiektów i natury ich metod finalizacji, może zająć pewien czas. Metoda AppDomain.!sFinalizingForUnwad zwraca wartość true, jeśli domena aplikacji została zwolniona i moduł CLR rozpoczął finalizację zawartych w niej obiektów; w przeciwnym wypadku wstaje zwrócona wartość folse.

3.1 O Uzyskiwanie informacji o typie Problem Chcesz otrzymać obiekt System. 1jpe, reprezentujący określony typ.

Rozwiązanie Zascosuj poniższe opcje: •

Operator typeof



Statyczną mecodę Getljpe klasy System. 1jpe



Metodę Getljpe istniejącej instancji tego typu



Metody GetNestedljpe lub GetNestedTypes klasy ljpe



Mecody Getljpe lub Getljpes klasy Assembly



Metody Getljpe, GetTypes lub Findljpes klasy System.Reflection.Module

Omówienie Obiekt 1jpe zapewnia punkt wyjścia do pracy z typami przy użyciu odbicia. Umożliwia spraw­ dzenie metadanych tego typu, uzyskanie szczegółowych informacji dotyczących jego pól i utwo­ rzenie instancji typu. Ze względu na wagę zagadnienia, .NET Framework udostępnia wiele mechanizmów dla uzyskania odniesień do obiektów Tjtpe. Najefektywniejszą metodą otrzymania obiektu ljpe dla określonego typu jest użycie opera­ tora typeofprzedstawionego niżej . System . Type tl

=

typeo f ( System . Text . St ringBuilde r ) ;

Nazwa typu nie jest zamknięta w cudzysłów i musi być możliwa do znalezienia przez kompilator. Ponieważ odniesienie jest rozwiązywane podczas kompilacji, asemblacja zawierająca dany typ staje się statyczną zależnością tworzonej asemblacji i jako taka zostanie wpisana do manifestu asemblacji. Alternatywą dla operatora typeofjest statyczna metoda Type. Getljpe, która wychwytuje łań­ cuch zawierający nazwę typu. Ponieważ do określenia typu używa się łańcucha, można go zmie­ niać podczas wykonania programu, co otwiera nowe możliwości programowania dynamicznego

Rozdział 3: Domeny aplikacji, odbicie i metadane

69

posługującego się odbiciem (przepis 3. 1 2) . Jeśli zostanie podana tylko nazwa typu, moduł runtime musi być w stanie zlokalizować typ w załadowanej uprzednio asemblacji. Alternatyw­ nie można wyspecyfikować nazwę typu zakwalifikowaną przez asemblację. Dokumentacja SDK .NET Framework zawiera kompletny opis metody

1jpe. Get1jpe i konstruowania kwal ifikowa­ Getljpe:

nych przez asemblację nazw typów. Poniższe i nstrukcje demonstrują użycie metody

li Roz różnianie wie l kości lite r , zwraca n ul l , j eś l i n ie z naleziono Type t 2 = Type . GetType ( " System . St ring " ) ; li Roz różnianie wie l kości lite r , zwraca wyj ątek TypeLoadException , j eśli nie li z naleziono Type t3 = Type . GetType ( " System . St rin g " , t rue ) ; li Bez roz różniania wielkości l ite r , zwraca TypeLoadException , j eś l i n ie li z naleziono Type t4 = Type . GetType ( " system . s t rin g " , t rue , t rue ) ; li Nazwa typu kwa l i fikowana p rzez asemblac j ę Type tS = Type . GetType ( "System . Data . DataSet , System . Data , " + "Ve rsion=l . 0 . 5000 . 0 , Cu l t u re=neut ral , Publ icKeyToken=b77a5c56 1934e089 " ) ; Aby otrzymać obiekt

1jpe reprezentujący typ istniejącego obiektu, należy użyć metody Get1jpe, Object i akceptowanej przez wszystkie typy. Oto przykład:

zaimplementowanej przez

System . Text . St ringBu ilder sb = new System . Text . St ringBuilde r ( ) ; Type t6 = s b . GetType ( ) ; Tabela 3-2 podsumowuje inne metody zapewniające dostęp do obiektów

Tabela 3-2

1jpe.

Metody zwracające obiekty Type

Metoda

Opis

1jpe. GetNested1jpe

Wybiera określony typ, zadeklarowany jako typ zagnieżdżony wewnątrz istniejącego obiektu

1jpe. GetNested1jpes

1jpe.

Wybiera tablicę obiektów

1jpe,

reprezentujących zagnieżdżone typy,

zadeklarowane wewnątrz istniejącego obiektu

Assembly. Get1jpe

Wybiera obiekt

1jpe

1jpe.

dla wyspecyfikowanego typu, zadeklarowanego

wewnątrz asemblacji.

Assembly. Get1jpes

Wybiera tablicę obiektów

1jpe,

reprezentujących typy zadeklarowane

wewnątrz asemblacji.

Module. Get1jpe

Wybiera obiekt

1jpe

dla wyspecyfikowanego typu zadeklarowanego

wewnątrz modułu.

Module. Get1jpes

Wybiera tablicę obiektów

1jpe,

reprezentujących typy zadeklarowane

wewnątrz modułu.

Module.Find1jpes

Wybiera przefiltrowaną tablicę obiektów

1jpe,

reprezentujących typy

zadeklarowane wewnątrz modułu - typy są filtrowane przy użyciu dele­ gata określającego, czy każdy obiekt wej tablicy.

1jpe powinien wystąpić w końco­

70

C#

-

Księga przykładów

3.1 1 Testowanie typu obiektu Problem Chcesz testować ryp obiektu.

Rozwiązanie Użyj odziedziczonej metody

Object. Get7jpe,

acjach możesz takie użyć operatorów is i

as

aby otrzymać

Tjpe dla obiektu. W pewnych sytu­

do testowania typu obiektu.

Omówienie Wszystkie typy dziedziczą metodę

GetTjpe po

klasie podstawowej

wione w przepisie 3. 1 O, metoda ta zwraca odniesienie runtime utrzymuje pojedynczą instancję

Tjpe

Tjpe,

Object. Jak

to zostało omó­

reprezentujące ryp obiektu. Moduł

dla każdego załadowanego typu, a wszystkie

odniesienia dla tego typu odnoszą się do tego samego obiektu. Oznacza to, że można porów­ nywać skutecznie dwa odniesienia typu. Metoda

lsStringReader pokazana niżej

przedstawia, jak

należy wykonać testowanie w wypadku obiektu System.10.String&ader.

li Two rzenie nowego St ringReade r . Obj ect someObj ect

=

new St ringReade r ( "This is a St ringReade r " ) ; li sp rawd zenie , czy someObj ect j es t t ypu St ringRead e r pop rzez odczytanie li i porównanie odnośnika Type p rzy użyciu opera t o ra t ypeof if ( typeof ( System . IO . St ringRead e r )

==

someObj ect . GetType ( ) ) {

li Kod użytkownika } W języku C# istnieje operator

IsString&ader. Jeśli

is,

umożliwiający szybkie wykonanie tego samego testu metodą

testowany obiekt jest wyprowadzony z określonej klasy, zostanie zwrócona

wartość true. Poniższy fragment kodu realizuje testowanie, o ile someObject jest instancją System.

IO. Text&ader lub

klasy wyprowadzonej (takiej jak

StringReader).

li s p rawdzen ie , czy someObj ect j es t typu TextReader lub potomnego li p rzy użyciu ope rato ra is . if ( someObj ec t is System . IO . TextRead e r ) { li Kod użyt kown ika } Obie możliwości wymagają, aby ryp użyty z operatorami

typeofi is był znany i

rozwiązywalny

podczas kompilacji. Bardziej elastyczną (chociaż wolniejszą) alternatywę stanowi użycie metody

Tjpe. Get 7jpe, która powoduje zwrócenie odniesienia Tjpe dla nazwanego typu. Odniesienia Tjpe

Rozdział 3: Domeny aplikacji, odbicie i metadane

71

nie da sic; określić przed wykonaniem programu, co powoduje wprawdzie spadek wydajności, ale umożliwia zmianc; porównywania rypu podczas wykonania programu w oparciu o wartość

ls'Jjpe pokazana tutaj zwraca true, jeśli obiekt jest nazwanego 'Jjpe.IsSubclassOfdo testowania, c:z.y obiekt jest podklasą tego typu.

łańcucha. Metoda metody

public static bool I sType ( ob j ect obj , s t ring t ype )

rypu i używa

{

li Pob ran ie nazwy typu , użycie wyszu kiwania i zwrócenie wyj ąt ku , li j eżeli typ nie został z naleziony .

Type t = Type . GetType ( type , t rue , t rue ) ; ret u rn t == obj . GetType ( ) l i obj . GetType ( ) . I sSubclas sOf ( t ) ; } Można wreszcie użyć operatora

as

do wykonania bezpiec:z.nego rzutowania dowolnego obiektu

do określonego typu. Jeśli obiekt nie może być rzutowany do określonego typu, operator zwróci wartość

as

nuli. Umożliwia to wykonanie bezpiec:z.nego rzutowania, łatwego do weryfikacji

- pod warunkiem, że porównywany ryp bc;dzie określony podczas wykonania programu. Oto przykład: li Użycie operat o ra " a s " do wykonania bezpiecznego rzutowania .

St ringReader reader = someObj ect as System . IO . St ringReade r ; i f ( reader ! = null )

{

li Kod użytkownika z czytnikiem

}

Wskazówka Metoda statyczna GetUnderlyingType klasy System.Enum umożliwia wydobycie podległego typu wyliczeniowego.

3.1 2 Tworzenie instancji obiektu przy użyciu odbicia Problem Chcesz utworzyć instancje; obiektu podczas wykonania programu przy użyciu odbicia.

Rozwiązanie Tjpe, reprezentującym ryp obiektu, którego instancje; chcesz utworzyć, GetConstructor, aby uzyskać obiekt System. &faction. Constructorlnfo, reprezen­ tujący konstruktora, którego chcesz użyć i zrealizuj metodc; Constructorlnfo.Invoke.

Posłuż sic; obiektem wywołaj merodc;

72

C#

-

Księga przykładów

Omówienie Pierwszy krok tworzenia obiektu przy użyciu odbicia stanowi uzyskanie obiektu

Tjpe,

repre­

zentującego typ, którego instancja ma być utworzona (przepis 3. 1 0 podaje więcej szczegółów). Po uzyskaniu instancji 7Jpe należy wywołać jego metodę GetConstructor, aby otrzymać Construc­ torlnfo, reprezentujący jeden z konstruktorów typu. Powszechnie używane przeciążenie metody GetConstroctor przyjmuje jako argumet tablicę Tjpe i zwraca Constructorlnfo, reprezentujący konstruktora, który pobiera numer, kolejność i typ argumentu, wyspecyfikowanego w tablicy

Tjpe.

Aby otrzymać

Constroctorlnfo,

reprezentującego konstruktora domyślnego (tj. bez para­

7Jpe, używając typu statycznego Tjpe.Emptyljpes; nie null, ponieważ wtedy GetConstructor zawiesi wykonanie programu, wywołując wyjątek System.ArgumentNullException. Jeśli GetConstroctor nie może odnaleźć konstruktora z sygnaturą odpowiadającą wyspecyfikowanym argumentom, zwróci wartość null. Po uzyskaniu żądanej informacji Constroctorlnfo, należy wywołać metodę lnvoke. Trzeba dostarczyć tablicę object zawierającą argumenty, które mają być przekazane do konstruktora. lnvoke powoduje utworzenie instancji nowego obiektu i zwraca jego odniesienie object, które metrów) , należy przekazać pustą tablicę

należy używać

należy dopasować do odpowiedniego typu. Niżej podany kod demonstruje sposób tworzenia instancji obiektu

Builder (łańcuch)

System. Text.StringBuilder, i jego objętości (int).

ze specyfikacją początkowej zawartości dla

String­

li Odczytywanie typu dla klasy St ringBuilde r . Type type

=

typeof ( Sy stem . Text . St ringBuilde r ) ;

li Two rzenie Type [ ) zawie raj ącej instancj e Type dla każdego li a rgume n t u konst rukt o ra - łańcucha i liczby cał kowitej . Type [ ) a rgTypes = new Type [ ) { typeof ( System . St ring ) , t ypeo f ( System . Int32 ) } ; li Pobie ranie obiektu Const ructorlnfo . Con st ructo rlnfo cl n fo

=

type . GetConst ructo r ( a rgTypes ) ;

li Two rzenie object [ ) zawie raj ącego a rgumenty kon s t ruktora . obj ect [ ) a rgVals = new obj ect [ J { " Same s t ring " , 30} ; li Two rzenie obj ektu i j ego rzutowanie do St ringBuilde r . St ringBuilder s b = ( St ringBuilde r ) clnfo . I nvoke ( a rgVal s ) ; Funkcjonalność odbicia jest często wykorzystywana do implementacji rozwiązań, w których odbicie służy do utworzenia instancji określonych klas - rozszerzających klasy bazy podsta­ wowej albo implementujących wspólny interfejs. Często używane są i interfejsy, i klasy bazy podstawowej. Abstrakcyjna klasa podstawowa implementuje interfejs i każdą podstawową funkcjonalność, a wtedy każda konkretna implementacja rozszerza klasę bazy. Brak jest mechanizmu formalnej deklaracji, stwierdzającej konieczność implementacji przez każdą konkretną klasę konstruktorów o określonych sygnaturach. Aby konkretne klasy były implementowane przez osoby trzecie, należy w dokumentacji wyspecyfikować sygnaturę konstruktora, wywoływaną przez współczynniki utworzonej aplikacji. Typowym podejściem, mającym na celu uniknięcie tego problemu, jest użycie domyślnego (pustego) konstruktora i skonfigurowanie obiektu po utworzeniu instancji przy użyciu odpowiednich właściwości i metod. Poniższy kod demonstruje sposób tworzenia instancji obiektów, z implementacją interfejsu

!Plugin {zastosowany już w przepisie 3.7).

Rozdział 3: Domeny aplikacji, odbicie i metadane using System ; using System . Reflection ; publ ic inte rface I Plugin { st ring Opis { get ; set ;

}

void S t a rt ( ) ; void Stop ( ) ;

} publ ic abst ract class Abst ractPlugin

I Plugin {

p rivate st ring opis = " " ; public s t ring Opis { get { retu rn opi s ; set { opis = value ;

} }

} public abst ract void S t a rt ( ) ; public a b s t ract void Stop ( ) ;

} public class SimplePlugin : Abst ract Plugin { public ove r ride void S t a rt ( ) { Console . WriteLine ( Opis

+

"

:

Sta rting . . . " ) ;

} public ove r ride void Stop ( ) { Consol e . WriteLine ( Opis

+

" ·

Stopping . . . ) ; "

} } public sealed class PluginFact o ry { public static I Pl ugin C reatePlugin ( st ring a ssembly , st ring pluginName , s t ring opi s ) { li Odczytywanie Type dla wskazanej wtyczki . Type type = Type . GetType ( pl u g inName

+

"

, " + a s sembl y ) ;

li Uzys kiwanie obiektu Const ructo rinfo . Cons t ructorinfo c i n f o = type . GetCo n s t ructo r ( Type . EmptyType s ) ; li Two rzenie obj ektu i j ego rzutowanie do St ringBuilde r . IPlugin plugin = ( IPl ugin ) cinfo . I nvoke ( null ) ; li Konfigu rowanie nowego I Plugin plugin . Opis = opi s ; ret u rn plug in ;

} } Ta i nsrrukcja ucworzy instancję dla

SimplePlugin przy użyciu klasy PluginFactory.

I Plugin plugin = PluginFactory . C reatePl ugin ( " C reateObj ec t Example " ,

li P rywatna nazwa asemblacj i

73

74

C# - Księga przykładów " SimplePlugin " , "A Simple Pl ugin "

li Nazwa klasy wt yczki li opis instanc j i wtyczki

);

Uwaga

Klasa System.Activator dostarcza dwóch statycznych metod o nazwach Cre­ atelnstance i CreatelnstanceFrom, które tworzą instancje obiektów w oparciu o obiekty Type lub łańcuchy, zawierające nazwy typu. Dokumentacja .NET Framework SOK podaje

więcej szczegółów.

3.1 3 Tworzenie niestandardowego atrybutu Problem Chcesz utworzyć atrybut niestandardowy.

Rozwiązanie Utwórz klasę, która jest wyprowadrona z podstawowej klasy abstract System.Attribute. Zaimple­ mentuj konstruktory, pola i właściwości, aby umożliwić użytkownikom skonfigurowanie atry­ butu. Użyj System.AttributeUsageAttribute, aby uzyskać odpowiedzi na następujące pytania: •

Które elementy programu stanowią uprawnione cele dla tworzonego atrybutu?



Czy można zastosować więcej niż jedną instancję atrybutu do elementu programu?



Czy atrybut jest dziedziczony przez typy wyprowadzone?

Omówienie Atrybuty zapewniają ogólny mechanizm kojarzenia informacji deklaratywnej (metadane) z ele­ mentami programu. Te metadane są zawarte w skompilowanej asemblacji, co umożliwia pro­ gramom wyszukiwanie ich poprzez odbicie podczas wykonania programu (przepis 3. 14). Inne programy, w szczególności moduł CLR, używają tej informacji do określenia sposobu interakcji i zarządzania elementami programu. Aby utworzyć atrybut niestandardowy, należy wyprowadzić klasę z bazowej klasy abs­ tract atrybutu System.Attribute. Klasy atrybutu użytkownika muszą być publiczne (public) i zgodnie z konwencją powinny mieć nazwę kończącą się wyrazem „Amibute". Atrybut nie­ standardowy musi mieć co najmniej jeden konstruktor public. Parametry konstruktora stają się pozycjonalnymi parametrami atrybutu. Podobnie jak dla wszystkich innych klas, można zadeklarować więcej niż jednego konstruktora, dając użytkownikom atrybutu opcję korzystania z różnych zestawów pozycjonalnych parametrów przy jego stosowaniu. Wszystkie publiczne

Rozdział 3: Domeny aplikacji, odbicie i metadane

75

pola i właściwości odczytu/zapisu (readlwrite) zadeklarowane przez atrybut są automatycznie eksponowane jako parametry nazwane. Aby mieć kontrolę nad stosowaniem danego atrybutu przez użytkownika (tzn. i nforma­ cję, jak i gdzie go używa), należy zastosować atrybut AttributeUsageAttribute do danego atry­ butu niestandardowego. AttributeUsageAttribute obsługuje jeden pozycjonalny i dwa nazwane parametry opisane w tabeli 3-3. Wartości domyślne określają wartość zastosowaną do danego atrybutu użytkownika, o ile nie zostanie zastosowana opcja AttributeUsageAttribute liib podana wartość dla tego konkrecnego parametru.

Tabela 3-3

Elementy typu wyliczeniowego A ttributeUsage

Parametr

Typ

Opis

Domyślnie

ValidOn

Pozycjo­ nalny

Element typu wyliczeniowego System. AttributeTargets identyfikujący ele­

Attribute Targets.Al.l

ment programu, w którym atrybut jest uprawniony.

Al/,ou;Multiple Nazwany Stwierdzający, czy atrybut może być

False

wyspecyfikowany dla pojedynczego elememu więcej niż raz.

lnherited

Nazwany Stwierdzający, czy atrybuc jest dziedziczony przez wyprowadzone klasy lub nadpisane pola.

True

Poniższy przykład pokazuje atrybut niestandardowy o nazwieAuthorAttribute, używany do iden­ tyfikacji nazwy i firmy tej osoby, która utworzyła daną asemblację lub klasę. AuthorAttribute deklaruje pojedynczy konstruktor public, który niesie string zawierający nazwisko autora. Ozna­ cza to, że użytkownicy AuthorAttribute muszą zawsze dostarczać pozycjonalny parametr string, zawierający nazwisko autora. Właściwość Company jest typu public, co czyni z niej opcjonalny parametr nazwany, jednak właściwość Name jest typu read only (nie zadeklarowano opcji dostę­ powej set). Oznacza to, że właściwość ta nie będzie ujawniona jako nazwany parametr. using System ; [ Att ributeUsage ( Att ributeTargets . Cl a s s I Att ributeTa rget s . As sembly , AllowMultiple = t rue , I n h e rited = false ) ] public class Autho rAtt ribute : System . At t ribute { p rivate st ring compa n y ; li f i rma twó rcy p rivate s t ring name ; // nazwa twó rcy li Dekl a rac j a kon st ru kto ra publicznego public AuthorAt t ribute ( s t ring name ) { this . name = name ; company = " " ; } li Dekl a racj a właściwoś c i do odczytu/u stawienia pola f i rmy public st ring Company { get { ret u rn company ; }

76

C#

-

Księga przykładów set { company = value ;

}

} li Dekla rac j a właściwości do odczytu pola wewnęt rznego public st ring Name{ get { ret u rn name ;

}

} } Poniższy przykład demonstruje niektóre zastosowania AuthorAttribute: [ a ssembly : Autho r ( " Allen " , Company = " P rincipal Obj ective Ltd . " ) ) [ Autho r ( " Allen " , Company = " P rincipal Obj ective Ltd . " ) ] public class SomeC lass {

} [ Autho r ( " Lena " ) J public c l a s s SomeOtherClass {

}

3.1 4 Sprawdzenie atrybutów elementu programu przy użyciu odbicia Problem Chcesz. użyć odbicia do sprawdzenia atrybutów niestandardowych zastosowanych do elementu programu.

Rozwiązanie Wywołaj metodę GetCustomAttributes w obiekcie wyprowadzonym z. System. Rejlection.Member­ Info, reprezentującym element programu, który ma być poddany sprawdzeniu.

Omówienie Wszystkie klasy reprezentujące elementy programu z.ostały wyprowadzone z. klasy Membe­ rlnfo. Ta klasa zawiera 1Jpe, Eventlnfo, Field!nfo, Propertylnfo i MethodBase. MethodBase ma dwie dalsze podklasy: Constructorlnfo i Method!nfo. Po otrzymaniu instancji którejś z. tych klas, można wywołać odz.iedz.icz.oną metodę GetCustomAttributes, która zwróci tablicę object zawiera­ jącą niestandardowe atrybuty zastosowane do elementu programu. Tablica object zawiera tylko niestandardowe atrybuty, nie zawiera natomiast atrybutów z. biblioteki głównych klas .NET Framework.

Rozdział 3: Domeny aplikacji, odbicie i metadane

77

Metoda GetCustomAttributes umożliwia dwie formy przeciążenia. Pierwsze posiłkuje się wyrażeniem boolowskim (bool}, zgodnie z którym kontroluje, c:z.y GetCustomAttributes powinno zwrócić atrybuty odziedzic:LOne po klasach macierzystych. Drugie przeciążenie GetCustom­ Attributes używa dodatkowego argumentu Ijpe, działąjącego jako filtr, co w rezultacie powo­ duje, że GetCustomAttributes zwraca tylko atrybuty określonego typu. W przedstawionym niżej przykładzie użyto niestandardowego atrybutu AuthorAttribute zadeklarowanego wc:z.eśniej w przepisie 3. 1 3 i zastosowano go do klasy GetCustomAttributes­ Exampk. Metoda Main wywołuje metodę GetCustomAttributes, która filtruje atrybuty, dzięki czemu ta metoda zwraca tylko instancje AuthorAttribute. Można bezpiecznie rzutować ten zestaw atrybutów do odniesień AuthorAttribute i realizować dostęp do ich pól bez potrzeby używania odbicia. -

-

using System ; [ Autho r ( " Lena " ) ] [ Autho r ( "Allen " , Company = " P rincipal Obj ect ive Ltd . " ) ] public class GetCustomAtt ributesExample { public s tatic void Main ( ) { li Pob ranie obiektu Type dla tej kla s y .

Type type = typeof ( GetCustomAt t ributesExample ) ; li Pobranie at rybutów typu . Zastosowanie f il t ra , aby zwracane były l i tylko instan c j e AuthorAtt ribute .

obj e ct [ J att rs = type . GetCustomAt t ributes ( typeo f ( Autho rAt t ribute ) , t rue ) ; li Wyl iczenie at rybutów i wyświet lenie s z czegółów .

foreach ( Au t h o rAtt ribute a in att rs ) { Console . WriteLine ( a . Name + " , " + a . Company ) ;

} li O c zekiwanie . . .

Console . ReadLine ( ) ;

} }

4 Wątki, procesy i synchronizacja Jedną z zalet systemu operacyjnego Microsoft Windows jest umożliwienie równoległej realiza­ cji wielu programów (procesów), z których każdy pracuje wielozadaniowa {przy użyciu wielu wątków) . Niniejszy rozdział demonstruje sposoby zarządzania procesami i wątkami we włas­ nej aplikacji przy użyciu właściwości dostarczonych przez bibliotekę klas systemu Microsoft .NET Framework. Przepisy w tym rozdziale odnoszą się przede wszystkim do następujących czynności: •

Wykorzystanie różnych technik i właściwości systemu .NET Framework do tworzenia nowych wątków (przepisy od 4 . 1 do 4.5).



Kontrola nad realizacją wątku z rozpoznaniem momentu jego zakończenia {przepisy 4.6 i 4.7).



Synchronizacja realizacji wielu wątków {przepisy 4.8 i 4.9).



Rozpoczynanie i zatrzymywanie nowych procesów {przepisy 4. 1 0 i 4. 1 1 ) .



Zapewnienie, że tylko jedna instancja aplikacji może być wykonywana w danym momencie (przepis 4. 1 2) .

4.1 Wykonanie metody przy użyciu puli wątków Problem Chcesz wykonać metodę wykorzystującą wątek z puH wątków modułu runtime.

Rozwiązanie Zadeklaruj metodę zawierającą kod, który chcesz wykonać. Metoda ta musi zwracać void (pustą przestrzeń) i pobierać pojedynczy argument object. Utwórz instancję delegata System.

80

C#

-

Księga przykładów

Threading. WaitCallback, która odnosi się do metody. Następnie wywołaj statyczną metodę QueueUserWorkltem klasy System. Threading. ThreadPool i przekaż instancję delegata jako argu­ ment. Moduł runtime ustawi instancję delegata w kolejce i wykona ją, gdy stanie się dostępny wątek z puli wątków.

Omówienie Aplikacje używające wielu wątków o krótkim czasie życia lub utrzymujące dużą liczbę współ­ bieżnych wątków mogą ponosić skutki przepełnienia, związanego z tworzeniem, działaniem i niszczeniem wątków, co przejawia się obniżeniem efektywności. Ponadto w systemach wie­ lowątkowych wątki często długo pozostają bezczynne, kiedy czekają na warunki wyzwalające ich aktywność. Zastosowanie puli wątków stanowi podstawowe rozwiązanie zwiększające skalo­ walność, wydajność i efektywność systemów wielowątkowych. System .NET Framework dostarcza prostej implementacji puli wątków, dostępnej poprzez statyczne pola klasy ThreadPool. Metoda QueueUserWorkltem umożliwia wykonanie metody przy użyciu wątku z puli wielowątkowej, poprzez umieszczenie elementu roboczego w kolejce. Element ten jest reprezentowany przez instancję delegata WaitCallback, zawierającą odniesienie do metody, która ma być wykonana. Kiedy wątek z puli wątków jest dostępny, metoda pobiera następną pozycję z kolejki i wykonuje ją. Wątek wykonuje przypisane do niego zadanie, a kiedy zostanie ono zakończone, zamiast sam również zakończyć działanie, wraca do puli wątków i pobiera następną pozycję z kolejki. Wykorzystanie puli wątków modułu runtime wydatnie upraszcza programowanie wielowąt­ kowe, należy jednak zdawać sobie sprawę, że ta implementacja stanowi prostą pulę wątków ogólnego zastosowania. Przed decyzją wykorzystania puli wątków należy rozważyć następujące zagadnienia: •

Host z z uruchomionym modułem runtime definiuje maksymalną liczbę wątków przydzie­ lonych do puli. Liczby tej nie można zmienić ani przy pomocy parametrów konfigurują­ cych, ani za pomocą zarządzanego kodu. Domyślny limit wynosi 25 wątków na procesor (CPU). Maksymalna liczba wątków w puli wątków nie ogranicza liczby pozycji oczekują­ cych w kolejce.



Poza wykorzystaniem puli wątków do bezpośredniego wykonywania kodu, moduł runtime używa jej do wielu innych celów wewnętrznych. Obejmuje to wykonanie metody asyn­ chronicznej (przepis 4.2) i obsługi zdarzeń timera (przepis 4.3). Cała ta działalność może doprowadzić do skumulowania wątków w puli wątków, co oznacza znaczne wydłużenie kolejki roboczej . I chociaż maksymalna długość kolejki jest ograniczona tylko przez obszar pamięci dostępnej dla modułu runtime, jej nadmierna długość spowoduje znaczne opóźnie­ nie wykonania ustawionych w kolejce.



Nie należy używać puli wątków do wykonywania długotrwałych procesów. Ograniczona liczba wątków w puli wątków oznacza, że nawet kilka wątków przypisanych do takiego pro­ cesu może mieć znaczący wpływ na całkowite działanie puli wątków. W szczególności należy unikać usypiania wątków z puli wątków na dowolny czas.



Ponieważ nie ma się wpływu na harmonogram wątków z puli wątków, nie można również ustanawiać priorytetów pozycji pracy. Puła wątków obsługuje każdą pozycję w takiej kolej­ ności, w jakiej zostanie dołączona do kolejki.

Rozdział 4: Wątki, procesy i synchronizacja •

81

Kiedy element zostanie ustawiony w kolejce, nie może już zostać skreślony łub zatrzymany.

Podany niżej przykład demonstruje wykorzystanie klasy 7hreadPool do realizacji metody DisplayMessage. W przykładzie metoda została zastosowana do puli wątków dwukrotnie: naj­ pierw bez argumentów, a następnie z obiektem Messagelnfo, który umożliwia kontrolę, jaki komunikat ma wyświecłić nowy wątek. using System ; using System . Th reading ; li Klasa użyta do p rzesłania danych do metody DisplayMes sage , gdy j est li wykonywana p rzy użyciu puli wątków . public class Mes sage!nfo { p rivate int ite ration s ; p rivate st ring message ; li Kon s t rukto r p rzejmuj ący u stawienia konfig u racyj ne wąt ku . public Messagei n f o ( int iterat ion s , st ring mes sage ) { t h i s . ite rat ions this . message

=

=

ite ration s ;

messag e ;

} li Właściwości odczytuj ące ustawienia konfigu racyj ne . public int I t e rations { get { retu rn ite ration s ; } } public st ring Mes sage { get { retu rn mes sage ; } } } public class Th readPoolExample { li Wyświet lanie komunikatu na konsol i . public static void DisplayMes sage ( ob j ect state ) { li Rzutowanie a rgumentu stanu do obiektu Messageinfo . Message i n fo config

=

state a s Message! n f o ;

li J eżeli a rgument config j est pusty , żadne a rgumenty n i e będą li p rzekazane do metody Th readPool . QueueUs e rWo rkitem , zostaną użyte li a rgumenty domyślne if ( config

==

nul l ) {

li T rzyk rotne wyświetlenie stałego komunikatu na konsol i . f o r ( int count

=

0 ; count < 3 ; count++ ) {

Conso le . W riteline ( "A th read pool example . " ) ; li Odczekiwanie w celu demons t racj i . Należy unikać u s ypiania li wątków z puli wąt ków w rzeczywistych aplikacj ach . Th read . Sleep ( l990 ) ; } } else { li Wyświet lenie wskazanego komun ikatu o k reśloną l iczbę razy . f o r ( int count

=

0 ; count < conf ig . It e rat ions ; count++ ) {

82

C#

-

Księga przykładów Console . W riteLine ( config . Message ) ; li Odczekiwanie w celu demonst rac j i . Należy unika ć u sypiania li wątków z puli wątków w rzeczywistych apl ikac j ach . Th read . Sleep ( l000 ) ; } } } public static void Ma in ( ) { li Utwo rzenie delegowanej instanc j i , aby można było p rzekazać metodę li DisplayMessage do puli wątków. WaitCallback workMethod

=

new WaitCa l l ba c k ( Th readPoolExample . Di splayMes sage ) ; li Wykonanie DisplayMes sage przy użyciu puli wątków, bez a rgumentów. Th readPool . QueueUserWo rki tem ( wo rkMethod ) ; li Wykonanie DisplayMessage p rzy użyciu p u l i wątków i udostępnienie li obiektu Mes sagelnfo w celu p rzekazania do metody DisplayMes sage . Mes sageinfo info

=

new Mes sagel n fo ( S , "A th read pool example with a rguments . " ) ; Th readPool . QueueUserWo rkitem ( wo rkMethod , info ) ; li Oczekiwanie . . . . Console . W riteLine ( "Main method complet e . P re s s Ente r . " ) ; Console . Readline ( ) ; } }

4.2 Asynchroniczne wykonywanie metody Problem Chcesz zapoczątkować wykonywanie metody w oddzielnym wątku, a jednocześnie kontynuo­ wać realizację innych rozpoczętych zadań. Kiedy metoda zostanie zako6czona, chciałbyś otrzy­ mać jej wartość zwromą.

Rozwiązanie Zadeklaruj delegata o tym samym podpisie co metoda, którą chcesz wykonać. Utwórz instancję delegata odwołującego się do tej metody. Wywołaj metodę Beginlnvoke dla instancj i, aby roz­ począć wykonanie metody. Wykorzystaj metodę Endlnvoke w celu określenia statusu metody i - po ukończeniu metody - otrzymania jej wartości zwrotnej.

Rozdział 4: Wątki, procesy i synchronizacja

83

Omówienie Zazwyczaj wywołuje się metodę synchronicznie, co oznacza, ie kod wywołujący wstaje zablo­ kowany do ukończenia metody. W więkswści przypadków takie zachowanie jest spodziewane i pożądane, ponieważ kod wymaga zakończenia danej operacji przed kontynuacją. Czasami jednak lepiej jest wykonać metodę asynchronicznie, co oznacza uruchomienie jej dla osobnego wątku i kontynuowanie innych operacji. System .NET Framework wdraża wzorzec asynchronicznego wykonania, co umożliwia wywołanie dowolnej metody asynchronicznie przy użyciu delegata. Podczas deklarowania i kompilowania delegata, kompilator automatycznie wygeneruje dwie metody, korzystające z asynchronicznego wykonania: Beginlnvoke i Endlnvoke. Przy wywołaniu metody Beginlnvoke dla instancji delegata, metoda powiązana z delegatem wstaje ustawiona w kolejce do wykonania asynchronicznego. Sterowanie zostaje natychmiast zwrócone do kodu wywołującego, a wywoły­ wana metoda realizuje się w ramach pierwszego dostępnego wątku z puli. Podpis metody Beginlnvoke zawiera te same argumenty, które są określone w podpisie delegata. Po nich następują dwa dodatkowe argumenty potrzebne do asynchronicznej realiza­ cji. Są to: •

Instancja delegata System.AsyncCaiiback, dająca odsyłacz do metody, którą moduł runcime ma wywołać po zakończeniu metody asynchronicznej. Ta metoda jest wykonywana w kon­ tekście wątku z puli wątków. Przekazanie zera (nuli) oznacza, ie żadna metoda nie wstanie wywołana i trzeba użyć innego mechanizmu (omówionego dalej w niniejszym przepisie) dla stwierdzenia, kiedy metoda asynchroniczna została ukońcwna.



Odniesienie object, kojarzone przez moduł runtime z operacją asynchroniczną. Metoda asynchroniczna nie ma dostępu do tego obiektu ani go nie używa, ale po zakończeniu jest dostępna dla kodu projektanta, co pozwala na powiązanie użytecznej informacji dotyczącej stanu z operacją asynchroniczną. Na przykład obiekt ten umożliwia mapowanie wyników na rozpoczęte operacje w sytuacji zainicjowania wielu operacji asynchronicznych, które wykorzystują wspólną metodę wywołania zwrotnego do zakończenia operacji.

Metoda Endlnvoke umożliwia uzyskiwanie wartości zwrotnej wykonywanej asynchronicznie metody, ale uprzednio należy określić, kiedy ta metoda się skończy. Niżej podane są cztery sposoby ustalenia, czy dana metoda asynchroniczna wstała zakońcwna. •

Blokowanie wstrzymuje wykonanie bieżącego wątku do momentu zakoń­ czenia wykonania metody asynchronicznej. Jest to w rezultacie działanie bardzo podobne do wykonania synchronicznego, jednak zapewnia swobodę wyboru momentu, kiedy kod ma wstać zablokowany, co daje możliwość wcześniejszej realizacji części przetwarzania.



Odpytywanie Wymaga powtarzalnego testowania stanu metody asynchronicznej dla

Blokowanie

upewnienia się, czy się zakończyła. Jest to bardw prosta technika, niezbyt ekonomiczna z punktu widzenia przetwarzania, ale można dzięki niej uniknąć ciasnych pętli, zużywają­ cych nieefektywnie czas procesora. Najlepszym sposobem jest uśpienie na jakiś czas wątku odpytującego przy użyciu metody Thread.Sleep między sprawdzaniem ukończenia opera­ cji. Ponieważ ta technika wymaga podtrzymywania pętli, działania oczekującego wątku są ogranicwne, ale można go wykorzystać na przykład do uaktualniania jakiegoś wskaź­ nika progresji.

84

C# •

-

Księga przykładów

Ocukiwanie Oczekiwanie Threading. WaitHand/.e w celu

posługuje się obiektem wyprowadzonym z klasy

System.

sygnalizacji momentu ukończenia metody asynchronicznej.

Oczekiwanie jest bardziej ekonomiczną wersją odpytywania, dodatkowo umożliwiającą czekanie na zakończenie widu metod asynchronicznych. Można także określić wartości limitów czasowych, co umożliwi kontrolę nad realizacją wątku poprzez wykazywanie błędu w momencie ich przekroczenia albo okresową aktualizację wskaźnika stanu. •

Wywołanie zwrotne

Wywołanie zwrotne jest metodą wywoływaną przez moduł runtime

po zako11czeniu operacji asynchronicznej. Kod wywołujący nie musi wykonywać żadnych dodatkowych kroków, aby określić moment zakończenia działania metody asynchronicznej, ma także swobodę co do dalszego działania. Ta technika umożliwia największą elastyczność, ale jednocześnie wprowadza największy stopień złożoności, zwłaszcza przy obsłudze widu współbieżnych operacji asynchronicznych, używających tego samego wywołania zwrot­ nego. W takim przypadku należy użyć odpowiednich obiektów stanu, aby odróżnić metody zakończone od rozpoczętych. Klasa

AsyncExecutionExampk w kodzie przykładowym

demonstruje użycie metody wykonania

asynchronicznego. Został cu zastosowany delegat o nazwie AsyncExampkDelegate do wykonania asynchronicznie metody

LongRunningMethod.

Symuluje ona długotrwałą metodę przy użyciu

dającego się skonfigurować opóźnienia (uzyskanego przy użyciu opcji usypiania

Thread.S/.eep).

Oto kod delegata AsyncExampl.eDe/.egate i metody LongRunningMethod:

li Delegac j a pozwalająca z realizować asynch roniczne wykonanie li AsyncExecut ionExample . LongRunn ingMethod . public delegate DateTime AsyncExampleDelegate ( int delay , st ring name ) ; li Symulac j a metody długot rwałego wykonania . public static DateTime LongRunningMethod ( int dela y , s t ring name ) { Consol e . W riteLine ( " {0 } : { l } example - th read s t a rting . " , DateTime . Now . ToSt ring ( "HH : mm : s s . ff f f " ) , name ) ; li Symulacj a czasochłonnego p rzetwa rzania . Th read . Sleep ( delay ) ; Consol e . WriteLine ( " { 0 } : { l } example - t h read finishing . " , DateTime . Now . ToSt ring ( "HH : mm : s s . f f f f " ) , name ) ; li Zwrócenie czasu wykonania metody . ret u rn DateTime . Now; } Przykład AsyncExecutionExamp/.e zawiera pięć metod demonstrujących różne podejście do zakoń­ czenia zarządzanej metody asynchronicznej . Ich opis i kod wstały podane niżej. Metoda

B/QckingExample

wykonuje

LongRunningMethod

asynchronicznie i kontynuuje

działanie przy ograniczonym zestawie środków przetwarzania. Po zakończeniu tego przetwa­

B/QckingExamp/.e blokuje akcję do momentu zakończenia metody LongRunningMethod. B/QckingExamp/.e wywołuje metodę Endlnvoke instancji delegata AsyncExampleDe/.egate. Jeśli metoda LongRunningMethod właśnie się zakończyła, Endlnvoke natychmiast dokonuje powrotu. W innym wypadku B/QckingExamp/.e będzie nadal utrzymywać blokadę - do momentu ukończenia metody LongRunningMethod. rzania,

W celu zastosowania blokady

Rozdział 4: Wątki, procesy i synchronizacja

85

public static void BlockingExample ( ) { Console . Writeline ( En v i ronment . Newline + " * * * Running Blocking Example * * * " ) ; li Wywołanie asynch roniczne LongRunningMet hod . P rzekazanie null do li delegata zwrotnego wywołania obiektu stanu asyn c h ronicznego . AsyncExampleDelegate longRunningMethod

=

new AsyncExampleDelegate ( LongRunningMethod ) ; IAsyncResult asyncResult

=

longRunn ingMethod . Begin invoke ( 2000 ,

" Blocking " , null , n u l l ) ; li Wykonanie innych operacj i , zanim będz ie można zablokował . for ( int count

=

0 ; count < 3 ; count++ ) {

Console . W riteline ( " { 0 } : Continue p rocess ing until ready



+

" to block . . . " , OateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; Th read . Sleep ( 2 00 ) ; } li Blokowanie do chwili zakończenia metody asynch ronicznej i uzyskania li danych końcowych . Console . Writeline ( " { 0 } : Blocking until method is complete . . .

· ,

OateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; DateTime completion

=

longRunningMethod . Endinvoke ( asyncResult ) ;

li Wyświetlenie informacj i o zakończeniu Console . W riteline ( " { 0 } : Blocking example complete . " , complet ion . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; }

Metoda PoliingExamp/e wykonuje LongRunningMethod asynchronicznie, wkraczając następnie w pętlę odpytywania, dopóki nie zakończy się metoda LongRunningMethods. PollingExample testuje właściwość lsCompleted instancji lasync&sult, zwróconego przez Beginlnvoke, aby spraw­ dzić, czy metoda LongRunningMethod wstała zakończona; jeśli nie PoliingExample wywoła -

7hread. Sleep. public static void PollingExample ( ) { Console . Writeline ( Envi ronment . Newline + " * * * Running Polling Example * * * " ) ; li Wywołanie asynchroniczne LongRunningMethod . P rzekazanie null do li delegata zwrotnego wywołania obiektu stanu asynch ron ic znego . AsyncExampleDelegate longRunningMethod

=

new AsyncExampleDelegate ( LongRunningMethod ) ; IAsyncResult asyncResult

=

longRunningMethod . Begininvoke ( 2000 ,

" Polling " , nul l , n u l l ) ; li Odpytywanie metody asynchronicznej w celu sp rawd zenia zakończenia . li Jeżeli n ie zakończono , odczekuj 300ms p rzed ponownym zapytaniem . Console . Writeline ( " { 0 } : Poll repeatedly until method is " + " complete . . . " , OateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ;

86

C#

-

Księga przykładów while ( ! asyncResul t . I s Completed ) { Console . Writeline ( " { 0 } : Polling . . . " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) ) ; Th read . Sleep ( 300 ) ; } li Odbió r danych zakończenia metody async h ronic znej . DateTime completion

=

longRunningMethod . Endinvoke ( asyncResult ) ;

li Wyświetlenie info rmacj i o zakończeniu Console . W riteLine ( " {0 } : Polling example complete . " , complet ion . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; }

Metoda Waiting.Example wykonuje asynchronicznie metodę LongRunningMethod i czeka na jej zakończenie. WaitingExampk wykorzystuje właściwość AsyncWaitHandk instancji IasyncResult zwróconą przez Beginlnvoke do uzyskania pola WaitHandk, a następnie wywo­ łuje jego metodę WaitOne. Użycie limitu czasu umożliwia procedurze Waiting.Exampk prze­ rwanie oczekiwania w celu realizacji pozostałego przetwarzania lub definitywnego zakończenia z komunikatem o błędzie przy zbyt długim wykonywaniu metody asynchronicznej . public static void WaitingExample ( ) { Console . WriteLine ( En v i ronment . NewLine + " * * * Running Waiting Example * * * " ) ; li Wywołanie asynchroniczne LongRu nningMet hod . P rzekazanie n u l l do li delegata zwrotnego wywołania obiektu stanu asynch roniczneg o . AsyncExampleDelegate longRunn ingMethod

=

new AsyncExampleDelega t e ( LongRunningMethod ) ; IAsyncResult a syncResult

=

longRunningMethod . Begininvoke ( 2000 ,

"Waiting " , null , null ) ; li Oczekiwanie na zakońc zenie metody asynch ronicznej . Po upływie li 300ms następu j e p rzekroczenie czasu i wyświet lenie komunikatu na li konsol i , a następnie dalsze oczekiwanie . Console . WriteLine ( " {0 } : Waiting until method is complete . . . , "

DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; while ( ! asyncResult . AsyncWaitHandle . Wait0ne ( 300 , false ) ) { Consol e . WriteLine ( " { 0 } : Wait t imeout . . . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; } li Odeb ranie danych zakończenia metody asynch ron icznej . DateTime completion

=

longRunningMethod . Endinvoke ( asyncResu lt ) ;

li Wyświetlenie info rmacj i o zakończeniu Console . WriteLine ( " { 0 } : Waiting example complete . " , completion . ToString ( " HH : mm : ss . f f f f " ) ) ; }

Rozdział 4: Wątki, procesy i synchronizacja Metoda

87

WaitAl/Example wykonuje asynchronicznie LongRunningMethod wielokrotnie, uży­ WaitHandle do efektywnej organizacji oczekiwania - dopóki wszystkie

wając tablicy obiektów

metody nie zostaną zakończone.

public static void WaitAllExample ( ) { Console . WriteLin e ( Envi ronment . NewLine + " * * * Running WaitAll Example * * * " ) ; l i Two rzenie A r rayList do p rzechowania i n stancj i IAsyncResult dla każdej l i u ruchomionej metody a synch ronicznej .

A r rayList asyncRe s u l t s = new A r rayList ( 3 ) ; li Wywołanie asynch roniczne t rzech LongRunningMethod . P rzekazanie null do li delegata zwrotnego wywołania obiektu stanu asynch ronicznego . li Dodanie instancj i IAsyncResult dla każdej metody do A r rayList .

AsyncExampleDelegate longRunn ingMethod = new AsyncExampleDelegate ( LongRunningMethod ) ; asyncRes ults . Add ( longRunn ingMethod . Begin invoke ( 3000 , "WaitAll 1 " , n ul l , n u ll ) ) ; asyncRes u lt s . Add ( longRunningMethod . Begin invoke ( 2500 , "WaitAll 2 " , nul l , n u ll ) ) ; asyncResult s . Add ( longRunn ingMethod . Begin invoke ( 1500 , "WaitAll 3 " , n ul l , n u ll ) ) ; l i Utwo rzenie tablicy obiektów WaitHandle , któ re zostaną użyte do li oczekiwania na zakończenie każdej metod y .

WaitHandle [ ] waitHandles = new WaitHandl e [ 3 ] ; for ( int count = 0 ; count < 3 ; count++ ) { waitHandles [ count ] = ( ( I AsyncResult ) a syncResult s [ count ] ) . AsyncWaitHandle ; } li Oczekiwanie na zakońc zenie ws zystkich t rzech metod asynch ronicznych .

Console . WriteLine ( " { 0 } : Waiting until all 3 met hods a re " + " complete . . . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f ff " ) ) ; while ( ! WaitHandle . WaitAl l ( waitHandles , 300 , false ) ) { Console . W riteLine ( " {0 } : WaitAll t imeout . . . " , DateTime . Now . ToSt ring ( "HH : mm : s s . f f f f " ) ) ; } li P rzej rzenie danych zakończenia dla ka żdej metody i sp rawdzenie czasu , li zakoń czenia metody finalnej .

DateTime completion = DateTime . MinValue ; foreach ( IAsyncResult result in asyncResul t s ) { DateTime t ime = longRunningMethod . Endinvoke ( resu l t ) ; I f ( t ime > completion ) completion = t ime ; } li Wyświet lenie informacj i o zakończeniu

Console . WriteLine ( " { 0 } : WaitAll example complete . " ,

88

C#

-

Księga przykładów completion . ToSt ring ( " HH : mm : s s . f f f f " ) ) ;

}

CalLbackExample wykonuje LongRunningMethod asynchronicznie i p rzekazuje instancję CallbackHandler) do metody Beginlnvoke. Metoda CallbackHandler (mająca odniesienie w AsyncCallback) jest wywoływana automatycznie po zakończeniu metody asynchronicznej LongRunningMethod, dając metodzie CallbackExampk Metoda

delegata AsyncCallback (odwołującego się do metody

"zielone światło" do kontynuacji przetwarzania.

public static void CallbackExampl e ( ) { Console . Writeline ( En v i ronment . Newline + " * * * Running Cal l back Example * * * " ) ; li Wywołanie asynch roniczne LongRunningMet hod . P rzekazanie instan c j i li delegata AsyncCallback odwołu j ącego s ię do metody CallbackHandle r , l i któ ra będzie wywołana automatycznie po zakończeniu metody li asynchronicznej . P rzekazanie odwołania do instanc j i delegata li AsyncExampleDelegate j ako stanu async h ronicznego ; w p rzeciwnym razie li metoda zwrotna nie będzie miała dostępu do instan c j i delegowanej w celu li wywołania Endinvoke . AsyncExampleDelegate longRunn ingMethod = new AsyncExampleOelegat e ( LongRunningMethod ) ; IAsyncResult asyncResult

=

longRunningMethod . Begininvoke ( 2000 ,

" Callback " , new AsyncCal l back ( CallbackHandle r ) , longRunningMethod ) ; li Dal sze p rzetwa rzanie . for ( int count

=

0 ; count < 1 5 ; count++ ) {

Console . Writeline ( " { 0 } : Continue p roces s ing . . . " , OateTime . Now . ToSt ring ( " HH : mm : s s . ff ff " ) ) ; Th read . Sleep ( 200 ) ; } } li Metoda obsługi zakończenia asynch ronicznego p rzy użyciu wywołania zwrotnego . public static void Cal l backHand l e r ( IAsyncResult result ) { li Wydobycie odwołania do instan c j i AsyncExampleDelegate z instancj i li IAsyncResul t . Pozwoli to uzyskać dane o zakoń czeniu AsyncExampleOelegate longRunn ingMethod

=

(AsyncExampleDelegat e ) result . AsyncState ; li Uzyskanie danych zakończenia metody asynch ron icznej . DateTime completion

=

longRunningMethod . Endinvoke ( result ) ;

li Wyświet lenie info rmac j i o zakończeniu Console . Writeline ( " {0 } : Call back example complete . " , completion . ToSt ring ( "HH : mm : s s . f f f f " ) ) ; }

Rozdział 4: Wątki, procesy i synchronizacja

89

4.3 Wykonanie metody przy użyciu timera Problem Chcesz wykonywać metodę w oddzielnym wątku - okresowo albo w określonym momencie.

Rozwiązanie object. Utwórz instancję System. TimerCa/lback razem z obiektem stanu object,

Zadeklaruj metodę zwracającą void i pobierającą pojedynczy argument

delegata System. Threading. TimerCa/lback z odniesieniem do tej metody. Utwórz obiekt

Threading. Timer i przekaż go

instancji delegata

który timer przekaże do twojej metody, gdy jego wyznaczony czas się skończy. Moduł run­ cime poczeka na zakończenie czasu timera, a następnie wywoła metodę projektanta przy użyciu wątku z puli wątków.

Omówienie Często wygodne jest wykonywanie metody w określonym momencie albo w regularnych odstę­ pach czasu. Na przykład można codziennie o godzinie 1 :OO po północy wykonywać kopię zapa­ sową albo co 20 minut zerować bufor danych. Klasa

Timer umożliwia bezpośrednie wykonanie

zadań czasowych poprzez wykonanie metody wywoływanej przez delegata TimerCallbackw okre­ ślonych przedziałach czasu. Wywołana metoda działa w kontekście wątku z puli wątków. Przy specyńkacji obiektu

Timer należy

podać dwa przedziały czasowe. Pierwsza z tych war­

tości określa opóźnienie (w milisekundach) przed rozpoczęc iem wykonywania metody. Dla natychmiastowego rozpoczęcia należy podać O. Wartość System. Threading. Timeout.Infinite two­ rzy

Timer w

scanie zatrzymanym. Druga wartość określa przedział czasu, po którym

Timer

będzie w sposób powtarzalny wywoływał tę metodę po pierwotnym wykonaniu. Jeśli zostanie

Timeout.Infinite, Timer wykona metodę tylko raz (o ile opóźnienie Timeout.Infinite). Przedziały czasowe mogą być wyspecyńkowane jako wartości int, long, uint lub System. TimeSpan. Kiedy obiekt Timer zostanie już utworzony, można modyfikować przedziały czasowe posługując się metodą Change, ale nie można zmienić wywoływanej metody. Po zakończeniu korzystania z klasy Timer należy wywołać jej metodę Dispose, aby uwolnić zasoby systemowe, blokowane przez timer. Zwolnienie Timer spowoduje anulowanie wszystkich metod przewi­ ustawiona wartość O lub

początkowe nie będzie równe

dzianych do wykonania.

TimerExample przedstawiona niżej pokazuje sposób użycia Timera do wywołania TimerHand/er. Na początku Timer zostaje skonfigurowany tak, żeby wywoły­ wał po dwóch sekundach TimerHandler, a następnie ponawiał wywołanie co sekundę. W tym Klasa

metody o nazwie

przykładzie istnieje możliwość wprowadzenia z konsoli nowego interwału przy pomocy metody

Timer. Change. using System ; using System . Th reading ; public class TimerExample {

90

C#

-

Księga przykładów li Metoda wykonywana po wygaśnięciu t ime ra . Wyświetla komunikat 11 na konsol i .

p rivate static void TimerHandle r ( obj ect stat e ) { Console . Writeline ( " { 0 } : { l } " , OateTime . Now . Tost ring { " HH : mm : s s . f f f f " ) , state ) ; } public static void Main { ) { li Utwo rzenie nowej instancj i delegowanej Time rCa l l back odwołuj ącej się li do statycznej metody TimerHand le r . Zostanie ona wywołana li po wygaśnięciu t imera . Time rCa l l back handler = new Time rCall bac k { TimerHand l e r ) ; li Utwo rzenie obiektu stanu , p rzekazywanego do metody Time rHandler li w momencie wyzwolenia . W tym wypadku wyświetlany j est komunikat . st ring state = "Time r expi red . " ; Console . W riteline ( " { 0 } : C reat ing Time r . " , DateTime . Now . ToSt ring { "HH : mm : s s . f f ff " ) ) ; li Utwo rzenie Time r u ruchamianego po raz pierwszy po 2 sekundach , li a następnie co sekundę . u s ing { Time r t ime r = new Time r ( handle r , state, 2000 , 1000 ) ) { int pe riod ; li Odczytanie nowego interwału z konsol i , dopóki użyt kown ik nie li wpisze 0 ( ze ro ) . Niep rawidłowe wa rtości powodu j ą u życie li domyśl nej wa rtości 0 , co zat rzyma p rog ram . do { t ry { period = Int32 . Pa rs e { Console . Readline ( ) ) ; } catch { pe riod = 0 ; } li Zmiana u ruchamiania t imera p rz y użyciu nowego interwał u li natychmiast . if ( pe riod > 0 ) t ime r . Change ( 0 , pe riod ) ; } while ( pe riod > 0 ) ; } li Oczekiwanie . . . . Console . Writeline ( " Main method complete . P ress Ente r . " ) ; Console . Readline ( ) ; } }

Timer- używany pierwotnie do wywoływania metod w regularnych odsrępach czasu - pozwala również na zwiększenie elasryczności pracy przy wywoływaniu merod w określonym momencie. Należy rylko obliczyć różnicę między bieżącym czasem, a posrulowanym czasem wykonania, jak

Rozdział 4: Wątki, procesy i synchronizacja pokazano w zademonstrowanej niżej metodzie

Exampk w przykładowym kodzie dla

RunAt {metoda RunAt należy

do klasy

91

RunAt­

tego rozdziału).

public static void RunAt ( DateTime execTime ) { li Wyl iczanie różnicy pomiędzy określonym czasem wykonania li i czasem bieżącym . TimeSpan waitTime

=

execTime - DateTime . Now ;

if ( waitTime < new TimeSpan ( 0 ) ) waitTime = new TimeSpan ( 0 ) ; li Utwo rzenie nowej instancj i delegowanej Time rCal l back , li odwoł uj ącej się do statycz nej metody TimerHandle r . TimerHandler li zostanie wywołana po wygaśnięciu time ra . Time rCallback handler = new Time rCal l bac k ( TimerHandl e r ) ; li Utwo rzenie Timer u ru chamianego raz w o k reślonym momen cie . Określenie li interwału - 1 pows t rzymuj e time r p rzed ponownym wykonaniem metody . new Time r ( handle r , n u l l , waitTime , new TimeSpan ( - 1 ) ) ; }

4.4 Wykonanie metody poprzez sygnalizację obiektu WaitHandle Problem Chcesz wykonać jedną lub więcej metod automatycznie, kiedy zostaje zasygnalizowany obiekt wyprowadzony z

System. Threading. WaitHandle.

Rozwiązanie System. Threading. WaitOrTimerCallback z odniesieniem do metody, WaitHandk, przełączający wyko­ pulę wątków przy użyciu metody statycznej ThreadPool&gisterWaitForSingkObject.

Utwórz instancję delegata

którą chcesz wykonać. Zarejestruj instancję delegata i obiekt nanie na

Omówienie WaitHandk (omówionej w przepisie 4.2) w celu RegisterWaitForSingkObject klasy Thre­ adPool pozwoli zarejestrować instancję delegata WaitOrTimerCallback do wykonania przez wątek z puli wątków w momencie, gdy określony obiekt, wyprowadzony z klasy WaitHandk, Można użyć klasy wyprowadzonej z klasy

wywołania wykonania metody. Posłużenie się metodą

wchodzi w stan sygnalizowany. Można skonfigurować pulę wątków dla pojedynczego wykona­ nia metody albo zarejestrować automatyczne wykonanie mecody przy kolejnych sygnalizacjach

WaitHandk. Jeśli WaitHandk jest SingkObject, wykonanie metody

już zasygnalizowany w chwili wywołania nastąpi natychmiast. Mecoda

Unregister

RegisterWaitFor­ System.

obiektu

92

C#

-

Księga przykładów

7hreading.RegisteredWaitHandl.e,

zwracanego przez metodę

RegisterWaitForSingl.eObject,

służy

do anulowania rejestrowanej operacji oczekiwania. Klasą powszechnie używaną jako przełącznik jest AutoResetEvent, która automatycznie przy­

Manual­ ResetEvent i Mutex, które jednakże wymagają ręcznej zmiany sygnalizacji stanu. Przykład poniżej demonstruje wykorzystanie zdarzenia AutoResetEvent do przełączania realizacji metody o nazwie EventHandl.er. wraca stan niesygnalizowany po poprzedniej sygnalizacji. Można używać również klas

using System ; u s ing System . Th reading ; public class EventExecut ionExample { li Metoda wykonywana po zasygnal izowaniu AutoResetEvent l u b po li p rzek roczeniu czasu oczekiwan ia . p rivate s tatic void EventHandl e r ( ob j e c t state , bool t imedout ) { li Wyświetlenie komunikatu na konsol i . if ( t imedout ) { Console . Write line ( " { 0 } : Wait timed out . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; } else { Console . Writeline ( " { 0 } : { 1 } " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) , s tate ) ; } } public static void Main ( ) { li Utwo rzenie nowego AutoResetEvent w s tanie n iesygnalizowanym . AutoResetEvent autoEvent = new AutoResetEvent ( fa l se ) ; li Utwo rzenie nowej instan cj i delegowanej WaitO rTime rCa l l back li odwołuj ącej się do metody EventHandle r . WaitO rTime rCa l l back handl e r = new Wa itOrTimerCal l ba c k ( EventHandle r ) ; li Utwo rzenie obiektu stanu p rzekazywanego do metody obsługi zda rzeń li W tym p rzypadku j est to komunikat do wyświetlenia . st ring state = "AutoResetEvent s ignaled . " ; li Rej est rowan ie instancj i delegowanej , oczekuj ącej na sygnalizac j ę l i AutoResetEvent . Registe redWaitHandle handle = Th readPool . RegisterWaitForSingleObj ec t ( autoEvent , hand l e r , stat e , 3000 , false ) ; Console . Writeline ( " P ress ENTER to signal the AutoResetEve n t "

+

" or ente r \ " Cancel \ " to u n register the wait ope ration . " ) ; while ( Consol e . Readline ( ) . ToUppe r ( ) ! = " CANCEL " ) { li Jeżeli nie wpisano " Cancel " , sygnalizowanie AutoResetEvent , li co spowodu j e wykonanie metody EventHandl e r .

Rozdział 4: Wątki, procesy i synchronizacja

93

autoEvent . Set ( ) ; } li Wyrej est rowanie ope racj i oczekiwania . Console . WriteLine ( " Un registe ring wait ope rat ion . " ) ; handle . Un registe r ( n u l l ) ; li Oczekiwanie . . . . Console . WriteLine ( "Main method complete . P re s s Ente r . " ) ; Console . ReadLine ( ) ; } }

4.5 Wykonanie metody przy użyciu nowego wątku Problem Chcesz uruchomić i wykonać kod w jego własnym wątku, zachowując pełną kontrolę nad sta­ nem i działaniem wątku.

Rozwiązanie Zadeklaruj metodę zwracającą void i nie wymagającą dostarczania argumentów. Utwórz instan­ cję delegata System. 1hreading. 1hreadStart zawierającą odniesienie do metody. Utwórz nowy obiekt System. 1hreading. 1hread i przekaż go instancji delegata jako argument dla jego kon­ struktora. Wywołaj metodę 1hread.Start, aby rozpocząć wykonywanie metody.

Omówienie Aby uzyskać maksimum kontroli i elastyczności przy tworzeniu aplikacji wielowątkowych, należy przejąć bezpośredni nadzór nad tworzeniem wątków i zarządzaniem nimi. Jest to naj­ bardziej złożony aspekt programowania wielowątkowego, ale jednocześnie jedyny sposób, żeby uniknąć restrykcji i ograniczeń związanych z podejściem prezentowanym w czterech poprzed­ nich przepisach, dotyczących użycia wątków z puli wątków. Klasa 1hread udostępnia mecha­ nizm tworzenia i kontroli wątków. Aby utworzyć i uruchomić nowy wątek, należy prześledzić następujący proces: l.

Utworzenie instancji delegata 1hreadStart, odwołującego się do metody zawierającej kod, który ma działać w nowym wątku. Jak każdy delegat, 1hreadStart może się odnosić do metody statycznej lub metody instancji. Ta metoda nie może przejmować argumentów i musi zwracać void.

2. Utworzenie nowego obiektu 1hread i przekazanie instancji delegata 1hreadStart jako argu­ ment konstruktorowi 1hread. Nowy wątek ma stan początkowy 'nie rozpoczęty' Unstarted (element typu wyliczeniowego System. 1hreading. 1hreadState). -

94

C#

-

Księga przykładów

3. Wywołanie mecody Start dla obiektu wątku 7hread, co zmieni jej stan na 7hreadState.Running i rozpocznie wykonywanie metody, do której odwołuje się instancja delegata 7hreadStart (jeśli metoda Start wstanie wywołana więcej niż raz, nastąpi wywołanie wyjątku System.

7hreading. 7hreadStateException). Ponieważ delegat 7hreadStart nie deklaruje argumentów, nie można przekazać danych bezpo­ średnio do metody odniesienia. Aby przekazać dane do nowego wątku, należy skonfigurować je w taki sposób, żeby kod w nowym wątku miał do nich dostęp. Najczęściej stosowanym podejściem jest zadeklarowanie klasy zawierającej zarówno wymagane dane przez wątek, jak i wykonywaną przez niego metodę. Aby zapoczątkować działanie nowego wątku, należy utwo­ rzyć instancję obiektu zasobnika, skonfigurować jego stan i dopiero wcedy uruchomić nowy wątek. Oto przykład: u s ing System ; u s ing System . T h reading ; public class Th readExample { li P rywatne zmienne p rzechowu j ą stan na użytek nowego wątku .

p rivate int iteration s ; p rivate st ring mes sage ; p rivate int delay ; public Th readExample( int ite ration s , st ring message , int delay ) { this . ite rations = iteration s ; this . mes sage = message ; this . delay

=

dela y ;

} public void Start ( ) { li Two rzenie instancj i delegowanej Th readSta rt , wywołu j ącej li DisplayMes sage .

Th readStart method = new Th readSt a rt ( this . DisplayMessage ) ; l i Two rzenie nowego obiektu Th read i p rzekazanie instanc j i delegowanej l i Th readSta rt do j ej kon st rukt o ra .

Th read t h read = new Th read ( method ) ; Console . W riteline ( " { 0 } : Sta rting new t h read . " , OateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; l i Uruchomienie nowego wąt ku .

th read . Sta rt ( ) ; } p rivate void DisplayMessage ( ) { li Wyświetlenie komunikatu okresloną l ic z bę raz y , oczekiwanie pomiędzy li nimi p rzez u stalony cza s .

f o r ( int count

=

0 ; count < ite rations ; count++ ) {

Consol e . Writeline ( " { 0 } : { 1 } " , OateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) , message ) ; li Oczekiwanie .

Rozdział 4: Wątki, procesy i synchronizacja

95

Th read . Sleep ( delay ) ; } } public static void Main ( ) { li UTwo rzenie nowego obiektu Th readExample . Th readExample example

=

new Th readExamp le ( S , "A th read example . " , 500 ) ; li U ruchomienie obiektu Th readExampl e . example . Sta rt ( ) ; li Dalsze p rzetwa rzanie . f o r ( int count

=

0 ; count < 13 ; count++ ) {

Console . WriteLine ( " {0 } : Continue p roce s s ing . . . " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) ) ; Th read . Sleep ( 200 ) ; } li Oczekiwanie . . . . Console . W riteLine ( " Main method complete . Press Ente r . " ) ; Console . ReadLine ( ) ; } }

4.6 Nadzór nad wykonaniem wątku Problem Chcesz nadzorować moment stanu i zakończenia wątku, chcesz mieć również możność wstrzy­ mania wykonania wątku.

Rozwiązanie Użyj następujących metod dla wątku

7hread, kt6ry chcesz nadwrować: Abort, lnterrupt, Resume,

Start i Suspend.

Omówienie Metody klasy

7hread,

przedstawione w tabeli 4- 1 , zapewniają wysoki stopień nadzoru nad

wykonaniem wątku. Każda z tych metod wraca natychmiast do wywołującego wątku. Sean bie­ żącego wątku ma wpływ na wynik przywołanej metody, ponieważ może on się szybko zmienić. Z tego powodu należy tworzyć kod defensywny, aby m6c przechwycić r6żne wyjątki, pojawia­ jące się podczas pr6b zarządzania wątkiem

7hread, i odpowiednio nimi zarządzać.

96

C#

-

Księga przykładów

Tabela 4-1

Nadzór nad wykonaniem wątku

Metoda

Opis

Abort

Powoduje zakończenie działania wącku poprzez przesłanie wyjątku System. 1hre­ ading. 1hread.AbortException do kodu, w którym pracuje wącek. Kod przerwa­ nego wątku może przechwycić wyjątek 1hread.AbortException, aby wyczyścić błąd, ale moduł runcime aucomacycznie wywoła wyjątek z powrotem, zapew­ niając cym samym zakończenie wącku - chyba że nastąpi wywołanie ResetAbort. Powrót z Abort nastąpi nacychmiasc, ale moduł runcime określi jedynie moment wywołania wyjątku, nie można więc wnioskować o momencie zakończenia wątku na podstawie powrotu z wywołania Abort. W celu określenia momentu zakończenia przerwanego wątku, należy zastosować techniki opisane w przepi­ sie 4.7. Po odrzuceniu wącku jego działania nie można wznowić.

lnterrupt

Wywołuje wyjątek System. 1hreading. 1hreadlnterruptedException w kodzie, w którym wątek pracuje, o ile znajduje się on w stanie WaitSleep]oin. Ozna­ cza to, że wątek wywołał Sleep, join (przepis 4.7) łub oczekuje na sygnalizację ze strony WaitHand/e, albo reż pozyskał obiekt używany do synchronizacji wąt­ ków (przepis 4.8). Jeśli jednak wątek nie znajduje się w stanie S/eep]oin, wyjątek 1hreadlnterruptedException wstanie wywołany następnym razem, gdy wątek osiągnie stan WaitS!eep]oin.

Resume

Wznawia działanie zawieszonego wątku (patrz metoda Suspenrl). Wywołanie Resume dla nie zawieszonego wątku generuje wyjątek System. 1hreading. 1hread­ StateException dla wątku wywołującego.

Start

Rozpoczyna działanie nowego wątku; w przepisie 4.5 podano, jak używa się metody Start.

Suspend

Zawiesza działanie wątku do momentu wywołania metody Resume. Próba zawieszenia wątku już zawieszonego nie daje efektu, ale wywołanie Suspend dla wątku, który jeszcze nie wystartował, albo już zakończył działanie, wygeneruje wyjątek 1hreadStateException dla wątku wywołującego.

Klasa 1hreadControlExample, przedstawiona niżej, demonstruje użycie metod 1hread, wyszcze­ gólnionych w Tabeli 4- 1 . W przykładzie podanym niżej wscał uruchomiony kolejny wątek, który okresowo wyświetla komunikat na konsoli, a następnie zasypia. Wprowadzając pole­ cenia można dokonywać operacji przerwania, zawieszenia, wznowienia i odrzucenia wtór­ nego wątku. u sing System ; u s ing System . Th reading ; public class Th readCont rolExample { p rivate static void DisplayMes sage ( ) {

li

Powta rzanie wyświetlania komun ikat u na konsol i .

wh ile ( t rue ) { t ry { Console . WriteLine ( " { 9 }

Second t h read running . E n t e r "

Rozdział 4: Wątki, procesy i synchronizacja + " ( S ) u spend ,

97

( R ) esume , ( I ) nt e r rupt , o r ( E ) xit . " ,

DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ;

li

Oczekiwanie 2 sekundy .

Th read . Sleep ( 2000 ) ; } catch ( T h read i nt e r ruptedExcept ion ) {

li li li

Wątek został p rzerwany . P rzechwycenie Th readinte r ruptedException umożliwia pod j ęcie odpowiedniej akcj i i kontynuac j ę .

Consol e . WriteLine ( " { 0 } : Second th read int e r rupted . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; } catch ( Th readAbo rtExcept ion abortEx ) {

li li li li

Obiekt we własności Th readAbo rt Exception . ExceptionState j est udos tępniany p rzez wątek , któ ry wywołał Th read . Ab o rt . W t ym p rzypadku zawiera on tekst opisuj ący p rzyczynę p rze rwania .

Console . WriteLine ( " { 0 } : Second t h read abo rted ( { 1 } ) " , DateTime . Now . ToSt ring ( "HH : mm : s s . f f f f " ) , abo rtEx . ExceptionState ) ; } } } public s tatic void Main ( ) {

li li

Utwo rzenie nowego obiektu Th read obj ect i p rzekazanie go do instan c j i delegowanej Th readSt a rt , wywołu j ącej DisplayMes sage .

Th read t h read = new Th read ( new Th readSta rt ( D isplayMessage ) ) ; Console . WriteLine ( " { 0 } : Sta rting second t h read . " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) ) ;

li

U ruchomienie d rugiego wąt ku .

t h read . Start ( ) ;

li

Pętla i wykonywan ie poleceń wpisywanych p rzez użytkownika .

c h a r command = ' ' ; do { st ring input = Console . ReadLine ( ) ; if ( input . Length > 0 ) command = input . ToUppe r ( ) [ 0 ] ; else command = '

'

;

switch ( command ) { case ' S ' :

li

Zawieszenie d rugiego wątku .

Console . WriteLine ( " { 0 } : Suspending second th read . " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f ff " ) ) ; t h read . S u s pend ( ) ; b reak ; case ' R ' :

98

C#

-

Księga przykładów

li

Wznowienie d rugiego wątku .

t ry { Console . W riteLine ( " {0 } : Resuming second t h read . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; t h read . Resume ( ) ; } catch ( Th readStateException ) { Console . Writeline ( " { 0 } : Th read was n ' t su spended . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; } b reak ; case ' I ' :

li

Przerwanie d rugiego wątku .

Console . WriteLine ( " { 0 } : I nte r rupting second t h read . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; th read . Interrupt ( ) ; b rea k ; case ' E ' :

li li

Po rzucenie d rugiego wątku i p rzekazanie obiektu stan u , w tym p rzypadku komun i katu .

Console . WriteLine ( " { 0 } : Aborting second t h read . " , DateTime . Now . ToSt ring ( "HH : mm : s s . f f f f " ) ) ; th read . Abort ( "Te rminating example . " ) ;

li

C zekanie na zat rzymanie d rugiego wątku .

t h read . Join ( ) ; b reak ; } } while ( command ! = ' E ' ) ;

li

Oczekiwanie . . . .

Console . W riteLine ( "Main method complet e . P ress Ente r . " ) ; Console . ReadLine ( ) ; } }

Rozdział 4: Wątki, procesy i synchronizacja

99

4. 7 Rozpoznanie momentu zakończenia wątku Problem Chcesz wiedzieć, kiedy skończył się wątek.

Rozwiązanie Użyj cechy IsAlive lub metody ]oin klasy

Thread.

Omówienie Najprostszą metodą sprawdzenia, czy wątek zakończył swoje działanie, jest testowanie właści­ wości

Thread.IsAlive. Ta właściwość zwraca wartość true, jeśli wątek wystartował, ale nie zakoń­

czył działania ani nie został przerwany. Zazwyczaj dany wątek będzie musiał czekać z działalnością, aż inny zakończy swoje prze­ twarzanie. Zamiast testować co w pęcli lsA/ive, można wykorzystać metodę

Thread.]oin

.

Metoda

]oin powoduje zablokowanie wątku wywołującego, dopóki wywołany nie zakończy działania, a wtedy wątek wywołujący wznowi swoje działanie. Opcjonalnie można określić czas (wartość int lub

TimeSpan),

po którym zakończy się operacja ]oin i wątek wywołujący wznowi działanie.

Jeśli zostanie wyspecyfikowana wartość limitu czasu (time-out), metoda ]oin zwróci wartość true, gdy wątek skończy działanie, albo fa/se - po wyczerpaniu limitu czasu przez metodę Join.

W poniższym przykładzie wykonywany jest drugi wątek i następuje wywołanie metody ]oin, aby zaczekać na zakończenie działania tego wątku. Ponieważ przetwarzanie zajmuje drugiemu wątkowi około pięciu sekund, a metoda]oin określa limit czasu równy trzy sekundy, ]oin będzie się zawsze znajdować w scanie wyczerpania czasu (time out), a na konsoli będzie wyświeclany odpowiedni komunikat.

using System ; using System . Th reading ; public c l a s s Th readFinishExample { p rivate static void DisplayMes sage ( ) {

li

Wyświetlenie komunikatu na konsoli 5 razy .

f o r ( int count

=

0 ; count < 5 ; count++ ) {

Console . W riteline ( " { 0 } : Second t h read " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ;

li

oczekiwanie 1 sekundę .

Th read . Sleep ( l000 ) ; } } public static void Main ( ) {

li li

Two rzenie instancj i delegowanej Th readSta rt , wywołuj ącej DisplayMe s sage .

1 OO C#

-

Księga przykładów Th readSta rt method = new Th readSt a rt ( Di splayMessage ) ;

li li

Two rzenie nowego obiektu Th read i p rzekazanie instanc j i delegowanej Th readSta rt do j ej konst ruktora .

Th read th read = new Th read ( method ) ; Console . WriteLine ( " { 0 } : Start ing second th read . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ;

li

Uruchomienie d rugiego wątku .

th read . Sta rt ( ) ;

li

Blokowanie do zakoń czenia d rug iego wątku lub do upływu 3 sekund .

if ( ! th read . Join ( 3000 ) ) { Console . WriteLine ( " { 0 } : J oin t imed out ! ! " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; }

li

Oczekiwanie . . . .

Console . WriteLine ( "Main method complete . Pres s Ente r . " ) ; Console . ReadLine ( ) ; } }

4.8 Synchronizacja działalności wielu wątków Problem Chcesz tak skoordynować działalność wielu wątków, aby zapewnić ekonomiczne użytkowanie wspólnych zasobów oraz zabezpieczyć współużytkowane dane przed ewentualnym uszkodze­ niem w wypadku, gdy przełączanie kontekstowe wątku wystąpi podczas operacji zmieniają­ cej dane.

Rozwiązanie Użyj klas: Monitor, Auto&setEvent, Manua/.ResetEvent i Mutex z przestrzeni nazw System. Threading.

Omówienie Największą trudność przy pisaniu wielowątkowych aplikacji sprawia zapewnienie zgodnej pracy tych wątków. Jest co zagadnienie powszechnie określane mianem synchronizacji wątków i obej­ muje poniższe problemy: •

Zapewnienie wątkom poprawnego dostępu do współdzielonych obiektów i danych, nie powodującego zniszczenia.

Rozdział 4: Wątki, procesy i synchronizacja •

1 01

Umożliwienie działania wątkom cylko wtedy, gdy są do tego uprawnione i ograniczenie do minimum ich nadmiarowości, gdy są w stanie jałowym.

Najczęściej używanym mechanizmem synchronizacji jest klasa Monitor. Klasa ta umożliwia założenie przez wątek indywidualnego zamka na obiekcie poprzez wywołanie metody statycznej Monitor.Enter. Dzięki założeniu zamka przed dostępem do dzielonych zasobów lub danych można zapewnić, że dostęp do nich będzie możliwy tylko dla jednego wątku w danym momen­ cie. Kiedy wątek przestaje się komunikować z zasobami, zwalnia zamek, umożliwiając dostęp innemu wątkowi. Blok kodu, umożliwiający takie zachowanie, jest często nazywany critical section (sekcją krytyczną). Można użyć dowolnego obiektu w taki sposób, żeby działał jako zamek. Często stosuje się kluczowe słowo this, żeby otrzymać zamek na bieżącym obiekcie. Zasadniczy problem stanowi fakt, że wszystkie wątki, próbujące uzyskać dostęp do dzielonych zasobów, usiłują uzyskać ten sam zamek. Wszystkie inne wątki, próbujące sforsować zamek na cym obiekcie, zostaną zablo­ kowane (czyli uśpione - wejdą w stan WaitSleep]oin) i dołącwne do kolejki korzystania z zamka ready q�. dopóki wątek, który włada zamkiem, nie przywoła metody statycznej Monitor. Exit. Gdy wątek władający zamkiem przywoła Exit {wyjfcie), zamek uzyska jeden z wątków z kolejki ready queue. Jeśli właściciel zamka nie zwolni go przez wywołanie Exit, wszystkie inne wątki zostaną zablokowane na czas nieokreślony. Tak więc ważne jest umieszczenie wywołania Exit w bloku finally, żeby mieć pewność, że wywołanie miało miejsce nawet wtedy, gdy wystą­ pił wyjątek. Ponieważ przy aplikacjach wielowątkowych często stosuje się Monitor, język C# zapew­ nia udogodnienie na poziomic języka w postaci instrukcji lock, które kompilator przekłada na użycie klasy Monitor. Blok kodu zawarty w instrukcji lock stanowi ekwiwalent wywołania Monitor.Enter przy wchodzeniu do bloku oraz Monitor.E.xit przy jego opuszczaniu. Ponadto kompilator automatycznie umieści wywołanie Monitor.Exit w blokufinally, aby zapewnić zwol­ nienie zamka w razie wywołania wyjątku. Wątek, który w danym momencie zarządza zamkiem, może wywołać Monitor. Wait, co spo­ woduje zwolnienie zamka i umieszczenie wątku wywołującego w kolejce do zamka wait queue. Wątki w tej kolejce są również w stanie uśpienia - WaitSleepfoin i nie przestaną być bloko­ wane do momentu, gdy wątek zarządzający zamkiem wywoła metodę Pu/se lub metodę PulseAll klasy Monitor. Pu/se przemieszcza jeden z oczekujących wątków z kolejki wait queue do ready queue, podczas gdy PulseAll przemieszcza do niej wszystkie wątki. Kiedy wątek zostanie prze­ mieszczony w ten sposób, może przejąć kontrolę nad zamkiem następnym razem, gdy zostanie on zwolniony. Ważne jest zrozumienie, że wątki w kolejce oczekiwania na zamek nie uzyskają kontroli nad zwolnionym zamkiem: będą czekać przez czas nieokreślony na któreś z wywołań Pu/se lub PulseAll, które je przemieści do kolejki gotowości ready queue. Użycie wywołań Wait i Pu/se jest powszechnie stosowane wtedy, gdy używa się puli wątków do przetwarzania pozycji pracy z dzielonej kolejki. Klasa ThreadSyncE.xample, pokazana niżej, ilustruje użycie zarówno klasy Monitor, jak i instrukcji lock. W przykładzie zapoczątkowano trzy wątki, z których każdy po kolei dysponuje zamkiem do obiektu o nazwie consoleGate. Każdy z wątków wywołuje Monitor. Wait. Kiedy użytkownik naciśnie przycisk Enter po raz pierwszy, zostanie wywołany Monitor.Pu/se, który zwolni jeden z czekających wątków. Kiedy przyciśnie Enter po raz drugi, zostanie wywołany Monitor.PulseAll, zwalniający wszystkie pozostałe czekające wątki. -

1 02 C #

-

Księga przykładów

u s ing System ; u s ing System . Th reading ; public class Th readSyncExample {

li li

Dekl a racj a obiektu statycznego , używanego do blokowania w metodach statycznych , gdyż nie ma innego dostępu .

p rivate static obj ect con soleGate

=

new Obj ect ( ) ;

p rivate static void D i s playMes sage ( ) { Console . WriteLine ( " { 0 } : Th read sta rted , acqui ring lock . . . " , DateTime . Now . ToSt ring ( " HH : mm : ss . f f f f " ) ) ;

li

Utwo rzenie blokady obiektu consoleGate .

t ry { Monito r . Ente r ( con soleGate ) ; Console . WriteLine ( " {e } : { l } " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) , "Acqui red consoleGate loc k , waiting . . . " ) ;

li

Oczekiwanie na wywołanie Pulse w obiekcie con soleGate .

Mon ito r . Wait ( consoleGate ) ; Console . WriteLine ( " {e } : Th read pul sed , t e rminating . " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) ) ; } f inally { Monito r . Exi t ( consoleGate ) ; } } public static void Ma in ( ) {

li

Utwo rzenie blokady obiektu consoleGate .

lock ( consoleGate ) {

li li

Two rzenie i u ruchamianie t rzech nowych wątków, wykonuj ących metodę D i splayMe s s sage .

for ( int count

=

e ; count < 3 ; count++ ) {

( new Th read ( new Th readSta rt ( DisplayMessage ) ) ) . St a rt ( ) ; } } Th read . Sleep ( l900 ) ;

li

Budzenie poj edynczego oczekuj ącego wąt ku .

Console . W riteLine ( " {e } : { l } " , Da teTime . Now. Tost ring ( "HH : mm : s s . ff f f " ) , " P re s s Ente r to pulse one waiting t h read . " ) ; Console . ReadLine ( ) ;

li

Utwo rzenie blokady obiektu consoleGate .

lock ( con soleGate ) {

li

wykonanie Pulse na oczekuj ącym wątku .

Mon ito r . Pulse ( consoleGate ) ;

Rozdział 4: Wątki, procesy i synchronizacja

1 03

}

li

Budzenie wszystkich oczekuj ących wątków .

Console . WriteLin e ( " { 0 } : { l } " , DateTime . Now . ToSt ring ( " HH : mm : s s . f f f f " ) , " P ress Enter to pulse a l l waiting t h read s . " ) ; Console . Read line ( ) ;

li

Utwo rzenie blokady obiektu consoleGate .

lock ( consoleGate ) {

li

Wykonanie Pul se na wszystkich oczekuj ących wątkach .

Monit o r . PulseAl l ( consoleGate ) ; }

li

Oczekiwanie . . . .

Console . WriteLine ( "Main method complete . P ress Ente r . " ) ; Console . Read Line ( ) ; } } Inne klasy, powszechnie używane w celu synchronizacji wątków, stanowią podklasę klasy System.

Threading. WaitHandk. Należą do niej AutoResetEvent, ManualResetEvent i Mutex. Instancje tych klas mogą występować w scanie sygnaliwwanym lub nie. Wątki mogą używać metod tych klas, wyszczególnionych niżej w tabeli 4-2 (odziedzicwnych po klasie

WaitSkep]oin i oczekiwać na stan jednego Handk, aby stać się stanami sygnalizowanymi. stan

Tabela 4-2

WaitHandk), aby wprowadzić Wait­

lub więcej obiektów wyprowadwnych z

Metody WaitHandle dotyczące synchronizacji wykonania wątków

Metoda

Opis

WaitAny

Metoda statyczna, powodująca wprowadzenie wątku wywołującego w stan

WaitS!eep]oin i oczekiwanie przez nią na sygnalizację jednego z obiektów Wait­ Handle z tablicy WaitHandk. Można również określić wartość limitu czasu. WaitAll

Metoda statyczna, powodująca wprowadzenie wątku wywołującego w stan

WaitSkep]oin WaitHandle z

i oczekiwanie przez nią na sygnalizację wszystkich obiektów tablicy

WaitHandle. Można również tutaj wyspecyfikować war­ WaitAliExample w przepisie 4.2 demonstruje użycie

tość limitu czasu. Metoda metody

WaitOne

WaitAll.

WaitSkep]oin i oczekiwanie WaitHandk. Metoda WaitingExampk w przepisie 4.2 demonstruje użycie metody WaitOne. Powoduje przejście wywołującego wątku w stan na sygnalizację określonego obiektu

AutoResetEvent, Manua/ResetEvent i Mutex dotyczą przejścia AutoResetE­ vent i ManuaLResetEvent są względem procesu lokalne. Aby zasygnalizować AutoResetEvent, należy wywołać jego metodę Set, która uwolni tylko jeden wątek oczekujący na zdarzenie. AutoResetEvent wróci automatycznie do stanu niesygnalizowanego. Przykład podany w przepisie 4.4 demonstruje użycie klasy AutoResetEvent. Główne różnice między klasami

od stanu niesygnalizowanego do sygnalizowanego oraz ich widzialności. Klasy

1 04 C#

-

Księga przykradów

Klasa Manuai&setEvent musi być ręcznie przełączana między stanami sygnalizacji i jej braku przy użyciu metod Set i Reset. Wywołanie Set dla zdarzenia ManuaLResetEvent spowoduje przełą­ czenie do stanu sygnalizacji i uwolnienie wszystkich wątków oczekujących na zdarzenie. Manuai­ &setEvent przejdzie w stan braku sygnalizacji tylko po wywołaniu &set. Mutex jest sygnalizowany wtedy, kiedy nie należy do żadnego wątku. Wątek bierze w posia­ danie Mutex albo podczas konstruowania, albo poprzez użycie jednej z metod wyszczególnio­ nych w tabeli 4-2. Z tego posiadania wątek zostaje zwolniony dzięki wywołaniu metody Mutex. &l.easeMutex, która sygnalizuje Mutex i umożliwia przejęcie własności przez inny wątek. Główną zaletą Mutex jest możliwość użycia wątków przecinających granice procesu do synchronizacji; przepis 4 . 1 2 demonstruje wykorzystanie Mutex. Obok opisanej wyżej funkcjonalności, główną różnicę między klasą WaitHandie a klasą Monitor stanowi to, że Monitor jest kompletnie zaimplementowany w zarządzany kod, a klasy WaitHandle zapewniają opakowania wokół prymitywów systemu operacyjnego. Srwarza co następujące konsekwencje: •

Użycie klasy Monitor sprawia, że kod staje się bardziej przenośny, ponieważ nie zależy od właściwości systemu operacyjnego.



Można wykorzystywać klasy wyprowadzone z WaitHandle, aby synchronizować wykonanie zarówno zarządzanych, jak i nie zarządzanych wątków, podczas gdy Monitor może synchro­ niwwać tylko zarządzane wątki.

4.9 Tworzenie instancji kolekcji przystosowanej do działania w trybie wielowątkowym Problem Chcesz, żeby liczne wątki mogły bezpiecznie realizować równoległy dostęp do zawartości kolekcji.

Rozwiązanie Użyj instrukcji lock w swoim kodzie, aby zsynchroniwwać dostęp wątków do kolekcj i, albo stosuj dostęp do kolekcj i poprzez opakowanie przystosowujące do pracy w trybie wielo­ wątkowym.

Omówienie Klasy standardowego zbioru z przestrzeni nazw System. Collections i System. Coilections.Specialized powinny domyślnie umożliwić licznym wątkom równoczesny odczyt zawartości zbioru. Jeśli jednak jeden lub więcej z tych wątków spróbuje ten zbiór modyfikować, mogą powstać prob­ lemy. Przyczyną cego jest możliwość przerwania aktywności wątków przez system operacyjny, gdy modyfikacje zbioru po części już dokonane, są w toku. Powstawienie wtedy zbioru w scanie przejściowym prawie na pewno spowoduje nieprawidłowe sytuacje, takie jak błędny dostęp

Rozdział 4: Wątki, procesy i synchronizacja

1 05

innego wątku do tego zbioru, zwrot nieprawidłowych danych lub nawet całkowite zniszcze­ nie zbioru.

Uwaga Używanie synchronizacji wątków wprowadza nadmiarowość działań. Decyzja o nie stosowaniu kolekcji nieprzystosowanych do pracy w trybie wielowątkowym popra­ wia wydajność w zdecydowanej większości sytuacji, gdy tryb wielowątkowy nie jest wyko­ rzystywany.

Synchronized; Hashtable, Queue, Sortedlist i Stack z przestrzeni nazw System. Collec­ tions. Metoda Synchronized przyjmuje jako argument obiekt zbioru odpowiedniego typu i zwraca Najpowszechniej używane kolekcje implementują metodę statyczną o nazwie

dotyczy to klas Arraylist,

obiekt, dostarczający zsynchronizowane opakowanie wyspecyfikowanego obiektu zbioru. Obiekt opakowania wstaje zwrócony jako ten sam typ, co oryginalna kolekcja, ale wszystkie metody i właściwości odczytu oraz zapisu kolekcji umożliwiają dostęp do jej zawartości tylko jednemu wątkowi w danym momencie. Poniższy kod wyjaśnia, jak należy utworzyć

Hashtable

przystosowaną do pracy w trybie wielowątkowym (to, czy kolekcja jest przystosowana do pracy w trybie wielowątkowym, można sprawdzić przy użyciu właściwości

li

IsSynchronized).

Two rzenie standa rdowej Hashtable

Hashtable hUnsync = new Hashtable ( ) ;

li

Two rzenie synch ronizowanego wrappe ra

Hashtable hSync = Hashtable . Sync h ronized ( hUnsync ) ; Klasy zbioru takie jak HybridDictionary, ListDictionary i StringCollection z przestrzeni nazw System. Collections.Specialized nie implementują metody Synchronized. Aby umożliwić bez­ pieczny dostęp do instancji tych klas w trybie wielowątkowym, należy zaimplementować synchronizację ręczną przy użyciu obiektu

object,

zwróconego przez właściwość

SyncRoot,

jak

to pokazuje przedstawiony niżej kod:

li

Two rzenie zbio ru NameValueCollection .

NameValueCollection nvCollect ion = new NameValueCollect ion ( ) ;

li

założenie blokady NameVal ueCollection p rzed modyfikacj ą .

lock ( ( ( ICollection ) nvCol lection ) . SyncRoot ) {

li

Modyfikowanie NameValueCollection . . .

}

Name ValueCollection jest wyprowadwna z klasy NameObject­ CollectionBase, która jawnie posługuje się interfejsem implementacji w celu zaimplementowania właściwości /Collection. SyncRoot. Jak widać, trzeba dopasować Name ValueCollection do instancji !Collection, zanim będzie można zrealizować dostęp do właściwości SyncRoot. To dopasowanie nie jest konjeczne dla innych wyspecjalizowanych klas zbiorów, takich jak HybridDictionary, ListDictionary i StringCollection, które nie używają jawnej implementacji interfejsu w celu zaim­ plementowania SyncRoot. Należy zauwazyc, ze klasa

1 06

C# - Księga przykładów Aby używać klasy synchronizowanego zbioru w sposób ekstensywny, należy uprościć kod poprzez utworzenie nowej klasy, wyprowadzonej z klasy zbioru, który ma być użyty. W tym wypadku należy nadpisać pola klasy bazowej, zapewniającej dostęp do zawartości zbioru, a następnie wykonać synchronizację przed wywołaniem pola ekwiwalentnej klasy. Normalnie w celu wykonania synchronizacji na obiekcie object zwróconym przez właściwość SyncRoot klasy bazowej zostałaby użyta instrukcja lock, jak to zostało omówione wcześniej. Jednakże przy two­ rzeniu wyprowadzonej klasy jest możliwa implementacja bardziej zaawansowanych technik syn­ chronizacji, jak np. zastosowanie techniki System. Threading.ReaderWriterLock, która umożliwia korzystanie z wielu wątków odczytu przy tylko jednym wątku zapisu.

4.1 O Zapoczątkowanie nowego procesu Problem Chcesz wykonać aplikację w nowym procesie.

Rozwiązanie Posłuż się obiektem System. Diagnostics.ProcessStartlnfo, aby określić szczegóły dla aplikacji, którą chcesz uruchomić. Następnie utwórz obiekt System.Diagnostics.Process, reprezentujący nowy proces, przypisz obiekt ProcessStartlnfo do właściwości Start!nfo obiektu Process twojego procesu i uruchom aplikację poprzez wywołanie Process.Start.

Omówienie Klasa Process stanowi zarządzaną reprezentację procesu systemu operacyjnego i zapewnia pro­ sty mechanizm wykonania obu rodzajów aplikacji: zarządzanych i nie zarządzanych. Klasa Process implementuje cztery przeciążone odmiany metody Start, konieczne do zapoczątkowania nowego procesu. Dwa przeciążenia są metodami statycznmi i pozwalają wyspecyfikować tylko nazwę i argumenty nowego procesu. Na przykład obydwie poniższe instrukcje ilustrują wyko­ nanie Notepad w nowym procesie: li Wykonanie notepad . exe bez pa ramet rów wie rsza polecenia . P rocess . St a rt ( " notepad . exe" ) ; li Wykonanie notepad . exe z p rzekazaniem nazwy pliku j ako a rgument u . P roces s . St a rt ( " notepad . exe " , " SomeFile . txt " ) ;

Pozostałe dwie przeciążone odmiany metody Start wymagają utworzenia obiektu ProcessStart­ Info, skonfigurowanego zgodnie ze szczegółami procesu, który należy uruchomić; wykorzysta­ nie obiektu ProcessStart!nfo zapewnia większą kontrolę nad zachowaniem i konfiguracją nowego procesu. Tabela 4-3 podaje zestawienie niektórych powszechnie używanych właściwości klasy

ProcessStartlnfo.

Rozdział 4: Wątki, procesy i synchronizacja

1 07

Tabela 4-3 Właściwości klasy ProcessStartlnfo

Właściwość

Opis

Arguments

Argumenty w linii poleceń przekazywane do nowego procesu.

ErrorDial.og

Jeśli

Process. Start nie może uruchomić określonego procesu, wywoła wyją­ System. ComponentModel Win32Exception. Jeśli wartość ErrorDial.og wynosi troe (prawda) , Start wyświetli okno dialogowe błędu przed wywo­ tek

łaniem wyjątku.

FileName

Nazwa uruchamianej aplikacji. Można również wyspecyfikować dowolny typ pliku, dla którego wstało skonfigurowane powiązanie aplikacji. Można na przykład wyspecyfikować plik z rozszerzeniem .doc lub .xls, który spowoduje odpowiednio uruchomienie Microsoft Word lub Microsoft Excel.

WindowStyle

Element

typu

wyliczeniowego

System.Diagnostics.ProcessWindowStyle,

sterujący sposobem wyświetlania okna. Uprawnione wartości zawierają

Hidden, Maximized, Minimized i Norma/. WorkingDirectory

W pełni kwalifikowana nazwa katalogu, zapoczątkowanego dla nowego procesu.

Po zakończeniu czynności dotyczących obiektu Process należy go usunąć w edu zwolnienia zaso­

Cl.ose lub Dispose, albo utworzyć obiekt Process w obrębie instrukcji obiektu Process nie ma wpływu na proces systemowy nieprzerwanie

bów systemu, tzn. wywołać

using.

Zadysponowanie

zachodzący w warstwie wewnętrznej. W poniższym przykładzie zastosowano obiekt Process do wykonania Notepad w zmaksymali­ wwanym oknie i otwarcia pliku o nazwie C:\Temp\fi.le.txt. Następnie zostaje wywołana metoda

Process. WaitForExit,

która blokuje wątek wywołujący do momentu zakończenia procesu lub

wygaśnięcia określonego limitu czasu (time-out).

using System ; using System . Diagnos t i c s ; publ ic class S t a rt P rocessExample { public static void Main ( ) {

li li

Two rzenei obiektu P rocessStartinfo i s konfigu rowa nie go p rzy użyciu i n f o rmacj i wymaganych do u ru chomienia nowego p rocesu .

P rocessSta rt info sta rtinfo = new P ro ces sSta rtlnfo ( ) ; s t a r t l n f o . FileName = " notepad . exe " ; sta rtlnfo . Arguments = " f ile . txt " ; s t a rt l n fo . Wo rkingDi rec t o ry = @ " C : \Temp " ; s t a rt l n fo . WindowStyle = P rocessWindowStyle . Maximized ; sta rtlnfo . E r ro rDialog = t rue ;

li

Two rzenie nowego obiektu P rocess .

u s ing ( P roce s s p rocess = new P rocess ( ) ) {

li

P rzypisanie P rocessSta rt l n fo do p rocesu .

p rocess . S t a rt l n fo = s t a rtlnfo ;

1 08

C# - Księga przykładów t ry { li Uruchomienie nowego p rocesu . p rocess . S t a rt ( } ; li Oczekiwanie na zakończenie nowego p rocesu . Con sole . Writeline ( "Waiting 30 seconds f o r p rocess to" + " finish . " ) ; p roces s . WaitFo rExit ( 30000 ) ; } catch ( Exception ex ) { Console . Writeline ( " Could not start p roces s . " ) ; Console . W riteline ( ex ) ; } } li Oczekiwanie . . . . Console . Writeline ( "Main method complet e . Press Ente r . " ) ; Console . Readline ( ) ; } }

4.1 1 Zakończenie procesu Problem Chcesz zakończyć proces, na przykład aplikację lub usługę.

Rozwiązanie Utwórz obiekt Process reprezentujący proces systemu operacyjnego, który chciałbyś zakończyć. Dla aplikacji sysremu Windows powinieneś wywołać Process. CloseMain Window, aby przesłać komunikat zamknięcia do głównego okna aplikacji. Dla aplikacji typu Windows, które igno­ rują CloseMain Window, a także dla innych aplikacji innych typów, należy wywołać metodę

Process. Kil/.

Omówienie Jeśli nowy proces został uruchomiony z poziomu zarządzanego kodu przy użyciu klasy Process (omówionej w przepisie 4 . 1 O), można go zakończyć przy użyciu obiektu Process, który go repre­ zentuje. Można również uzyskać obiekt Process, który się odnosi do innych działających współ­ bieżnie procesów, przy użyciu metod statycznych klasy Process, które zebrano w tabeli 4-4.

Rozdział 4: Wątki, procesy i synchronizacja 1 09 Tabela 4-4

Metody umożliwiające otrzymanie odniesień Process

Metoda

Opis

GetCurrentProcess

Zwraca obiekt

Process, reprezentujący aktualnie akrywny proces.

GetProcessByld

Zwraca obiekt

Process, reprezentujący proces o wskazanym ID.

GetProcesses

Zwraca tablicę obiektów

Process,

reprezentującą wszystkie aktualnie

Process,

reprezentującą wszystkie aktualnie

akrywne procesy.

GetProcessesByName

Zwraca tablicę obiektów

akrywne procesy o podanej przyjaznej nazwie. Ta nazwa jest nazwą wykonywalnego pliku lub wątku bez jego rozszerzenia, na przykład

notepad lub cale. Po otrzymaniu obiektu wywołać jedną z metod:

Process reprezentującego proces, który ma być zakończony, należy CwseMain Windaw lub /(jU. Metoda CwseMain Windaw przesyła pole­

cenie zamknięcia do głównego okna aplikacji Windows. Metoda ta wywołuje taki sam skutek,

co zamknięcie przez użytkownika głównego okna z menu systemu i umożliwia wykonanie przez aplikację jej normalnego programu zamknięcia. Metoda

CwseMain Window

nie spowoduje

zakończenia tych aplikacji, które nie mają głównego okna, ani tych, które mają to okno niedo­ stępne

(disabled)

-

zazwyczaj dlatego, że wyświetlane jest aktualnie modalne okno dialogowe.

CwseMain Window zwróci wartość fa/se. CwseMain Windaw zwraca wartość true, jeśli polecenie zamknięcia wstało wysłane pomyśl­

W tych okolicznościach

nie, co nie gwarantuje jednak, że proces rzeczywiście się zakończył w tej chwili. Na przykład aplikacje używane do edycji danych po otrzymaniu tego polecenia umożliwiają zazwyczaj użytkownikowi zapamiętanie danych nie zapisanych do tej pory. W tych okolicznościach

CwseMa­ true, ale aplikacja po anulowaniu będzie nadal działała. Można wykorzystać metodę Process. WaitForExit dla zasygnalizowania zakończenia procesu oraz właś­ ciwości Process.HasExited do sprawdzenia, czy proces został zakończony. Alternarywnie można użytkownik ma zwykle możliwość anulowania operacji zamykania. Oznacza to, że

in Window

przywróci wartość

użyć metody J(jJl. Metoda

f(jfl po

prostu spowoduje natychmiastowe zakończenie procesu. Użytkownik nie

będzie w stanie zatrzymać zamykania i wszystkie nie zapamiętane dane wstaną utracone. Kilt stanowi więc jedyną opcję zakończenia zarówno dla aplikacji typu Windows, która nie odpo­ wiada na

CwseMain Windaw, jak i dla aplikacji innych typów.

W poniższym przykładzie zostaje uruchomiona nowa instancja dotycząca Notepad, nastę­ puje pięciosekundowe oczekiwanie, po czym proces Notepad zostaje zakończony. Początkowo

CwseMain Windaw. Jeśli CW­ seMain Window zwróci fa/se lub po wywołaniu CwseMain Window process Notepad nadal będzie działał, zostanie wywołana metoda Kil/, co definirywnie zakończy proces Notepad. Można wymusić na CwseMain Windaw zwrócenie wartości fa/se poprzez pozostawienie otwartego okna w przykładzie czynione są próby zakończenia procesu przy użyciu

dialogowego File Open.

using System ; using System . Th reading ; using System . Oiagnostics ; public class TerminateP rocessExample { public s tatic void Main ( ) {

1 10 C# - Księga przykładów li Two rzenie nowego obiektu P rocess i u ruchomienie notepad . ex e . u s ing ( P rocess p rocess = P rocess . Sta rt ( " notepad . exe" ) ) { li Oczekiwanie 5 sekund na zamknięcie p rocesu notepad . Con sol e . Writeline ( "Wait ing 5 seconds befo re te rminat i n g " + " notepad . exe . " ) ; Th read . Sleep ( 5000 ) ; li Zamykanie p rocesu notepad . Console . Writeline ( "Te rminating Notepad with CloseMainWindow . " ) ; li P róba wysłania komunikatu zamknięcia do głównego okna . if ( ! p roce s s . CloseMainWindow ( ) ) { li Komunikat zamknięcia n ie został wysłany - Kill Notepad . Console . Writeline ( " CloseMainWindow retu rned false " te rminating Notepad with Kill . " ) ;

-

" +

p rocess . Ki ll ( ) ; } e l se { li Komunikat zamknięcia wysłany ; oczekiwanie 2 sekundy li na potwie rdzenie zakoń czenia p rzed wykonaniem Kill . if ( ! p roces s . WaitFo rExit ( 200e ) ) { Console . Writeline ( " CloseMainWindow failed to" + " t e nninate - te rminating Notepad with Kil l . " ) ; p rocess . Ki l l ( ) ; } } } li Oczekiwanie . . . . Console . W riteline ( "Main method complete . P re s s Ente r . " ) ; Console . Readline ( ) ; } }

Rozdział 4: Wątki, procesy i synchronizacja

111

4.1 2 Ograniczenie równoczesności działania do jednej tylko instancji aplikacji Problem Chcesz, żeby użytkownik mógł uruchomić na raz tylko jedną instancję aplikacji.

Rozwiązanie Utwórz obiekt o nazwie System. Threading.Mutex i wywołaj sytuację, w której aplikacja spróbuje przejąć go na własność w momencie startu tego obiektu.

Omówienie Obiekt

Mutex zapewnia

mechanizm synchronizacji wykonania wątków pomiędzy granicami

procesów, a ponadto daje wygodną opcję ograniczenia działania do pojedynczej instancji danej aplikacji w danym momencie. Poprzez wymuszenie próby przejęcia na własność w momencie startu obiektu o nazwie

Mutex i wyjścia z

aplikacji

(exit)

w wypadku niepowodzenia tej akcji,

można zapewnić działanie tylko jednej instancji danej aplikacji. W poniższym przykładzie użyto obiektu

Mutex o nazwie MutexExample, żeby zrealirować to zadanie.

using System ; using System . Th reading ; public class MutexExample { public s tatic void Main ( ) {

li li

Zmienna logiczna wskazuj ąca , czy ta aplikacj a j es t począt kowym właścicielem Mutex .

bool ownsMutex ;

li li

P róba utwo rzenia i p rzej ęcia na wła s ność obiektu Mutex o nazwie MutexExampl e .

using ( Mutex mutex new Mutex ( t rue , " MutexExample " , out own sMutex ) ) { =

li li

J eżeli aplikacj a j est właścicielem Mutex , może kontynuować ; w p rzeciwnym wypadku ma zostać zakończona .

if ( ownsMutex ) { Console . Writeline ( "This application c u r rently owns the" + " mutex named MutexExampl e . Add it ional i nstances o f " + " this appl ication wil l not run until you relea s e " + " the mutex by p res sing Ente r . " ) ; Console . Read line ( ) ;

li

zwolnienie mutex

mutex . ReleaseMutex ( ) ;

112

C# - Księga przykładów } else { Consol e . WriteLine ( "Anothe r instance of t h i s application " al ready own s the mutex named MutexExample . Thi s " + " instance of the application will te rminate . " ) ; } } l i Oczekiwanie . . . . Console . WriteLine ( " Main method complet e . P res s Ente r . " ) ; Console . ReadLine ( ) ; } }

"

+

5 Przetwarzanie XM L Jednym z najważniejszych aspektów systemu Microsoft .NET Framework jest jego ścisła inte­ gracja z XML. Przy korzystaniu z wielu aplikacji .NET trudno zdać sobie sprawę, że stosuje się technologię XML; ten fakt znajdzie się niejako „poza sceną". Dotyczy to zarówno serializacji Microsoft ADO.NET DataSet, jak i wywoływania ush.ig sieciowych XML Web, czy wczytywa­ nia ustawień aplikacji w pliku konfiguracyjnym Web.config. W innych przypadkach, aby móc się posługiwać danymi XML konieczne jest bezpośrednie użycie przestrzeni nazw System.Xml. Typowe zadania XML obejmują nie tylko rozbiór gramatyczny pliku XML ale również jego sprawdzenie w oparciu o schemat, zastosowanie transformacji XSL w celu utworzenia nowego dokumentu lub strony HTML, a także inteligentne wyszukiwanie w ścieżce XPath. Przepisy w tym rozdziale obejmują następujące tematy: ,

,



Techniki wczytywania rozbioru gramatycznego i posługiwania się danymi XML (przepisy 5. 1 , 5.2, 5.3 oraz 5.7).



Sposoby przeszukiwania dokumentu XML pod kątem określonych węzłów - według nazwy (przepis 5.4), przestrzeni nazw (przepis 5.5) albo przy użyciu ścieżki XPath (przepis 5.6).



Sposoby sprawdzenia zgodności dokumentu XML ze schematem XML (przepis 5.8).



Sposoby serializacji obiektu do postaci XML (przepis 5.9), tworzenie schematu XML dla klasy (przepis 5 . 1 0) i generowanie kodu źródłowego dla klasy w oparciu o schemat XML (przepis 5 . 1 1 ) .



Transformacja dokumentu XML na inny dokument przy użyciu arkusza stylu XSLT (prze­ pis 5. 1 2).

1 14

C#

Księga przykładów

-

5.1 Przedstawienie struktury dokumentu XML w postaci drzewa Problem Chcesz wyświetlić srrukrurę i zawartość dokumentu XML w aplikacji sysremu Windows.

Rozwiązanie Załaduj dokument XML przy użyciu klasy System.Xmi.Xm/Document. Utwórz merodę wielo­ krotnego użycia, króra będzie wykonywała konwersję pojedynczego węzła Xm/Node w drzewie System. Wind()UJs.Forms. TreeNode i wywołuj ją rekursywnie, rak by możliwe było przejście przez cały dokument.

Omówienie Sysrem .NET Framework zapewnia dostęp do szeregu różnych metod przetwarzania doku­ mentów XML. Użyty w danym przypadku sposób realizacji zależy częściowo od zadania pro­ gramistycznego, które ma być wykonane. Jedną z lepiej wyposażonych klas jest XmlDocument, zapewniająca wewnętrzną (w pamięci) reprezentację dokumentu XML, zgodną ze standardem W3C Document Object Model (DOM). Klasa XmLDocument umożliwia poruszanie się pomię­ dzy węzłami w dowolnym kierunku, ich dołączanie i usuwanie, a także możliwość zmiany struktury w locie. Szczegóły specyfikacji DOM zawiera witryna http://www. w3c.org. Aby wykorzystywać klasę XmlDocument, należy po prostu utworzyć nową instancję klasy i przywołać metodę Loadłącznie z nazwą pliku lub obiektem Stream, TextReader albo XmlReader. Można nawer posłużyć się adresem URL wskazującym na dokument XML. Instancja XmlDo­ cument wstanie zapełniona drzewem elementów (węzłów - nodes) z dokumentu źródłowego. Punktem stanowym dostępu do tych węzłów jest element korzenia (root), dostarczany przy pomocy właściwości XmLDocument. DocumentElement. DocumentEiement jest obiektem XmLE­ Lement, który może zawierać jeden lub więcej zagnieżdżonych obiektów XmLNode, te zaś z kolei mogą zawierać następne obiekty Xm/Node itd. Podstawowym elementem struktury pliku XML jest Xm/Node. Zwykłe węzły XML zawierają elementy, atrybuty, komentarze oraz tekst. Kiedy marny do czynienia z XmlNode lub wyprowadzoną z niego klasą (taką jak as XmLELement lub X m/Attribute}, można użyć następujących właściwości podstawowych: •

ChilJNoJes

jest zbiorem Xm/Nodelist, zawierającym pierwszy poziom zagnieżdżonych

węzłów. •

Name jest



NodeTjtpe zwraca element

nazwą węzła. typu wyliczeniowego System.XmLXmlNodelJpe, oznaczający typ

węzła (element, atrybut, tekst, itd.). •

Value jest zawartością węzła, jeżeli jest to węzeł tekstowy lub węzeł typu CDATA.



Attributes mentu.

dostarcza zbiór obiektów węzła, reprezentujący atrybuty zastosowane do ele­

Rozdział 5: Przetwarzanie XML

115

• /nnerText wydobywa łańcuch z połączoną wartością danego węzła i wszystkich zagnieżdżo­

nych węzłów. • InnerXml wydobywa łańcuch z połączonymi znacznikami

XML dla wszystkich zagnieżdżo­

nych węzłów. • OuterXml wydobywa łańcuch z połączonymi znacznikami

XML dla bieżącego węzła

i wszystkich zagnieżdżonych węzłów. W poniższym przykładzie następuje przejście metodą rekursywną przez każdy element

XmlDocument przy użyciu cechy ChiUNodes. Każdy węzeł jest wyświetlany w konerolce Tree View z opisowym tekstem, który albo go identyfikuje, albo pokazuje jego zawartość.

using System ; using System . Windows . Fo rms ; using System . Xml ; publ i c class XmlTreeDisplay : System . Windows . Fo nns . Form { p rivate System . Windows . Fo rms . Button cmdLoad ; p rivate System . Windows . Fo rms . Label lblFil e ; p rivate System . Windows . Fo rms . TextBox txtXmlFile; p rivate System . Windows . Fo rms . T reeView t reeXml ;

li

( Kod p roj ektanta pominięto . )

p rivate void cmdLoad_Click ( ob j e c t sende r , System . EventArgs e ) {

li

Czyszczenie d rzewa .

t reeXml . Node s . Clea r ( ) ;

li

Ładowanie dokumentu XML

XmlDocument doc = new XmlDocumen t ( ) ; t ry { doc . Load ( txtXml File . Text ) ; } catch ( Except ion e r r ) { Mes sageBox . Show ( e r r . Me s sage ) ; ret u r n ; }

li

Wypeł nianie widoku T reeView .

ConvertXmlNodeToTreeNod e ( doc , t reeXml . Nodes ) ;

li

Rozwinięcie wszyst kich węzłów .

t reeXml . Node s [ 0 ] . ExpandAll ( ) ; } p rivate void Conve rtXmlNodeToTreeNode ( XmlNode xmlNode , T reeNodeCollec t ion t reeNodes ) {

li

Dodanie węzła T reeNode reprezen t u j ącego ten XmlNode .

T reeNode newTreeNode = t reeNodes . Add ( xmlNode . Name ) ;

li

Dostowanie tekstu T reeNode w opa rciu o typ i zawa rtość XmlNode

switch ( xmlNode . NodeType ) { case XmlNodeType . P roces s inginst ruct ion :

1 16 C#

-

Księga przykładów case XmlNodeType . XmlDecla ration : newTreeNode . Text = " < ? " + xmlNode . Name + " " + xmlNode . Value + " ?> " ; b reak ; case XmlNodeType . Element : newTreeNode . Text = " < " + xmlNode . Name + " > " ; b reak ; case XmlNodeType . Att ribut e : newTreeNode . Text = "ATTRIBUTE : " + xmlNode . Name ; b reak ; case XmlNodeType . Text : case XmlNodeType . CDATA : newTreeNode . Text = xmlNode . Value ; b reak ; case XmlNodeType . Comment : newTreeNode . Text = " < ! - - " + xmlNode . Value + " - - > " ; b rea k ; } l i Wywołanie reku rsywne p rocedu ry d l a każdego at rybutu . li ( XmlAtt ribute j est pod klasą XmlNode . ) if ( xmlNode . Att ributes ! = n u l l ) { fo reach ( XmlAtt ribute att ribute in xmlNode . At t ributes ) { Conve rtXmlNodeToTreeNode ( at t ribut e , newTreeNode . Nodes ) ; } } li Wywołan ie reku rsywne p roced u ry dla każdego węzła pod rzędnego . li Zazwyczaj węzeł pod rzędny rep rezentuj e element zagnieżdżony li lub zawa rtośt element u . fo reach (XmlNode childNode i n xmlNode . ChildNode s ) { Conve rtXmlNodeToTreeNode ( childNode , newTreeNode . Nodes ) ; } }

} Poniższy wydruk prostego kodu dla ProductCatałog.xmł jest przykładowym plikiem XML:

J ones and Jones Unique Catalog 2004 2005 - 0 1 - 0 l Gourmet Coffee< l p roductName> The finest beans f rom ra re Chilean plant ation s . 0 . 99

Rozdział 5: Przetwarzanie XML

117

t rue < p roduct id=" le0 2 " > Blue China Tea Pot A t rendy update fo r tea d rinkers . l02 . 99 t rue Rysunek 5-1 przedstawia umieszczenie tego pliku w formularzu Xm/TreeDisplay.

_, � " ->

lane$ ..t Jcwws u;que � 2004

- -> 2005-01-01 - -> - -> 7

ATTRJ!IUT?:: ld .

1001

� -> - Ccłfee - ->



Tht fWlMt t... hOłftr„ o..n pln.łlti:n;.

0.99

_ _,, .... - ->

,.. AnRJIUTE: Id 1002 - -> et. OW Toe Pot - ->

A trflf'ldv .we for tu �

- -> 102.99

.- -. ....

Rysunek S-1

Wyświetlana struktura dokumentu XML

5.2 Wstawianie węzłów do dokumentu XML Problem Chcesz zmodyfikować dokument XML poprzez włączenie nowych danych, albo też chcesz utworzyć w pamięci całkowicie nowy dokument XML.

Rozwiązanie XmlDocument (takiej jak CreateEkment, CreateAttribute, CreateNode itd.), a następnie włącz go do dokumentu przy użyciu odpowied­ niej metody Xm/Node (takiej jak lnsertAfter, lnsertBefore lub AppendChild). Utwórz węzeł przy użyciu odpowiedniej metody

118

C#

-

Księga przykładów

Omówienie Włączenie węzła do dokumentu klasy XmlDocument jest wykonywane w dwóch krokach. Naj­ pierw należy utworzyć węzeł, a następnie włączyć go w odpowiednim położeniu. Opcjonalnie, można następnie wywołać XmlDocument. Save, aby utrwalić zmianę. Aby utworzyć węzeł, należy użyć jednej z metod XmlDocument, które rozpoczynają się od słowa c�ate, zależnie od rodzaju tworzonego węzła. To zapewnia wszystkim węzłom taką samą przestrzeń nazw w całym dokumencie (alternatywnie, można dostarczyć przestrzeń nazw jako dodatkowy argument w postaci łańcucha string). Następnie należy znaleźć odpowiedni, powiązany z nim węzeł i skorzystać z jednej z metod dołączenia go do drzewa. Poniższy przykład demonstruje programistyczną technikę tworzenia nowego dokumentu XML: u s ing System ; u sing System . Xml ; public class Gene rateXml { p rivate static void Main ( ) {

li

Two rzenie nowego , pustego dokumentu

XmlDocument doc = new XmlDocument ( ) ; XmlNode docNode = doc . C reateXmlDec l a rat ion ( " 1 . 9 " , " UTF - 8 " , n u ll ) ; doc . AppendChild ( docNode ) ;

li

Utwo rzenie i wstawienie nowego element u .

XmlNode p roduct sNode = doc . C reateElement ( " p rodu c t s " ) ; doc . AppendChild ( p roduct sNode ) ;

li

Utwo rzenie zagnieżdżonego elementu ( z at rybutem ) .

XmlNode p roductNode = doc . C reateElement ( " p roduct " ) ; XmlAtt ribute p roductAtt ribute = doc . C reateAtt ribute ( " id " ) ; p roductAtt ribute . Value = " 190 1 " ; p roductNode . Att ributes . Append ( p roductAtt ribut e ) ; p roductsNode . AppendChild ( p roductNode ) ;

li li

Two rzenie i dodawanie podelementów dla tego węzła p roduktu (wraz z danymi tekstowymi ) .

XmlNode nameNode = doc . C reateElement ( " p roductName " ) ; nameNode . AppendChil d ( doc . C reateTextNode ( " Gou rmet Coffee" ) ) ; p roductNode . AppendChil d ( nameNode ) ; XmlNode p riceNode = doc . C reateElement ( " p roductPrice " ) ; p riceNode . AppendChild ( doc . C reateTextNode ( " 9 . 99 " ) ) ; p roductNode . AppendChild ( p riceNode ) ;

li

Two rzenie i dodawanie innego węzła p rodukt u .

p roductNode = doc . C reateElement ( " p roduct " ) ; p roductAtt ribute = doc . C reateAtt ribute ( " id" ) ; p roductAtt ribute . Value = " 1902 " ; p roductNode . Att ributes . Append ( p roductAtt ribute ) ; p roduct sNode . AppendChild ( p roductNode ) ;

Rozdział 5: Przetwarzanie XML

119

nameNode = doc . C reateElement ( " p roductName " ) ; nameNode . AppendChild ( doc . C reateTextNode ( " Blue China Tea Pot " ) ) ; p roductNode . AppendChild ( nameNode ) ; p riceNode = doc . C reateElemen t ( " p roductP rice" ) ; p riceNode . AppendChild ( doc . C reateTextNode ( " l02 . 99 " ) ) ; p roductNode . AppendChild ( p riceNode ) ;

li

Zapisanie dokumentu ( w oknie kon sol i , n ie zaś w pliku ) .

doc . Save ( Con sole . Out ) ; Console . Read Line ( ) ; } } Wygenerowany dokument przedstawia się następująco:

c?xml ve rsion= " l . 0 " encoding=" UTF - 8 " ?> cproducts> Gou rmet Coffee < p roductP rice>S . 99 Blue C h i n a Tea Pot c p roductP rice>l02 . 99

5.3 Szybkie dołączanie węzłów do dokumentu XML Problem Chcesz dołączyć węzły do dokumentu XML bez użycia długiego, jawnego kodu.

Rozwiązanie Utwórz pomocniczą funkcję, która zaakceptuje skróconą nazwę i zawartość i która może od razu wygenerować cały element. Alternatywnie możesz użyć metody Xm/Docu�nt. ClimeNode, aby skopiować rozgałęzienia dokumentu Xm/Docummt.

Omówienie Włączenie pojedynczego elementu do Xm/Docummt wymaga szeregu linii kodu, jednak istnieją możliwości jego skrócenia. Jedną z nich jest utworzenie dedykowanej klasy pomocniczej, zawie­ rającej metody wyższego poziomu, pozwalające na dodawanie elementów i atrybutów. Na przy­ kład można utworzyć metodę AddEkmmt, która generuje nowy element, włącza go i dodaje

1 20

C#

-

Księga przykładów

dowolny zawarty w nim tekst - trzy podstawowe operacje wymagane przy włączaniu większości elementów. Niżej przedstawiamy przykład takiej klasy pomocniczej:

u s ing System ; us ing System . Xml ; public class XmlHelper { public static XmlNode AddElement ( s t ring tagName , st ring textContent , XmlNode pa rent ) { XmlNode node = pa rent . Owne rDocument . C reateElement ( t agName ) ; pa rent . AppendChil d ( node ) ; if ( t extContent ! = nul l ) { XmlNode conten t ; content = pa rent . Owne rDocument . C reateTextNode ( textContent ) ; node . AppendChild ( content ) ; } ret u rn node ; } public static XmlNode AddAtt ribute ( st ring att ributeName , st ring textContent , XmlNode pa ren t ) { XmlAtt ribute att ribut e ; att ribute = pa rent . Owne rDocument . C reateAtt ribut e ( att ributeName ) ; att ribute . Value = textContent ; pa rent . Att ributes . Append ( att ribute ) ; ret u rn att ribute ; } } Można teraz spakować wygenerowany przez XML kod z przepisu

5.2, używając prostszej składni:

public class GenerateXml { p rivate static void Main ( ) {

li

Two rzenie pod stawowego dokumen tu .

XmlDocument doc = new XmlDocument ( ) ; XmlNode docNode = doc . C reateXmlDecla ration ( " l . 0 " , " UTF - 8 " , n u l l ) ; doc . AppendChil d ( docNode ) ; XmlNode p roducts = doc . C reateElement ( " p roduct s " ) ; doc . AppendChild ( p roduct s ) ;

li

Dodanie dwóch p rodu któw .

XmlNode p roduct = XmlHelpe r . AddElement ( " p roduct " , n u l l , p roduct s ) ; XmlHelpe r . AddAtt ribute ( " id " , " 100 1 " , p roduct ) ; XmlHelper . AddElemen t ( " p roductName " , " Gou rmet Coffee " , p roduct ) ; XmlHelpe r . AddElement ( " p roductPrice " , " 0 . 99" , p roduct ) ; p roduct = XmlHelpe r . AddElement ( " p roduct " , null , p roduct s ) ; XmlHelpe r . AddAtt ribute ( " id " , " 1992 " , p roduct ) ; XmlHelpe r . AddElement ( " p roductName " , " Blue China Tea Pot " , p roduct ) ; XmlHelpe r . AddElement ( " p roductPrice " , " 102 . 99 " , p roduct ) ;

Rozdział 5: Przetwarzanie XML

1 21

li Zapisanie dokumentu w oknie kon soli . doc . Save ( Con sole . Out ) ; Console . Readline ( ) ; } } Alternatywnie można użyć metod pomocniczych, takich jak

Add.Attribute

łub

AddElement

i uczynić z nich wersje metod w niestandardowej klasie wyprowadwnej z XmlDocument. Innym sposobem uproszczenia tworzenia XML jest powielenie węzłów przy użyciu metody

XmlNode. CloneNode. CloneNode akceptuje boolowski parametr deep. Po dostarczeniu wartości true, metoda CloneNode powieli całą gałąź z zagnieżdżonymi węzłami. A oto przykład konstrukcji nowego węzła produktu poprzez kopiowanie pierwszego.

li ( Dodanie pierwszego węzła p rodukt u . ) li Two rzenie nowego elementu w opa rciu o i s t n iej ący p rodukt . p roduct = p roduct . CloneNode ( t rue ) ;

li Modyfikac j a danych węzła . p roduct . Att ributes [ 0 ] . Value = " 1002 " ; p rod u c t . ChildNodes [ 0 ] . ChildNodes [ 0 ] . Value = " Blue China Tea Pot " ; p roduct . ChildNodes [ l ] . ChildNodes [ 0 ] . Value = " 102 . 99 " ;

li Dodawanie nowego elementu . p rod u c t s . AppendChil d ( p roduct ) ; Należy zauważyć, że w danym przypadku poczyniono pewne założenia dotyczące istniejących węzłów (założono na przykład, że pierwszym następnikiem w węźle pozycji jest zawsze nazwa, a drugim - cena). Gdyby prawdziwość tego założenia nie wstała zagwarantowana, należałoby sprawdzać programowo nazwę węzła.

5.4 Odnajdywanie określonych elementów przy pomocy nazwy Problem Chcesz wydobyć określony węzeł z dokumentu XmlDocument znając jego nazwę, ale nie znając jego położenia.

Rozwiązanie XmlDocument. GetElementsByTagName, która przegląda System.XmLXmlNodelist, zawierającą wszystkie trafienia.

Skorzystaj z metody i zwraca listę

cały dokument

1 22 C #

-

Księga przykładów

Omówienie Klasa XmLDocument dostarcza wygodną metodę GetEl.ementsByTagName, która przeszukuje cały dokument, aby znaleźć węzły o wskazanej nazwie elementu. Metoda ta zwraca wyniki w postaci zbioru obiektów Xm/Node. Poniższy kod demonstruje, jak można użyć metody GetEl.ementsBy­ TagName w celu skalkulowania całkowitej ceny pozycji w katalogu, poprzez wydobycie wszyst­ kich elementów o nazwie productPrice: u s ing System ; u s ing System . Xml ; public class FindNodesByName { p rivate static void Main ( ) {

li Ładowanie dokument u . XmlDocument doc = new XmlDocument ( ) ; doc . Load ( " P roduct Catalog . xml " ) ;

li Odczytanie wszystkich cen . XmlNodeList p rices = doc . GetElementsByTagName ( " p roductPrice " ) ; decimal totalP rice = 0 ; fo reach ( XmlNode p rice i n p rice s ) {

li Pob ranie wewnęt rznego tekstu dla każdego pasuj ącego element u . totalP rice + = Decimal . Pa rse ( p rice . ChildNodes [ 0 ] . Value ) ; } Con sole . WriteLine ( "Total catalog value : " + totalP rice . ToSt ring ( ) ) ; Console . ReadLine ( ) ; } }

Można również przeglądać fragmenty dokumentu XML przy użyciu mecody XmlEl.ement. Get­ El.ementsBy TagName. Przegląda ona wszystkie węzły w kierunku malejącym i przeszukuje je pod kątem zgodności. Aby skorzystać z tej metody, należy najpierw wyszukać obiekt Xm/Node, który odpowiada elementowi, a następnie dopasować ten obiekt do XmlE/.ement. Poniższy przykład pokazuje, jak znaleźć węzeł price pod pierwszym elementem product.

li Odczytanie odnośnika do pierwszego p roduktu . XmlNode p roduct = doc . Get ElementsByTagName ( " p roduct s " ) [ 0 ] ;

li Wyszukanie ceny dla tego p rodukt u . XmlNode p rice

=

( ( XmlElemen t ) p roduct ) . GetElementsByTagName ( " p roductP rice" ) [ 0 ] ;

Console . WriteLine ( " P rice j est " + p rice . I nne rText ) ;

Jeżeli elementy aplikacji zawierają atrybut typu ID, można skorzystać z metody o nazwie GetEl.ementByld, aby wydobyć element, który ma odpowiadającą wartość ID.

Rozdział 5: Przetwarzanie XML

1 23

5.5 Odczytywanie węzłów XML w określonej przestrzeni nazw XML Problem Chcesz wydobyć węzły z określonej przestrzeni nazw przy użyciu Xm/Document.

Rozwiązanie Zastosuj przeciążenie metody Xm/Document. GetElementsBy TagName, wymagające podania jako argumentu nazwy przestrzeni nazw w postaci łańcucha. Nadto, posłuż się gwiazdką (*) dla okre­ ślenia nazwy elementu, jeżeli chcesz znaleźć koincydencję dla wszystkich etykiet.

Omówienie Wiele dokumentów XML zawiera węzły pochodzące z więcej niż jednej przestrzeni nazw. Na przykład dla dokumentu XML, reprezentującego artykuł naukowy, można używać osob­ nego typu oznakowania dla równości matematycznych i diagramów wektorowych, podobnie jak dla dokumentu XML, dotyczącego zamówień sprzedaży, informacja o klientach i zamówie­ niach może być zagregowana z danymi o spedycji. Z kolei dokument reprezentujący transak­ cję business-to-business dwóch przedsiębiorstw może zawierać częściową informację o każdym z nich, zapisaną w różnych językach znaczników. Podstawowym zadaniem w programowaniu XML jest wydobycie tych elementów, które należą do określonej przestrzeni nazw. Można tego dokonać dzięki przeciążonej wersji metody Xm/Document. GetElementsBy TagName Xm/Document, która wymaga podania nazwy przestrzeni nazw. Można skorzystać z tej metody w celu odszukiwania etykiet poprzez nazwę lub w celu znalezienia wszystkich etykiet należących do określonej przestrzeni nazw, jeśli jako parametr nazwy (name) etykiety załączy się gwiazdkę (*). Rozważmy przykładowo złożony dokument XML, zawierający w dwóch różnych przestrze­ niach nazw informacje o zamówieniach i klientach (http://mycompany/OrderML i http://mycom­

pany/ClientML). Sally Se rgeyeva

1 24

C#

-

Księga przykładów

Niżej podajemy prostą aplikację konsolową, która wybiera wszystkie etykiety w przestrzeni nazw http:llmycompanylOrMrML: using System ; using System . Xml ; public class SelectNodesByNamespace { p rivate static void Main ( ) { li Ładowanie dokumentu . XmlDocument doc = new XmlDocument ( ) ; doc . Load ( " O rde r . xml " ) ; li Odczytanie wszystkich etykiet zamówień . XmlNodeList mat ches = doc . GetElementsByTagName ( " * " , " http : ll mycompany l O rderML " ) ; li Wyświet lenie wszystkich informacj i . Console . WriteLine ( " Element \tAtt ributes " ) ; Consol e . WriteL ine ( " * * * * * * * \t * * * * * * ** * * " ) ; fo reach ( XmlNode node in matches ) { Consol e . W rite ( node . Name + " \ t " ) ; fo reach ( XmlAtt ribute att ribute in node . Att ributes ) { Consol e . W rite ( att ribute . Value + "

");

} Console . WriteLine ( ) ; } Console . ReadLine ( ) ; } }

Dane wyjściowe tego programu są następujące: Element *******

Att ributes **********

o rd : o rder

http : llmycompanylO rderML

ord : o rderitem o rd : o rder! tem

3211

http : ll mycompany l C l ientML

1 155

5.6 Odnajdywanie elementów poprzez przeszukiwanie XPath Problem Chcesz przeszukiwać dokument XML pod kątem węzłów przy użyciu zaawansowanych kryteriów wyszukiwania. Na przykład chcesz przeszukiwać konkretną gałąź dokumentu XML pod kątem węzłów, posiadających pewne atrybuty albo określoną liczbę węzłów zagnieżdżonych.

Rozdział 5: Przetwarzanie XML 1 25

Rozwiązanie Wykonaj wyrażenie XPath przy użyciu metody

SelectNodes

lub

SelectSingleNode

klasy

XmlDocument.

Omówienie Klasa

XmlNode

XPath: SelectNodes i SelectSingleNode. XmlDocument metodę XmlDocument.SelectNodes do przeszukiwania

definiuje dwie metody przeszukiwania

Te metody operują na wszystkich zawartych węzłach następników. Ponieważ pochodzi od

XmlNode,

można wywołać

całego dokumentu. Na przykład rozważmy podany niżej dokument XML, który reprezentuje zamówienie dla dwóch pozycji. Dokument ten zawiera dane tekstowe i numeryczne, zagnieżdżone elementy oraz atrybuty i stanowi dobry przykład do przetestowania prostego wyrażenia XPath.

Rema rkable Office Supplies Elect ronic P rot ractor 42 . 99 Invisible I n k 200 . 25 Podstawowa składnia Xpath używa notacji typu ścieżka. Na przykład ścieżka /Order/ltems/ltem wskazuje element

cltem>, zagnieżdżony wewnątrz elementu , ten zaś z kolei zagnież­ , będącym korzeniem dokumentu. Jest to ścieżka absolutna.

dżony jest w elemencie

W poniższym przykładzie wykorzystano absolutną ścieżkę XPath dla znalezienia nazwy każdej z pozycji zamówienia.

using System ; using System . Xml ; public class XPathSe lectNodes { p rivate static void Main ( ) {

li Ładowanie dokumentu . XmlDocument doc = new XmlDocument ( ) ; doc . Load ( " o rders . xml " ) ;

li Odczytanie nazw dla każdego element u . / I Nie można tego os iągnąć tak p rosto p rzy u życiu metody

1 26

C#

-

Księga przykładów li GetElement sByTagName ( ) , gdyż nazwy elementów są używane li w obiektach Item i Client , tak więc zostaną zwrócone oba rod za j e l i nazw. XmlNodelist nodes = doc . SelectNodes ( " I O rd e rl itemslitemlName " ) ; fo reach ( XmlNode node in nodes ) { Console . Writeline ( node . InnerText ) ; } Console . Readline ( ) ; }

}

Dane wyjściowe tego programu są następujące: Elect ron ic P rot ract o r Invis ible I n k

Ścieżka XPath zapewnia bogatą i silną składnię przeszukiwania, tak więc nie sposób objaś­ nić w tym krótkim przepisie wszystkich wariantów, z jakich można korzystać. W tabeli 5-1 wypunktowano niektóre kluczowe opcje używane w bardziej zaawansowanych wyrażeniach XPath i przedstawiono przykłady, jak będą one działać z dokumentem zamówienia. Aby zapo­ znać się ze szczegółowymi odniesieniami, można zajrzeć do zaleceń W3C XPath pod adresem

http:llwww. w3. orglTR!xpath. Tabela 5-1

Składnia wyrażenia XPath

Wyrażenie Opis I

Rozpoczyna ścieżkę absolucną, która wybiera z węzła korzenia. /Order/ltems/ltem wybiera wszystkie elementy Item, które są następnikami ele­ mentu ltems, a cen jako raki jest następnikiem elementu Order korzenia.

li

Rozpoczyna relatywną ścieżkę, która wybiera węzły w dowolnym miejscu. //ltem/Name wybiera wszystkie elementy Name, które są następnikami elementu Item, niezależnie od miejsca, w którym się pojawiają w dokumencie.

@

Wybiera atrybut węzła. /Order/ @id wybiera atrybut o nazwie id z elementu korzenia Order.

*

Wybiera dowolny element na ścieżce. /Order/* wybiera zarówno węzeł !tems jak i Client, ponieważ obydwa są zawarte w korzeniu elementu Order. Łączy różne ścieżki. /Order/ltems/ltem/NamelOrder/CliendName wybiera węzły Name zastosowane do opisu węzłów Client i Name, które z kolei opisują Item. Wskazuje węzeł bieżący (domyślny). Jeżeli bieżącym węzłem jest Order, to wyrażenie ./ltems odnosi się do powiąza­ nych z nim pozycji dla tego zamówienia. Wskazuje węzeł macierzysty. //Name/ . . wybiera każdy element, który jest macierzysty dla Name, zawierającego elementy Client i Item.

Rozdział 5: Przetwarzanie XML Tabela 5-1

1 27

Składnia wyrażenia XPath

Wyra7.enie Opis Definiuje kryteria wyboru, które mogą testować zawarty węzeł lub wartość atrybutu. /Order[@id= "2004-0 1-30. 1 95496"] wybiera elementy Order ze wskazaną war­ tością atrybutu. /Order/ltems/Item[Price > 50) wybiera produkty o cenie wyższej niż $50. /Order/ltems/Item[Price > 50 and Name="Laser Primer"] wybiera produkty speł­ niające dwa kryteria.

[J

starts-with Funkcja wybierająca elementy w zależności od tekstu, od którego rozpoczynają się te elementy. /Order/ltems/ltem [starts-with(Name, "C")) znajduje wszystkie elementy Item, które mają element Name rozpoczynający się od litery C.

position

Funkcja wyszukuje elementy w oparciu o ich pozycję. /Order/ltems/ltem[position 0=2] wybiera drugi element Item.

count

Funkcja zliczająca elementy. Określa się nazwy elementów następnika, które mają być zliczane lub gwiazdkę (*) dla wszystkich następników. /Order/ltcms/ltem[count(Price) = 1 ) wybiera elementy Item, które mają dokład­ nie jeden zagnieżdżony element Price.

Uwaga Wyrażenia XPath oraz wszystkie nazwy elementów i atrybutów używanych wewnątrz nich zawsze rozróżniają wielkości liter, ponieważ sam język XML również jest w ten sposób uwarunkowany.

5. 7 Odczyt i zapis XML bez ładowania do pamięci całego dokumentu Problem Chcesz wczytać XML ze strumienia lub odwrotnie - zapisać w strumieniu. Jednakże wolałbyś kolejno przetwarzać informacje poszczególnych węzłów, niż ładować je wszystkie do pamięci przy pomocy XmlDocument.

Rozwiązanie Aby zapisać X.ML, utwórz XmlTextWriter opakowujący strumień i skorzystaj z metod Write (takich jak WriteStartElement i WriteEndElement). Aby odczytać X.ML, należy utworzyć XmlTextReader, który opakowuje strumień i przywołuje procedurę czytania Read, umożliwia­ jącą poruszanie się od węzła do węzła.

1 28

C#

-

Księga przykładów

Omówienie Klasy XmlTextWriter i XmlTextReader wczytują i zapisują XML bezpośrednio ze strumienia węzeł po węźle. Te klasy nie zapewniają wprawdzie takich cech jak XmlDocument, umożliwiających nawigację i manipulowanie dokumentem XML, ale mają wpływ na zwiększenie wydajności pracy i zostawiają mniej śladów w pamięci - dotyczy to w szczególności bardzo rozbudowanych dokumentów XML. Aby zapisać XML w dowolnym strumieniu, można użyć dostosowanego do tego narzędzia Xml­ TextWriter dostarczającego metod Write, które zapisują węzeł po węźle. Są to następujące metody: •

WriteStartDocument - zapisuje prolog dokumentu oraz WriteEndDocument - zamyka otwarte elementy przy końcu dokumentu.



WriteStartEkment zapisuje etykietę otwarcia dla wskazanego elementu. Można w tym momencie dodać więcej elementów zagnieżdżonych w nim lub wywołać WriteEndElement w celu zapisania zamykajacej etykiety.



-

WriteElementString

-

zapisuje cały element z etykietami otwierającą i zamykającą oraz

zawartość tekstową. •

WriteAttributeString- zapisuje cały atrybut dla najbliższego otwartego elementu, wraz z jego nazwą i wartością.

Stosowanie tych metod wymaga zazwyczaj mniejszej ilości kodu, niż ręczne utworzenie XmlDo­ cument, jak to pokazano w przepisach 5.2 i 5.3. Aby wczytać dokument XML, można skorzystać z metody Read klasy Xm/TextReader. Ta metoda powoduje wczytanie następnego węzła i zwraca wartość true. Jeżeli nie można zna-

\� vi\�t� -wętlb-w, �tbt()n� 'L()�\�)t 'WXt\()�t Ja\se. \1\.()1.m. wy\)\en.t \n\()nw�c\t () \)\rn,C'jm

węźle dzięki właściwościom XmlTextReader, zawierającym jego nazwę, wartość i typ węzła (Name, Value i NodeTjpe). Aby określić, czy dany element posiada atrybuty, należy explicite sprawdzić jego właści­ wość HasAttributes, a następnie skorzystać z metody GetAttribute do wyszukiwania atrybutów na podstawie nazwy i numeru indeksu. Klasa XmlTextReader ma dostęp tylko do jednego węzła w danym momencie i nie może się cofać ani wykonać skoku do arbitralnie wyznaczonego węzła, co daje znacznie mniejszą elastyczność, nii klasa XmlDocument. Poniższa aplikacja konsoli zapisuje i wczytuje prosty dokument XML przy użyciu klas stru­ mieni XmlTextWriter i XmlTextReader. Jest to ten sam dokument XML, który został utworzony w przepisach 5.2 oraz 5.3 przy wykorzystaniu klasy XmlDocument. u s ing System ; u s ing System . Xml ; u s ing System . IO ; u s ing System . Text ; public class ReadWriteXml { p rivate static void Main ( ) {

li Utwo rzenie pliku . FileSt ream fs = new FileSt ream ( " p roduct s . xml " , FileMode . C reate ) ; XmlTextWriter w = new XmlTextWrite r ( f s , E ncoding . UTFS ) ;

li Rozpoczęcie dokument u .

Rozdział 5: Przetwarzanie XML w . WriteSta rtDocument { ) ; w . Wri teSta rtElement { " p roduct s " ) ;

li Zapisanie p rodukt u . w . WriteSt a rtElement ( " p roduct " ) ; w . W riteAtt ributeSt ring { " id " , " 1001 " ) ; w . WriteElementSt ring { " p roductName " , " Gou rmet Coffee " ) ; w . W riteElementSt ring ( " p roductP rice " , " 0 . 99 " ) ; w . W riteEndElement ( ) ;

li Zapisanie innego p rodukt u . w . WriteSta r t Element ( " p roduct " ) ; w . WriteAt t ributeSt ring ( " id " , " 1002 " ) ; w . W riteElementSt ring ( " p roductName " , "Blue C h ina Tea Pot " ) ; w . W riteElementSt ring ( " p roductP rice " , " 102 . 99 " ) ; w . WriteEndElement ( ) ;

li Zakończenie dokumentu . w . W riteEndElement ( ) ; w . W riteEndDocument ( ) ; w . Fl u s h ( ) ; f s . Close ( ) ; Console . W riteline ( " Document c reated . " + " P ress Enter to read the document . " ) ; Consol e . Readline ( ) ; f s = new FileSt ream ( " p roduct s . xml " , FileMode . Open ) ; XmlTextRead e r r = new XmlTextReade r ( fs ) ;

li Odczytanie ws z y s t kich węzłów . while ( r . Read ( ) ) { if ( r . NodeType == XmlNodeType . Element ) { Console . Writelin e ( ) ; Console . Writeline { " < " + r . Name + " > " ) ; if ( r . HasAtt ributes ) { f o r ( int i = 0 ; i < r . At t ributeCount ; i++ ) { Con sole . Writeline ( "\ tATTRIBUTE : " + r . GetAtt ribute ( i ) ) ; } } } else if ( r . NodeType == XmlNodeType . Text ) { Console . Writeline ( "\ tVALU E : " + r . Value ) ; } } Console . Readline ( ) ; } }

1 29

1 30 C #

-

Księga przykładów

5.8 Sprawdzenie zgodności dokumentu XML ze schematem Problem Chcesz wykonać test poprawności zawartości dokumentu XML, sprawdzając, czy zachodzi zgodność ze schematem XML.

Rozwiązanie Użyj klasy System.XmLXm!Validating&ader. Utwórz instancję, załaduj schemat do zbioru Xm!Validating&ader.Schemas, a następnie posuwaj się po dokumencie węzeł po węźle, wywołu­ jąc metodę Xm!Validating&ader.&ad i wychwytując niezgodności. Aby znaleić wszystkie błędy w dokumencie bez wychwytywania wyjątków, uruchom zdarzenie Va/idationEventHandkr.

Omówienie Schemat XML definiuje zasady, które dokument XML danego typu musi spełniać. Zasady takie określają: •

elementy oraz atrybuty, które mogą pojawić się w dokumencie,



typy danych dla elementów i atrybutów,



strukturę dokumentu wskazującą, które elementy są następnikami innych elementów,



porządek i numer następników, które pojawiają się w dokumencie,



wskazanie, czy elementy mogą być puste, mogą zawierać tekst lub wymagają ustalonych wartości.

Dokumenty schematu XML wykraczają poza ramy niniejszego rozdziału, ale wiele można się nauczyć z prostego przykładu. Przepis ten wykorzystuje katalog produktów, zaprezentowany uprzednio w przepisie 5 . 1 . Na najbardziej podstawowym poziomie definicja schematu XM L [XML Schema Defi­ nition - XSD)] definiuje elementy, które mogą wystąpić w dokumencie XML. Dokumenty XSD same są zapisane w formacie XML, należy więc użyć wstępnie zdefiniowanego elementu (o nazwie ) w dokumencie XSD, aby wskazać każdy element, który jest wymagany w dokumencie docelowym. Atrybut type wskazuje typ danych. Oto przykład nazwy produktu:

A o to przykład ceny produktu:

Podstawowe typy danych schematu są zdefiniowane pod adresem http://www. w3.org/TRI xmlrchema-2. Mapują się one ściśle na typy danych .NET i zawierają typy string, int, long, deci­ mal, foat, date Time, bookan i base64Binary to kilka spośród najczęściej używanych. -

Rozdział 5: Przetwarzanie XML

1 31

Zarówno productName, jak i productPrice są typami prostymi (simpl.e types), ponieważ zawie­ rają wyłącznie dane znakowe. Elementy zawierające dane zagnieżdżone noszą nazwę typów zło­ żonych (compfex types). Można je zagnieżdżać korzystając z etykiet , jeżeli konieczne jest zachowanie kolejności lub użyć etykiety , jeśli to nie jest konieczne. Niżej pokazano, jak można modelować element w katalogu produktów. Należy zauważyć, że atrybuty są zawsze zadeklarowane po elementach i nie są zgrupowane przy pomocy etykiet lub , ponieważ ich kolejność nie jest istotna.

Domyślnie przyjmuje się, że wyłistowany element może wystąpić w dokumencie tylko raz. Można skonfigurować to zachowanie poprzez wyspecyfikowanie atrybutów maxOccurs i minOccurs. Niżej podajemy przykład, który wnożłiwia umieszczenie w katalogu nielimitowanej liczby produktów:

A oto kompletny schemat XML dla katalogu produktów: < ! - - Definiowanie p roduktu złożonego typu . - - > cxsd : element name= " inStock" type= " x s d : boolean "/> < ! - - Dokument musi być zgodny z następuj ącą s t ruktu rą : zaczynać s ię od elementu p roductCatalog , w któ rym zagnieżdżone są inne elementy . - - >

1 32

C#

-

Księga przykładów < l xsd : sequence>

< l xsd : complexType>

Klasa Xm!Validating&ader wymusza wszystkie wymienione wyżej zasady schematu, dzięki czemu dokument staje się poprawny (valid), a także sprawdza, czy dokument X.ML ma prawidłową formę (wellformed). Oznacza ro, że nie zawiera on nidcgalnych znaków, wszystkie tagi otwarcia posia­ dają odpowiadające tagi zamykające ird. W edu sprawdzenia dokument jest wczytywany węzeł po węźle przy pomocy wywołanej metody Xm!Validating&ader. &ad. Jeżeli :u>SCanie napotkany błąd, meroda XmlValidating&ader zgłosi zdarzenie ValidationEventHandlerz informacją o błędzie. Można obsłużyć ro zdarzenie nie wstrzymując wyszukiwania dalszych błędów. W wypadku ponie­ chania obsługi informacji o błędzie, przy napotkaniu pierwszego błędu wstanie uruchomiony wyjątek Xm/Exception i przerwananie wstanie przerwane. Jeżeli chodzi rylko o sprawdzenie, czy dokument ma poprawną formę, można skorzystać z Xm!Validating&ader bez schematu. Następny przykład pokazuje klasę właściwości, wyświerlającą wszystkie błędy w dokumencie XML w przypadku wywołania merody ValidateXml. Błędy zostaną wyświetlone w oknie kon­ soli, a zwrócona ko11cowa zmienna boołowska wskaże poprawność lub brak poprawności całego sprawdzenia poprawności. us ing System ; using System . Xml ; using System . Xml . Schema ; public class ConsoleValidator { li Ustawienie wa rtości t ru e , j eś l i wystąpił c hoć j eden błąd . p rivate bool failed ; public bool Failed { get { return failed ; } } public bool ValidateXml ( st ring xml Filename , st ring sc hema Filename ) { li Utwo rzenie walidatora . XmlTextReader r = new XmlTextRead e r ( xmlFilename ) ; XmlVal idat ingReade r val idato r = new XmlValidatingReade r ( r ) ; val idato r . Validat ionType = Validat ionType . Schema ; li Załadowanie pliku schematu . XmlSchemaCollection schemas = new XmlSchemaCollection ( ) ; s chema s . Ad d ( null , schemaFilename ) ; val idat o r . Schema s . Add ( schemas ) ; li Ustawienie obsługi zda rzeń we ryfikacj i . validat o r . Validat ionEventHandler += new Validat ionEventHandle r ( Val idationEventHandle r ) ; failed = false ;

Rozdział 5: Przetwarzanie XML

1 33

t ry {

li Odczytanie wszystkich danych XML . while ( validato r . Read ( ) ) {} catch ( XmlException e r r ) { }

li To nastąpi , j eśli dokument XML zawiera niedozwolone znaki li lub niewłaściwie zagnieżdżone a l bo n iezamknięte etykiety . Console . WriteLine ( "A c ritical XML e r ror has occ u r red . " ) ; Console . W riteLine ( e rr . Me s sage ) ; failed = t ru e ; } finally { validato r . Close( ) ; } ret u rn ! fai led ; } p rivate void ValidationEventHandl e r ( obj ect sende r , ValidationEventArg s a rg s ) { failed = t rue ;

li Wyświet lenie błęd u . Consol e . WriteLi n e ( "Validation e r ro r : " + a rg s . Mes sage ) ; Consol e . WriteLine ( ) ; } }

A oto jak powinno się wykorzystać klasę do sprawdzenia poprawności katalogu produktu: using System ; public c l a s s Val idateXml { p rivate static void Main ( ) { ConsoleValidat o r con soleValidato r = new ConsoleValidat o r ( ) ; Con sole . WriteLine ( "Validating P roductCatalog . xml . " ) ; bool success = consoleValidat o r . Val idateXml ( " P roductCatalog . xml " , " P roductCatalog . xs d " ) ; if ( ! s u c ce s s ) { Console . WriteLine ( "Va l idation failed . " ) ; } else { Console . WriteLine ( " Validation s u cceeded . " ) ; } Console . Read Line ( ) ; } }

Jeśli dokument jest poprawny, nie pojawi się żaden komunikat i zmienna success zostanie usta­ wiona na true. Należy jednak zwrócić uwagę, co się stanie w przypadku dokumentu łamiącego zasady schematu - takiego jak pokazany plik ProductCatalog_Jnvalid.xml:

1 34 C #

-

Księga przykładów

Acme Fall 2003 Catalog Jan l, 2004 Magic Ring $342 . 10 t rue Flying Ca rpet < p roductPrice>982 . 99 Yes

Przy próbie testu poprawności tego dokumentu, zmienna success wstanie ustawiona na .folse, a dane wyjściowe będą wskazywać każdy błąd: Validating P roductCatalog_ Inval id . xml . Validation e r ro r : The ' expi ryDate ' element has an invalid value according to its data type . [ path information t runcated ] Validation e r ro r : The ' p roduct P rice ' element has an inval id value acco rd i ng to its data type . [ path information t runcated ] Validation e r ro r : The ' inSto c k ' element has an invalid value according to its data type . [ path info rmat ion t ru n cated ] Validation failed .

Ostatecznie do testu poprawności dokumentu XML i jego dalszego przetwarzania można wyko­ rzystać XmlValidatingReader, żeby go zeskanować podczas wczytywania do pamięci wewnętrznej XmlDocument. Ten przykład działa następująco: XmlDocument doc = new XmlDocument ( ) ; XmlTextReader r = new XmlTextReade r ( " P roductCatalog . xml " ) ; XmlVal idat ingReader validator = new XmlValidatingReade r ( r ) ;

li

Załadowanie schematu .

validato r . ValidationType = Val idationType . Schema ; XmlSchemaCollection s chemas = new XmlSchemaCollection ( ) ; schemas . Add ( nul l , " P roductCatalog . xs d " ) ; val idato r . Schemas . Add ( s chemas ) ;

li li li

Załadowanie i we ryfikacj a dokumentu w tym samym czasie . Nie obsługuj e zda rzenia Val idat ionEventHandl e r . Zamiast tego błędy zostaną od rzucone j ako XmlSchemaExcept ion .

t ry { doc . Load ( val idato r ) ;

li

(We ryfikacj a zakoń czona powodzeniem . )

Rozdział 5: Przetwarzanie XML

1 35

} catch ( XmlSchemaException e r r ) {

li (We ryfikacj a się nie powiodła . ) }

5.9 Serializacja XML przy pomocy obiektów niestandardowych Problem Chc.esz wykorzystać XML jako format seńalizacji, jednakże nie chc.esz przerwarzać XML �pośrednio w twoim kodzie - wolisz dokonać interakcji z danymi przy pomocy niestandardowych obiektów.

Rozwiązanie Użyj klasy System.XmlSeria/ization.Xm/Seria/iz,er, aby przenieść dane z twojego obiektu do X.ML i odwrotnie. Możesz również zaznaczyć kod swojej klasy przy pomocy atrybutów, aby dostoso­ wać go do postaci X.ML.

Omówienie Klasa Xm/Serializer umożliwia konwersję obiektów na dane XML i vice versa. Ten proces jest używany standardowo przez usługi sieci Web i zapewnia możliwy do dostosowania mechanizm serializacji, nie wymagający ani jednej linii kodu użytkownika. Klasa Xm/Serializer jest ponadto dostatecznie inteligentna, by poprawnie utworzyć tablicę po znalezieniu zagnieżdżonych elemen­ tów. Jedyne wymagania przy stosowaniu serializacji Xm/Seria/izer są następujące: • • •

Xm/Seria/izer serializuje tylko właściwości i zmienne public. Klasy, które mają być serializowane, muszą zawierać domyślnego konstruktora � argumentów.

Xm/Seria/izer używa tego konstruktora przy tworzeniu nowego obiektu podczas deserializacji. Wszystkie właściwości klasy muszą być dostępne zarówno do odczytu, jak i do zapisu, ponieważ Xm/Serializer używa modułu dostępu get właściwości do wyszukiwania informacji i modułu dostępu set do odnawiania danych po deserializacji.

Uwaga Można zapamiętywać swoje obiekty w formacie XML przy użyciu serializacji N ET i programu formatującego (formatera) System. Runtime.Serialization.Formatters. Soap.SoapFormatter. W tym wypadku wystarczy po prostu dysponować klasą zdolną do serializacji - nie trzeba zapewniać domyślnego konstruktora, ani sprawdzać, czy wszystkie właściwości posiadają zdolność zapisu. Jednakże w takim wypadku traci się kontrolę nad formatem seńalizowanego XML. .

1 36 C #

-

Księga przykładów

Aby korzystać z serializacji XML, należy przedtem oznakować wszystkie obiekty danych atry­ butami, wskazującymi pożądane mapowanie XML. Atrybuty te pochodzą z przestrzeni nazw System.XmLSerialization i są następujące: •

XmlRoot Określa nazwę elementu korzenia pliku XML. Domyślnie XmlSerializer użyje



XmlElement Wskazuje nazwę elemenm do wykorzystania dla właściwości lub zmiennej public. Domyślnie XmlSerializer użyje nazwy właściwości lub zmiennej public. XmlAttribute Wskazuje, że właściwość lub zmienna public powinny być serializowane jako



nazwy klasy. Ten atrybut może być zastosowany do deklaracji klasy.

atrybuty, a nie jako elementy, i określa nazwę atrybutu. • •

XmlEnum Konfiguruje tekst, który powinien być wykorzystany przy serializacji wyliczanych wartości. Jeżeli nie użyje się XmlEnum, zostanie użyta nazwa stałej typu wyliczeniowego. Xmllgnore Wskazuje, że właściwość lub zmienna public nie powinny być serializowane.

Rozważmy katalog produktów pokazany uprzednio w przepisie 5. 1 . Można przedstawić cen dokument XML przy użyciu obiektów ProductCatalog i Product. Niżej podajemy kod klasy, który można zastosować: u sing System ; u s ing System . Xml . Se rial ization ; [ XmlRoot ( " p roductCatalog " ) J public class P rodu ctCatalog { [ XmlElement ( " catalogName " ) ) public st ring CatalogName ;

li Wyko rzystanie typu danych date ( igno rowanie części czasowej li w se rializowanym XML ) . [ XmlElemen t ( ElementName= " expi ryDate " , DataType= " date " ) J public DateTime Expi ryDate ;

li Skonfigu rowanie nazwy etykiety p rzechowuj ącej wszystkie p rodukty li o raz samą nazwę etykiety p rodukt u . [ XmlA r ra y ( " p roduct s " ) ) [ XmlA r rayitem ( " p roduct " ) ) public P roduct [ ) P roduct s ; public P roductCatalog ( ) {

li Domyślny konst rukto r dese rial izacj i . } public P roductCatalog ( st ring catalogName , DateTime expi ryDate ) { t�is . CatalogName th i s . Expi ryDate

=

=

catalogName ; expi ryDate ;

} } public class P roduct { [ XmlElement ( " p roductName " ) J public st ring P roductName ; [ XmlElement ( " p roductPrice " ) )

Rozdział 5 : Przetwarzanie XML

1 37

public decimal P roductPrice ; [ XmlElement ( " inStock " ) J public bool InStoc k ; [ XmlAtt ributeAt t ribute ( Att ributeName= " id " , DataType=" intege r" ) ] public st ring Id ; public P roduct ( ) {

li

Domyślny kon s t ruktor s e rializacj i .

} public P roduct ( st ring p roductName , decimal p roductP rice ) { this . P roduct Name

=

p roductName ;

this . P roductP rice = p roductP rice ; } }

Można zauważyć, że te klasy wykorzystują atrybuty serializacji XML do nadania powtórnie nazwy elementowi (przy użyciu formy Pascala dla nazw pola klasy i formy camel dla nazw etykiet XML) i wskazują typy danych, które nie są oczywiste. Określają także, w jaki sposób elementy zostaną zagnieżdżone w katalogu . Przy użyciu tych klas użytkownika i obiektu Xm/Seria/izer można dokonywać translacji XML na obiekty i vice versa. Poniższy kod może być przydatny przy tworzeniu nowego obiektu ProductCatabJgt, serializacji wyników do postaci dokumentu XML, powtórnej deserializacji dokumentu do obiektu, a następnie wyświetlenia dokumentu XML. using System ; using System . Xml ; using System . Xml . Serialization ; using System . IO ; public class Se rial izeXml { p rivate s tatic void Main ( ) {

li

Two rzenie katalogu p rodukt u .

P roductCatalog catalog = new P roductCatalog ( " New Catalog " , DateTime . Now . AddYea rs ( l ) ) ; P roduct [ ] p roduct s = new P roduct [ 2 ] ; p roduct s [ e ] = new P roduct ( " P roduct 1 " , 42 . 99m ) ; p roduct s [ l ] = new P roduct ( 11 P roduct 2 11 , 282 . 99m ) ; catalog . P rod u c t s = p roduct s ;

li

Se rial izac j a zamówień w pliku .

XmlSe rial ize r s e rializer = new XmlSe rial ize r ( typeof ( P roductCatalog ) ) ; FileSt ream f s = new FileSt ream ( " P roductCatalog . xml " , FileMode . C reate ) ; se rialize r . Se ria l iz e ( f s , catalog ) ; f s . Close ( ) ; catalog = n u l l ;

li

Dese rial izac j a zamówień z pliku .

fs = new FileSt ream ( " P roductCatalog . xml " , FileMod e . Open ) ; catalog = ( P roductCatalog ) se rialize r . Deserialize ( fs ) ;

1 38

C#

-

Księga przykładów li Se rializacja zamówień do okna konsoli . se rial ize r . Se rial ize ( Console . Out , catalog ) ; Console . Readline ( ) ; }

}

5.1 O Tworzenie schematu dla klasy .NET Problem Chcesz utworzyć schemat XML w oparciu o jedną lub więcej klas C#. Umożliwi to testowanie poprawności dokumentów XML przed ich desarializacją przy pomocy XmlSerializer.

Rozwiązanie Użyj właściwości utiłity XM L Schema Definition Tool (xsd.exe) z linii poleceń, zawartej w .NET Framework. Podaj nazwę swojej asemblacj i jako argumentu linii poleceń i dodaj para­ metr /t: [TypeName] , aby wskazać typy, które chcesz poddać konwersji.

Omówienie Przepis 5.9 demonstruje użycie Xm/Serializer do serializacji obiektów .NET do postaci XML i do deserializacji obiektów XML w obiekty .NET. Jeżeli XML ma być zastosowane jako narzę­ dzie do interakcji z innymi aplikacjami, procesami businessowymi lub aplikacjami nie należą­ cymi do środowiska Framework, potrzebny jest prosty sposób sprawdzenia poprawności XML przed jego ewentualną deserializacją. Trzeba będzie także zdefiniować schemat XML, który z kolei zdefiniuje strukturę i typy danych używane w wykorzystywanym formacie XML, tak by inne aplikacje mogły z nim współpracować. Szybkim rozwiązaniem jest wygenerowanie sche­ matu XML przy użyciu narzędzia xsd.exe. Narzędzie utility xsd.exe jest włączone do systemu .NET Framework. Jeżeli zainstalowano Microsoft Visual Studio .NET, będzie się ono znajdować w katalogu C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin. Program xsd.exe może generować dokumenty schematu ze kompilowanych asemblacji. Wystarczy podać nazwę pliku i wskazać klasę repre­ zentującą dokument XML przy pomocy parametru I t: [TypeName] . Rozważmy na przykład klasę ProductCatawg i klasę Product z przepisu 5.9. Można utworzyć schemat XML dla katalogu produktów przy pomocy następującej linii poleceń: xsd RecipeS - 09 . exe lt : P roductCatalog

Trzeba wyspecyfikować klasę ProductCatawgz linii poleceń, ponieważ ta klasa reprezentuje aktu­ alny dokument XML. Schemat utworzony w tym wypadku będzie reprezentował kompletny katalog produktu z zawartymi w nim pozycjami produktów. Zostanie mu nadana domyślna nazwa pliku schemaO.xsd. Teraz można skorzystać z procedury Xm/ValidatingReader; omówio­ nej w przepisie 5.8, aby się przekonać, czy dokument XML może być pomyślnie sprawdzony przy pomocy tego schematu.

Rozdział 5: Przetwarzanie XML

1 39

5.1 1 Generowanie klasy ze schematu Problem Chcesz utworzyć jedną lub więcej klas języka C# w oparciu o schemat XML, a następnie doku­ ment XML w odpowiednim formacie, przy użyciu tych obiektów i serializatora Xm/Serializer.

Rozwiązanie Użyj narzędzia xsd.exe, należącego do .NET Framework. Określ nazwę swego pliku schematu jako argument linii poleceń i dodaj parametr le dla wskazania, że chcesz wygenerować kod klasy.

Omówienie W przepisie 5. 1 0 przedstawiono narzędzie xsd.exe, którego można użyć do wygenerowania schematu w oparciu o definicję klasy. Operacja odwrotna - generowanie kodu źródłowego języka C# w oparciu o dokument schematu XML również jest możliwa. Używa się jej przede wszystkim wtedy, gdy trzeba zapisać pewien określony format dokumentu XML, bez potrzeby ręcznego zapisywania poszczególnych węzłów przy pomocy klas XmiDocument lub XmlTextWri­ ter. Zamiast tego można wygenerować zestaw obiektów .NET. Następnie można je serializować do wymaganej reprezentacji XML przy użyciu XmlSerializer, jak to opisano w przepisie 5.9. Aby wygenerować kod źródłowy ze schematu, należy dostarczyć nazwę pliku dokumentu schematu i dodać parametr le dla wskazania, że mają być wygenerowane wymagane klasy. Na przykład dla schematu przedstawionego w przepisie 5.8 można wygenerować kod języka C# za pomocą następującej linii poleceń: -

xsd P roductCatalog . xsd / c

Spowoduje to wygenerowanie pojedynczego pliku (ProductCatalog.cs), który zawiera dwie klasy:

product i productCatalog. Te klasy są podobne do klas utworzonych w przepisie 5.9, z wyjątkiem tego, że dla nazw pól klasy zachodzi ścisła koincydencja z dokumentem XML

.

5.1 2 Wykonywanie transformacji XSL Problem Chcesz przetransformować dokument XML w inny dokument przy użyciu arkusza stylów XSLT.

Rozwiązanie Użyj klasy System.Xml.XsLXslTransform. Załaduj arkusz stylów XSLT przy użyciu metody XslTransform.Load, po czym wygeneruj dokument wyjściowy przy użyciu metody Transform, podając dokument źródłowy jako argument.

1 40

C# - Księga przykładów

Omówienie XSLT (lub XSL transforms) jest językiem utworzonym w opaci u o XML, służącym do transfor­ macji dokumentu XML na inny dokument. XSLT może być używany do utworzenia nowego dokumentu XML, zawierającego te same dane, jednakże zaaranżowane w inną strukturę, albo do wyselekcjonowania z dokumentu podzbioru danych, albo utworzenia dokumentu struk­ turalnego innego typu. Najczęściej XSLT jest używany do formatowania dokumentów XML do postaci strony HTML. XSLT jest bogatym językiem, więc transformacje XSL są skomplikowane i wykraczają poza ramy niniejszego podręcznika. Można jednak nauczyć się korzystać z prostych dokumentów XSLT na podstawie poniższego przykładu. Ten przepis przeształca dokument orders.xmł (z prze­ pisu 5.6) na dokument HTML zawierający tabelę, a następnie wyświetla uzyskane wyniki. Aby dokonać tej transformacji, należy przygotować następujący arkusz stylów XSLT: O rder for IDNamePrice

Zasadniczo każdy arkusz srylów XSL składa się z zestawu wzorników (template). Każdy wzornik pokrywa się z jakimś zestawem elementów w dokumencie źródłowym i opisuje zależność, jaką te elementy muszą spełnić względem dokumentu wynikowego. Aby zachodziła taka koincyden­ cja, dokument XSLT wykorzystuje wyrażenia Xpath, przedstawione w przepisie 5.6. Arkusz stylów orders.xslt zawiera dwa elemenry wzorcowe (następniki korzenia stylesheet). Pierwszy wzornik pokrywa się z elementem korzenia Order. Kiedy procesor XSLT znajdzie element Order, utworzy erykiery, niezbędne do utworzenia tabeli HTML z odpowiednimi nagłówkami kolumn i włączy do niej określone dane o kliencie przy użyciu polecenia value--of, które zwraca wynik tekstowy wyrażenia XPath. W tym wypadku wyrażenia XPath (Cłient/@id i Cłient/Name) pokrywają się z atrybutem id oraz elementem Name. Następnie używane jest polecenie apply-templates do rozwinięcia i przetwarzania elementów w każdej pozycj i (Item). Jest to konieczne, ponieważ mogą to być elementy użyte wielokrotnie.

Rozdział 5: Przetwarzanie XML

1 41

Każdy element Item jest wyszukiwany przy użyciu wyrażenia ltems/Item ściciki XPath. Węzeł korzenia Order nie jest wyspecyfikowany, ponieważ jest to węzeł bieżący. Na koniec początkowy wzornik zapisuje etykiety, niezbędne do zakończenia dokumentu HTML. Jeśli ta transformacja wstanie wykonana na przykładowym pliku orders.xml, przedstawio­ nym w przepisie 5.6, działanie powinno dać w wyniku następujący dokument HTML: O rder ROS - 930252034 f o r Rema rkable Off ice Supplies ID Name Price l00 1 Elect ron ic P rot ractor 42 . 99 l002 I nvisible I n k 200 . 25

Aby zastosować arkusz stylów XSLT w środowisku .NET, nalciy użyć klasy Xs!Transform. Poniż­ szy kod zawiera aplikację Windows, która przetwarza, a następnie wyświetla przetransformo­ wany plik w oknie przeglądarki internetowej. W tym przykładzie kod wykorzystuje przeciążoną wersję metody Transform która zapamiętuje dokument wynikowy bezpośrednio na dysku, cho­ ciaż można równie-i otrzymać go w postaci strumienia i przetwarzać wewnątrz aplikacji. ,

using System ; u s ing System . Windows . Fo rm s ; using System . Xml . Xs l ; public c l a s s Tran sfo rmXml : System . Windows . Fo rms . Form { p rivate AxSHDocVw . AxWebB rowser webB rows e r ; l i ( Kod p roj ektanta pominięto . ) p rivate void T ra n s f o rmXml_Load ( ob j ect sende r , System . EventArgs e ) { XslT rans f o rm t ra n s f o rm = new XslTra n s f o rm ( ) ; li Ładowanie a rkusza s tylów XSL . t ran s f o rm . Load ( " o rders . xsl t " ) ; // P rzekształcenie o rders . xml w o rde rs . html p rzy użyciu o rders . xslt .

1 42

C#

-

Księga przykładów t ransform . Tran s f o rm ( " o rd e r s . xml " , " o rders . html " , null ) ; obj ect va r = null ; webB rowse r . Navigate ( " file : // / " + Application . Sta rtupPath + @ " \ o rders . html " , ref va r , ref va r , ref va r , ref v a r ) ; }

}

Uwaga System .NET Framework nie zawiera kontrolek pozwalających na wyświetle­ nie zawartości HTML. Taka funkcjonalność jest dostępna dzięki interopercyjności COM, przy korzystaniu z kontrolki przeglądarki internetowej ActiveX, dołączonej do Microsoft Internet Explorer i systemu operacyjnego Windows. To okno może pokazywać lokalne lub zdalne pliki HTML. Pozwala na użycie JavaScript, VBScript, a także wszystkich wty­ czek przeglądarki Internet Explorer (w przepisie 1 1 .4 podano szczegółową informacją na temat dodawania kontrolki przeglądarki do projektu).

Aplikacja przedstawiona jest na rysunku 5-2.

�i"J

fhc XML

�@!FJ

Tr..nsform Output

Order ROS-9302520� for Remarkable Offic e Supplies ID

Name

Price

1001 FJectronic Prolractor 42.99 1002 Invisible Ink.

200.25

Rysunek 5-2 Dane wyjściowe na arkuszu stylów dla orders.xml.

6 Formularze Windows System Microsoft .NET Framework zawiera bogaty zestaw klas do tworzenia tradycyjnych aplikacji Windows (Windows-based) w przestrzeni nazw System. Windows.Forms. Obejmuje on podstawowe elementy, takie jak klasy TextBox, Button i MainMenu, aż po specjalizowane kontrolki jak TreeView, Linklabel i Notifjlcon. Ponadto zawiera również wszystkie narzędzia potrzebne do zarządzania aplikacjami MDI (Multiple Document Interface), integracji pomocy kontekstowej, a nawet do tworzenia wielojęzycznych interfejsów użytkownika. Wszystkie te opcje nie wymagają zagłębiania się w zawiłości systemu Win32 API. Niniejszy rozdział obej­ muje następujące tematy: •

Uzyskanie maksimum efektywności przy użyciu kontrolek, w tym: dodawanie ich podczas wykonania programu (przepis 6. 1 ), wiązanie z dowolnymi danymi (przepis 6.2), a następ­ nie przetwarzanie w ogólny sposób (przepis 6.3).



Praca z formularzam i: śledzenie w aplikacjach (przepis 6.4), posługiwanie się MDI (przepis 6.5), wreszcie zapamiętywanie informacji o ich wielkości i lokalizacji (przepis 6.6), a także tworzenie formularzy wielojęzycznych (przepis 6. 1 3) i pozbawione obramowania (przepisy 6. 1 4 i 6. 1 5).



Wskazówki dotyczące pracy z typowymi kontrolkami, takimi jak ListBox (przepis 6.7), TextBox (przepis 6.8), ComboBox (przepis 6.9), ListView (przepis 6. 1 0) i klasy Menu (prze­ pisy 6. 1 1 i 6. 1 2).



Tworzenie animowanych ikon paska systemowego (przepis 6. 1 6) .



Omówienia użycia wielu typów kontrolek, w tym: testu poprawności (przepis 6. 1 7), prze­ ciągania i upuszczan ia (przepis 6. 1 8), pomocy kontekstowej (przepis 6. 1 9) oraz stylów kon­ trolek Windows XP (przepis 6.20).

Uwaga Większość przepisów w tym rozdziale używa klas sterujacych, które są zdefiniowane w przestrzeni nazw System. Windows.Forms. Przy wprowadzaniu tych klas pełna przestrzeń nazw nie jest podawana, zatem przyjmuje się korzystanie z przestrzeni nazw Systems. Windows.Forms.

1 44 C# - Księga przykładów

6.1 Programowe dodawanie kontrolki Problem Chcesz dodać konrrolkę do formularza podczas działania programu, a nie w czasie projekto­ wania.

Rozwiązanie Utwórz instancję odpowiedniej klasy kontrolki, a następnie dołącz obiekt kontrolki do formu­ larza lub do zasobnika kontrolek przy użyciu metody Add klasy

ControlColkction.

Omówienie W aplikacji .NET Windows nie ma w istocie różnicy między tworzeniem kontrolki podczas projektowania i podczas wykonywania programu. Jeżeli kontrolka zostanie utworzona w fazie programowania (przy użyciu takiego narzędzia jak Microsoft Visual Studio .NET), niezbędny kod dodawany jest do klasy formularza projektanta, zazwyczaj w specjalnej metodzie o nazwie

lnitializeComponent.

Można jednak użyć tego samego kodu w swojej aplikacji do utworzenia

kontrolki na bieżąco. Wystarczy wykonać następujące kroki:

1. Utworzyć instancję odpowiedniej klasy kontrolki.

2.

Skonfigurować odpowiednio właściwości kontrolek {w szczególności rozmiar i koordynaty pozycji).

3. Dodać kontrolkę do formularza albo do innego zasobnika. 4. Dodatkowo - w celu zarządzania zdarzeniami dla nowej kontrolki - można powiązać ją z istniejącymi metodami. Każda kontrolka zapewnia właściwość

Controls,

która odnosi się do zbioru

ControlColkction,

zawierającego wszystkie podrzędne kontrolki. Aby dodać podrzędną kontrolkę, trzeba przy­

ControlCollection.Add. Poniższy przykład demonstruje realizację dynamicznego check boxes. Do każdej pozycji w tablicy dodawane jest jedno okno. Wszystkie te okna zostają dodane do panelu, który ma właściwość AutoScroll ustawioną na true

wołać metodę

kreowania listy okien typu

i udostępnia podstawową funkcjonalność przeglądania listy.

using System ; us ing System . Windows . Fo rms ; public class DynamicCheckBox : System . Windows . Fo rms . Form {

li ( Kod p roj ektanta pominięto . ) p rivate void DynamicCheckBox_Load ( obj ect sende r , System . EventA rg s e ) {

li Two rzenie tablicy . st ring [ ] foods

=

{ "G rain " , " B read " , " Beans " , " Eggs " ,

" Chicken " . "Mil k " . " F ruit " . "Vegetables " .

Rozdział 6: Formularze Windows

1 45

" Pasta " , " Rice " , " Fi sh " , " Beef " } ; int topPos it ion = 10 ; f o reach ( s t ring food in food s ) {

li Two rzenie nowego pola wybo ru . CheckBox checkBox = new CheckBox ( ) ; checkBox . Left = 1 0 ; checkBox . Top = topPosition ; topPosit ion += 3 0 ; checkBox . Text = food ;

li Dodanie pola wyboru do formula rza . panel . Cont rol s . Add ( checkBox ) ; } } } Rysunek 6- 1 przedstatwia wygląd formularza.

"

r Gra1n r BreOox,cotOdbox, or tut. c.ptkri.

Rysunek 6-8

Dwa typy nieprzesuwalnych formularzy.

6.1 5 Przekształcenie formularza bez ramki w obiekt ruchomy Problem Chcesz urworzyć formularz bez ramki, który jednak może być przesuwany, na przykład gdy rworzysz okno użytkownika o unikalnym wyglądzie (dla wizualnie bogarej aplikacji , takiej jak gra lub odrwarzacz multimedialny).

Rozwiązanie Urwórz inną kontrolkę, która będzie odpowiadała na zdarzenia MouseDown, MouseUp i Mouse­ Move oraz będzie przesuwać formularz w sposób programistyczny.

Omówienie Formularze bez ramki nie mają paska tytułowego, co uniemożliwia użytkownikowi ich przesuwa­ nie. Tę niedogodność można skompensować poprzez dodanie do formularza odpowiedniej kon­ trolki. Na przykład: Rysunek 6-9 przedstawia formularz, który zawiera etykietę w celu obsh1gi przeciągania. Użytkownik może kliknąć etykietę, a następnie przeciągnąć formę do nowego poło­ żenia na ekranie, trzymając przez cały czas wciśnięty przycisk myszy. Kiedy użytkownik przesuwa mysz, formularz przesuwa się automatycznie tak jakby był „przywiązany" do wskaźnika myszy. -

lN5 form has a cootrol-stylo border, bU. no IM- box, - box, cxrtrol box, ar text cOl)tlcn.

llWM

Rysunek 6-9

Ruchomy formularz bez ramki.

Rozdział 6: Formularze Windows

1 67

Aby zaimplementować to rozwiązanie, należy wykonać następujące kroki : 1 . Utworzyć zmienną boolowską na poziomie formularza, która będzie śledzić,

czy formularz

jest aktualnie przeciągany, czy też nie.

2.

Po kliknięciu erykiery kod ustawia flagę wskazującą, że formularz znajduje sic; w stanie prze­ ciągania. W rym samym momencie aktualna pozycja myszy zostaje zapamiętana. Tc; logikę należy dodać do programu obsługi zdarzeń dla zdarzenia

Labei.MouseDown.

3. Kiedy użytkownik przeciągnie mysz poza etykietę, formularz zostanie odpowiednio przesu­ nięty, tak jednak, że pozycja myszy ponad erykietą pozostanie nie zmieniona. Należy dodać teraz te; logikę do programu obsługi zdarzeń dla zdarzenia

LabeLMouseMove.

4. Kiedy użytkownik zwolni przycisk myszy, tryb przesuwania zostanie przełączony na off. Należy dodać teraz ce; logikę do programu obsługi zdarze11 dla zdarzenia

LabeLMouseUp.

A oto kompletny kod formularza: using System ; using System . Windows . Fo rms ; using System . D rawing ; public class D ragForm : System . Windows . Fo rms . Form {

li ( Kod p roj ektanta pominięto . ) li Ś ledzenie , czy f o rmularz j es t w t rybie p rzeciągania . Jeżeli tak , li ruchy myszy zostaną p rzetwo rzone na ruch f o rmu l a rza . p rivate bool d ragging ;

li Zapamiętanie pozycj i , w któ rej kliknięto etykiet ę . p rivate Point pointClicked ; p rivate void lblD rag_MouseDown ( ob j ect sende r , System . Windows . Fo rms . MouseEventA rg s e ) { if ( e . Button == MouseButton s . Left ) { d ragging = t rue ; pointClicked = new Point ( e . X , e . Y ) ;

} else { d ragging

false ;

} } p rivate void l b l D rag_Mou seMove ( obj ect sende r , Sys tem . Windows . Fo rms . MouseEven tArgs e ) { if ( d ragging ) { Point pointMoveTo ;

li Ods z u kanie bieżącej pozycj i myszy we współ rzędnych e k ranu . pointMoveTo = thi s . PointToSc reen ( new Point ( e . X , e . Y ) ) ;

li Kompe n s a c j a do pozycj i kliknięcia kont rol ki . pointMoveTo . Of f s et ( - pointClicked . X , - pointClicked . Y ) ;

1 68 C #

-

Księga przykładów li P rzesunięcie formu la rza . this . Location

=

pointMoveTo ;

} } p rivate void l blD rag_Mou seUp ( obj ect sende r , System . Windows . Fo rms . Mou seEventA rgs e ) { d ragging

=

fal se ;

} p rivate void cmdClose_ Cl ic k ( obj ect sende r , System . EventArg s e ) { t h i s . Close ( ) ; } }

6.1 6 Tworzenie animowanej ikony paska systemowego Problem Chcesz utworzyć animowaną ikonę paska systemowego (na przykład by wskazać status aktual­ nego zadania o długim czasie wykonywania).

Rozwiązanie Utwórz i wyświetl konrrołkę

Notifjlcon.

Skorzystaj z timera, który aktywizuje się okresowo

(na przykład co sekundę lub podobnie) i aktualizuje właściwość Notifjlcon.lcon.

Omówienie System .NET Framework w prosty sposób umożliwia pokazanie ikony paska systemowego

Notifjlcon. Wystarczy dodać rę kontrolkę do formularza, dostarczyć ikonę poprzez !con i - opcjonalnie - dodać przypisane menu kontekstowe dzięki właś­ ContextMeml. Notifylcon, w przeciwieństwie do większości innych kontrolek, automa­

z konrrolką

ustawienie właściwości ciwości

tycznie wyświetli swoje menu kontekstowe po prawidłowym kliknięciu.

Można animować ikonę systemową po prostu okresowo ją zamieniając. Podany niżej pro­ gram korzysta z ośmiu ikon, z których każda pokazuje grafikę księżyca w różnych fazach. Prze­ chodzenie od jednego obrazu do drugiego tworzy złudzenie animacji .

using System ; us ing System . Windows . Fo rms ; using System . D rawing ; public class AnimatedSystemT rayicon

li ( Kod p roj ektanta pominięto . )

System . Windows . Fo rms . Form {

Rozdział 6: Formularze Windows

1 69

Icon [ l images ; int offset = 0 ; p rivate void Forml_Load ( obj ect sende r , System . EventArgs e ) {

li Załadowanie podstawowego zestawu ośmiu ikon . images = new Icon [ B l ; imag es [ 0 1

new I con ( " moon01 . ico" ) ;

images [ l l images [ 2 l

new I con ( " moon93 . ico" ) ;

new I con ( " moon02 . ico" ) ;

images [ 3 l

new Icon ( " moon94 . ico" ) ;

images [ 4 1

new Icon ( " moones . ico" ) ;

images [ S l

new Icon ( " moon96 . ico" ) ;

images [ 6 l

new Icon ( " moon97 . ico" ) ;

images [ 7 ]

new Icon ( " moon98 . ico" ) ;

} p rivate void t ime r_Elapsed ( obj ect sende r , System . Time rs . ElapsedEventArgs e ) {

li Zmiana ikony . li Zda rzenie to u ruchamiane j est raz na sekundę ( co 1000 ms ) . noti fyicon . I con = images [ of f s et l ; off set++ ; if ( of f set > 7 ) offset = 0 ; } }

6.1 7 Test kontrolki wejściowej Problem Chcesz ostrzec użytkownika o wadliwej informacji wejściowej w kontrolce (takiej jak TextBox).

Rozwiązanie Użyj kontrolki

ErrorProvider,

aby wyświetlić ikonę błędu, usytuowaną obok zaangażowanej

kontrolki. Sprawdź, czy nie ma błędów, zanim umożliwisz użytkownikowi kontynuację pracy.

Omówienie Istnieje wiele sposobów weryfikowania danych w aplikacjach Windows. Jednym z nich jest reagowanie na zdarzenia dotyczące testów kontrolki i zabezpieczenie użytkowników przed prze­ kazaniem sterowania do kolejnej kontrolki, gdy wystąpił błąd. Mniej inwazyjnym podejściem jest oflagowanie „atakującej" kontrolki, aby użytkownik mógł przeglądać wszystkie błędy jed­ nocześnie. Można to wykonać w systemie .NET przy użyciu kontrolki

ErrorProvider.

1 70

C# - Księga przykładów

ErrorProvider jest

specjalnym rodzajem kontrolki, która wyświecla ikonę błędu przy kon­

trolce zgłaszającej nieprawidłowość. Dokonuje się co dzięki metodzie

ror

ErrorProvider.SetEr­ ErrorProvider

ze wskazaniem odpowiedniej kontrolki i łańcucha komunikatu o błędzie.

wyświecła wówczas automatycznie ikonę ostrzegawczą po prawej stronie kontrolki. Kiedy użyt­ kownik przesunie mysz nad ikonę ostrzegawczą, pojawi się szczegółowy komunikat. Rysunek

6-I O pokazuje identyfikację błędu wejściowego przez ErrorProvider dla kontrolki TextBox.

�@�

�� lrrorProvidorVdh dation fmoil:

(lllO.con;j

Rysunek 6-1 0

ms is oot a valld omal ad " ; Page . RegisterSta rtupSc ript ( " PopupSc ript " , popupSc ript ) ;

7 .6 Programowe ustawianie fokusu kontrolki Chcesz wyspecyfikować kontrolkę tak, żeby otrzymała fokus, gdy strona zostanie zrenderowana i przesłana do użytkownika.

Rozwiązanie Utwórz instrukcję JavaScript ustawiającą fokus i dodaj ją do strony przy pomocy metody Page.

RegisterStartupScript.

Rozdział 7: ASP. NET i formularze sieciowe Web

1 89

Omówienie Kontrolki ASP.NET Web nie przewidują żadnej możliwości programistycznego ustawiania fokusu. Istnieje wprawdzie właściwość

Tablndex,

pozwalająca ustawić kolejność zakładek, ale odnosi się

wyłącznie do przeglądarki Microsoft Internet Explorer i nie może być zastosowana do tego zada­ nia. Aby obejść co ograniczenie, należy dodać nieco drobnych ulepszeń kodu JavaScript. Poniższy podprogram stanowi uogólnione rozwiązanie tego zadania. Akceptuje on odnie­ sienie do dowolnego obiektu kontrolki, wydobywa odpowiedni identyfikator klienta ID Qest co identyfikator, którego musi użyć kod JavaScript w odniesieniu do kontrolki), a następnie buduje i rejesteruje początkowy skrypt do ustawienia fokusu.

p rivate void SetFoc u s ( Cont rol c t rl ) { li Definiowanie polecenia JavaSc ript p rzenos zącego fokus do li pożądanej kont rol ki . st ring setFocus = " c s c ript language= ' j avasc ript ' > " + " doc ument . get ElementByid ( ' " + c t rl . Cl ientID + " ' ) . focu s ( ) ; " ; li Dodanie kodu JavaSc ript do st rony . this . Reg isterSta rtupSc ript ( " SetFoc u s " , set Foc u s ) ; } Po dołączeniu tego podprogramu do formularza Web można wywołać funkcję SetFocus w miarę potrzeb. A oto przykład ustawienia fokusu przy pierwszym załadowaniu strony:

p rivate void Page_Load ( obj ect sende r , System . EventArgs e ) { if ( ! t h i s . I s PostBa c k ) { li P rzesunięcie do wskazanego pola tekstowego p rzy ładowaniu s t rony . SetFoc u s ( TextBoxl ) ; } }

7 .7 Umożliwienie załadowania pliku przez użytkownika Problem Chcesz utworzyć stronę umożliwiającą załadowanie pliku przez użytkownika.

Rozwiązanie Posłuż się kontrolką ASP.NET

form-data i zapamiętaj

HtmllnputFile,

ustaw typ kodowania formularza na

plik przy użyciu metody HtmllnputFile.PostedFile.SaveAs.

multipartl

1 90

C# - Księga przykładów

Omówienie Ponieważ ASP.NET działa na serwerze, nie ma sposobu zrealizowania dostępu do zasobów na komputerze klienta, w tym również do plików. Jednakże można użyć kontrolki

System. web.

Ul.HtmlControlr.HtmllnputFik,

aby umożliwić użytkownikowi załadowanie pliku. Ta kon­ trolka jest traktowana jako element HTML , wyświetlany jako przycisk przeglądania (Browse) i pole tekstowe, które zawiera nazwę pliku. Użytkownik klika przycisk Browse i wybiera żądany plik. Ten krok jest realiwwany automatycznie i nie wymaga żadnego kodu niestandardowego. Użytkownik powinien następnie kliknąć inny przycisk (który musi utworzyć projektant), żeby zapoczątkować aktualny proces załadowania. Zanim zostanie przygotowana strona do załadowania pliku, należy wykonać następujące czynności: • Ustawić

typ kodowania formularza na multipartlform-data. Aby wykonać tę zmianę, trzeba

znaleźć etykietę



w swoim pliku .aspx i zmodyfikować ją w następujący sposób:

• Dodać kontrolkę

HtmllnputFik.

W systemie Microsoft Visual Studio .NET znajduje się

ona (pod nazwą File Field) pod zakładką HTML w pasku narzędzi. Po dodaniu tej kon­ trolki należy kliknąć na niej prawym przyciskiem myszy i wybrać Run As Server Control, który utworzy wymaganą etykietę



Ustawienia dotyczące bezpieczeństwa katalogu.

Następnie należy kliknąć przycisk Ecłit, aby zmodyfikować ustawienia bezpieczeństwa katalogu. Pojawi się okno pokazane na rysunku 7-5. W dolnej połowie okna można ustawić działanie jednej z metod uwierzytelnienia Windows. Jednakie żadna z tych metod nie może być użyta, zanim nie zostanie wyczyszczona opcja zezwalająca na dostęp anonimowy (Anonymous Access) . Aulhentocatoon Mothod•

o�__ N • - namolpouword oeąa.d to - !IW , Aa:oon uted r.. """""""' occen: -łJseuwne: �,.„.__�;-:.�--_J fr�:• ---: -.; ;;;; e�ct r.::-;

[X!

�-- --- ----

Auther6:atcd a«XCU Fao llle kllowiig ..-.cion motl1ock. - nomo ond � „ iequloed when . � ICCel1 it o-ood io ..... in dos ts.i) ;--·- - -. Dol... Jjomlin: j ;,., ·-·-

OK

Rysunek 7-5

I I

Canool

l i

''

li8'>

Uwierzytelnienie katalogu.

1 94 C #

-

Księga przykładów

Można dopuścić więcej niż jedną metodę uwierzytelnienia; w tym wypadku klient będzie używał najmocniejszej ze wspieranych metod. Jeśli jednak dozwolony jest dostęp anonimowy, zostanie zawsze użyty. Poszczególne mecody uwierzytelnienia zawiera tabela 7-2.

Tabela 7-2 Typy uwierzytelnienia

Tryb

Opis

Anonimowy

Nie wymaga się od klienta przedstawienia jakiejkolwiek informacji . Użytkow­ nicy są zalogowani przy użyciu aktualnego anonimowego koma (zazwyczaj I USR_ (ServerName) ).

Podstawowy

Podstawowe uwierzytel nienie stanowi część standardu HTTP 1 .0 i jest dostar­ czane przez niemal wszystkie przeglądarki i serwery Web. Przy zastosowaniu podstawowego uwierzytelnienia, przeglądarka wyświetła na ekranie użyt­ kownika okno logowania z polami na nazwę i hasło. I nformacje te zostają przekazane do I IS, gdzie są porównywane z poświadczeniami lokalnego koma użytkownika. Uwierzytelnienie podstawowe powinno być zawsze używane w połączon iu z SSL, ponieważ nie umożliwia zaszyfrowania informacji logo­ wania podczas transmisji .

Skrócony

Uwierzytelnienie digest przesyła hash (skrót kryptograficzny) poświadczeń użyt­

(digest)

kownika. Jest znacznie bezpieczniejsze od podstawowego, ponieważ przechwy­ cona informacja logowania nie może być wykorzystana powtórnie. Główną jego wadą jest fakt, że tego typu uwierzytelnienie wspiera tylko przeglądarka Internet Explorer (wersji 5.0 i późniejszej). jak również to, że serwer aplikacji Web musi używać Active Directory lub mieć dostęp do kontrolera domeny.

Zintegrowany Zintegrowane uwierzytelnienie Windows stanowi najlepszy wybór dla większo­ ści intranetowych scenariuszy. Przy wykorzystaniu tego typu uwierzytelnienia, przeglądarka I nternet Explorer automatycznie wysyła żeton logowania bieżą­ cego użytkownika, pod warunkiem, że znajduje się on w zaufanej domenie. Uwierzytelnienie zintegrowane umożliwia tylko wersja 3.0 i późniejsze przeglą­ darki Internet Explorer i nie może być ono zrealizowane przez serwer proxy. Po włączeniu odpowiednich ustawień bezpieczeństwa dla wirtualnego katalogu należy się upew­ nić, że plik Web.config jest ustawiony na uwierzytelnienie Windows. W projekcie Visual Scudio .NET jest to ustawienie domyślne.

< ! - - I nne ustawienia pominięt o . - - >

Od tej chwili wirtualny katalog będzie wymagał uwierzytelnienia użytkownika i aplikacja Web będzie mogła wyszukiwać informacje o użytkowniku. Można ponadto dołączyć reguły autory­ zacji, wykluczające dostęp pewnych użytkowników lub grup do stron Web lub podkatalogów. Można to zrealizować poprzez dodanie etykiet

i



Ostatecznie można napisać własną logikę procedury uwierzytelnienia, działającą na zasadzie sprawdzania tożsamości użytkownika w kodzie danej strony Web przy użyciu właściwości Page. Use, dostarczającej obiekt WindowsPrincipal. Można wyszukiwać nazwę użytkownika przy pomocy właściwości WindowsPrincipal.Identity.Name, a także sprawdzać przynależność do grupy przy użyciu metody WindowsPrincipal.Is/nRole. Poniższy kod strony Web pokazuje zastosowanie tej techniki: using System ; u s ing System . We b ; using System . Web . UI . WebCont rol s ;

1 96

C# - Księga przykładów u sing System . Web . Secu rity . P rincipal ; public class WindowsSecu rityTest : System . Web . Ul . Page { p rotected System . Web . UI . WebCont rol s . Label l blMessage ;

li ( Kod p roj ektanta pominięt o . ) private void Page_Load ( obj ect sende r , System . EventArgs e ) {

li Odczytanie toż samości uwie rzytelnionej p rzez I I S . Windowsidentity identity

=

(Windows identity ) Us e r . Ident ity ;

li Sp rawdzenie , czy j est to Administ rato r . bool isAdmin

Use r . I s inRol e ( @ " BU I LTIN\Administ rato rs " ) ;

=

li Wyświetlenie info rmacj i o tożsamości . lblMessage . Text

=

"You have reached the secu red page , " +

Use r . Identity . Name + " . " + " < b r>Authentication Type : " + identity . Authenticat ionType . ToSt ring ( ) + " < b r>Anonymous : " + identity . I sAnonymou s . ToString ( ) + "Aut henticated :

"

+ identit y . I sAuthent icated . Tost ring ( ) +

" < b r>Guest : " + identity . I sGuest . ToSt ring ( ) + " < b r>System :

"

+ ident ity . I sSystem . ToSt ring ( ) +

" < b r>Administ rat o r :

"

+ isAdmin . Tost ring ( ) ;

} }

7 .9 Użycie uwierzytelniania opartego o formularze Problem Chcesz wykluczyć dostęp użytkowników do pewnych stron, o ile nie uwierzytelnili się oni wcześniej, logując się na niestandardowej srronie.

Rozwiązanie Zastosuj uwierzytelnianie oparre o formularze, konfigurując erykierę



w pliku

aplikacji Web.config. Musisz utworzyć monę logowania, ale ASP.NET zapamięta stan uwierzy­ telnienia użytkownika.

Omówienie Uwierzytelnianie oparre o formularze stanowi elasryczny model zabezpieczeń, króry umożliwia wykluczenie dostępu nieuwierzytelnionych użytkowników do pewnych stron. Należy utworzyć kod, który dokona uwierzytelnienia, a ASP.NET wystawi cookie dla uwierzytelnionych w ten

Rozdział 7: ASP. NET i formularze sieciowe Web

1 97

sposób użytkowników. Użytkownicy nie posiadający cookie zostaną przekierowani do strony logowania, gdy będą usiłowali dostać się do zabezpieczonej strony. Aby wdrożyć uwierzytelnianie oparte o formularze, należy wykonać następujące kroki: • Skonfigurować uwierzytelnianie oparte o formularze przy użyciu etykiety



w pliku aplikacji Web.config. • Wyeliminować dostęp anonimowych użytkowników do określonych stron lub katalogów

przy użyciu ustawień Web.config. • Utworzyć stronę logowania i dodać do niej swoją logikę uwierzytelnienia, posiłkując się

klasą

FormsAuthentication z przestrzeni nazw System. Web.Security.

Pierwszy krok stanowi skonfigurowanie Web.config w katalogu korzenia aplikacji dla umożli­ wienia uwierzytelnienia opartego o formularz, co pokazano w poniższym przykładzie. Należy także wskazać niestandardową stronę logowania (na którą zostaną przekierowani użytkownicy nieuwierzytelnieni) oraz czas, po którym cookie zostanie usunięte. Cookie uwierzytelnienia jest automatycznie odnawiane przy każdym żądaniu Web.

< ! - - Inne u stawienia pominięto . - - >

Następnie należy dołączyć regułę autoryzacji, odmawiającą dostępu anonimowym użytkow­ nikom. Najprostszą drogą zabezpieczenia stron jest utworzenie podkatalogu z własnym pli­ kiem Web.config. Plik Web.config powinien odrzucać dostęp anonimowych użytkowników, jak to pokazano niżej :



< ! - - Inne ustawienia pominięto . - - > Teraz system ASP.NET będzie przekierowywał nieuwierzytelnione żądania stron w tym podka­ talogu do strony logowania. Inną opcję stanowi specjalne wykluczenie dostępu do niektórych stron w bieżącym katalogu przy użyciu etykiety

docation>:

< ! - - Inne u stawienia pominięto . - - >

1 98 C #

-

Księga przykładów

< l authorization> < l syste11 . web> < l location>

Można także odrzucić określonych użytkowników przez wprowadzenie rozdzielonej przecin­ kami listy nazw, zamiast znaku wildcard (?), który oznacza po prostu „wszyscy anonimowi użytkownicy" . Należy teraz utworzyć stronę logowania. Strona ta może uwierzytelniać użytkownika przy użyciu hasła zakodowanego na stałe (wariant odpowiedni dla prostych testów) , bazy danych po stronie serwera łub innej , dowolnego typu logiki uwierzytalniającej . Kiedy użytkownik

FormsAuthentication.RedirectFrom­ LoginPage z nazwą użytkownika. Ta metoda jednocześnie ustawia cookie uwierzytelnienia opar­ zostanie uwierzytelniony, należy wywołać metodę statyczną

tego o formularz i przekierowuje użytkownika do pierwotnie żądanej strony. A oto podsrawowa strona logowania, która po prostu sprawdza określone hasło po kliknięciu przycisku logowania przez użytkownika :

using System ; us ing System . Web ; using System . Web . UI . WebCont rol s ; using System . Web . Se c u rit y ; public class LoginPage : System . Web . Ul . Page { p rotected Sys tem . Web . UI . WebCont rols . Label l bl Stat u s ; p rotected System . Web . UI . WebCont rol s . Button cmdlogin ; p rotected System . Web . UI . WebCont rol s . TextBox txtPasswo rd ; p rotected System . Web . UI . WebCont rol s . TextBox txtName ; li ( Kod p roj ektanta pominięto . )

p rivate void cmdlogin_Cl i c k ( obj ect sende r , System . EventArg s e ) { if ( txtPa s sword . Text . Tolowe r ( ) == " sec ret " ) { FormsAuthent icat ion . Redi rect F romloginPage ( t xtName . Text , fal se ) ; } else { l blStat u s . Text = "Try again . " ; } } } Aby przetestować tę stronę przy pomocy przykładowego kodu dołączonego do książki, należy zażądać dostepu do strony SecurePage.aspx, umieszczonej w zabezpieczonym katalogu. Aplika­ cja zostanie przekierowana do login.aspx i - pod warunkiem, że zostanie wpisane prawidłowe hasło - ponownie przekierowana do strony SecurePage.aspx.

Rozdział 7: ASP.NET i formularze sieciowe Web

1 99

7 .1 O Wykonanie selektywnych testów poprawności danych wejściowych Problem Chcesz wykorzystać kontrolki weryfikacyjne ASP.NET, jednakże chciałbyś wykonać test pro­ gramowo, tak by sprawdzić określone kontrolki lub ich zestawy, a także by móc dostosowywać komunikaty o błędach poprawności danych wejściowych.

Rozwiązanie Wyłącz właściwość EnableC/ientScript w każdej z kontrolek testowych na swojej stronie, aby mogła być ponownie odesłana. Nastepnie użyj metody Page. Validate, aby dokonać sprawdzenia strony lub metody BaseValidator. Validate w celu sprawdzenia poszczególnych kontrolek.

Omówienie Kontrolki testowe ASP.NET są idealnym rozwiązaniem szybkiego sprawdzania formularzy. Działają sprawnie, o ile wymagane jest jednoczesne sprawdzanie całej strony. Jeśli trzeba doko­ nać sprawdzenia tylko części formularza albo konieczne jest podjęcie decyzji co do testu kolejnej kontrolki w oparciu o wynik testu kontrolki poprzedniej, potrzebna jest właśnie weryfikacja selektywna. Pierwszą fazą testu selektywnego jest wyłączenie własciwości ClientScript dla każdej kontrolki testowej na stronie. W przeciwnym wypadku test będzie wykonany po stronie klienta przy uży­ ciu JavaScript i strona nie wstanie odesłana, jeżeli będzie zawierała niedozwolone wartości, a zarządzający zdarzeniami kod projektanta nie będzie wykonywany. Po dokonaniu zmiany można sprawdzać stronę kontrolka po kontrolce przy użyciu metody Base Validator. Validate albo przetestować całą scronę przy użyciu metody Page. Validate. W poniższym przykładzie zastosowano weryfikację po stronie serwera przy pomocy dwóch walidatorów: RangeValidator i &gularExpression Validator (który sprawdza poprawność adresu e-mail). Jeśli sprawdzanie da wynik negatywny, kod sprawdza kolejne walidatory formularza, wykorzystując właściwość Page. Validators. Za każdym razem, gdy znajdzie chybiony walidator, znajduje również odpowiadającą mu kontrolkę przy użyciu metody Page.FindControl, a następ­ nie wyświetla błędną wartość (nie jest to możliwe przy sprawdzaniu automatycznym). using System ; using System . Web ; using System . Web . UI . WebCon t rol s ; public class SelectiveValidation : System . Web . Ul . Page { p rotected System . Web . UI . WebCont rol s . TextBox txtNumbe r ; p rotected System . Web . UI . WebCont rol s . TextBox txtEmail ; p rotected System . Web . UI . WebCont rol s . Label lblCustomSumma ry ; p rotected System . Web . UI . WebCont rol s . Reg u l a rExp ress ionValidator va l idato rEmail ;

200

C#

-

Księga przykładów p rotected System . Web . UI . WebCont rol s . RangeValidato r val idato rNumbe r ; p rotected System . Web . UI . WebCont rol s . Button cmdValidat e ; li ( Kod p roj ektanta pominięto . ) p rivate void cmdValidate_Clic k ( obj ect sende r , System . EventArgs e ) { li Weryfikacj a st rony . this . Validate ( ) ; if ( ! Page . I sVal id ) { lblCustomSumma ry . Text

=

" ; "

fo reach ( BaseValidato r validato r in this . Val idato rs ) { if ( ! validator . I sVal id ) { TextBox inval idCont rol

( TextBox )

thi s . FindCont rol ( validato r . Cont rolToValidate ) ; l blCu stomSumma ry . Text += "The page contains the fol lowing e r ro r : " + validato r . E r ro rMessage + " . " + "The invalid input i s : " + inval idCont rol . Text + " . " + "cb r> " ; } } } else { l blCus tomSumma ry . Text

"Val idation s u cceeded . " ;

} } }

Rysunek 7-6 przedscawia formularz z nieprawidłowymi danymi wejściowymi.

A

number len than 120: 1«

The page conta1ns the folowing error: Yalue out of range. The lnvałtd input 11 1 11. 'Tł'Mi póMJe

COl"ltain-. the folowing error· Not The 1n1ri11łłd 1nput is ebcd.etg

Rysunek 7-6

o

Yolld tunoll nddnt�•.

Wykorzystanie niestandardowej weryfikacji.

Rozdział 7: ASP.N ET i formularze sieciowe Web

201

7 .1 1 Dynamiczne dołączanie kontrolek do formularza Web Problem Chcesz dodać kontrolkę Web do strony Web podczas wykonywania programu i obsługiwać zdarzenia generowane przez cę kontrolkę.

Rozwiązanie Utwórz obiekc kontrolki, dodaj ją do zbioru kontrolek Controls zasobnika i użyj inscrukcji AddHandler dla dołączenia wszysckich programów obsługi zdarzeń. Musisz tworzyć kontrolki po każdym odesłaniu strony.

Omówienie Aby dodać kontrolki Web do strony, można wykorzystać technikę podobną do dodawania kon­ crolek Windows do formularza z następującymi różnicami: •

Dynamicznie dodawane kontrolki będą występować tylko do momentu następnego ode­ słania scrony. Jeżeli będą pocrzebne później, należy je ponownie utworzyć przy ponownym pobraniu strony. To wymaganie nie ogranicza jednak możliwości wpisania kodu, który zarządza zdarzeniami.



Dynamicznie dodawane kontrolki nie są łatwe do pozycjonowania. Naj�ciej trzeba będzie użyć elementów scerujących kodu HTML (takich jak ) do oddzielania dynamicznie utworzonych kontrolek, jeśli tworzona jest więcej niż jedna.



Dynamicznie tworzone kontrolki powinny być umieszczane raczej w zasobniku (tak.im jak

Panel lub Litera/Control;, niż bezpośrednio na stronie, co umożliwia ich łatwiejsze pozycjo­ nowanie. •

Jeżeli zachodzi potrzeba interakcji z kontrolką, należy jej nadać unikacowy identyfikator ID. Można go używać do przeglądania i wyszukiwania kontrolek ze zbioru Controls ich zasobnika.

Najlepszym miejscem generowania nowych kontrolek jesc program obsługi zdarzeń Page.Load, który zapewnia utworzenie kontrolki za każdym razem, gdy dana strona jest obsługiwana. Dodatkowo, kiedy dodaje się kontrolkę wejściową używającą widoku stanu, widok ten wstanie przywrócony po wystąpieniu zdarzenia Page.Load. Oznacza co, że dynamicznie wygenerowane pole tekstowe zachowa swój tekst przy wielokrotnych odesłaniach strony, podobnie jak pole cekscowe zdefiniowane w pliku .aspx. Podobnie, ponieważ zdarzenie Page.Load zawsze wystę­ puje, zanim jakieś inne zdarzenie będzie miało miejsce, można odtworzyć kontrolkę przejmującą zdarzenia po stronie serwera i jej kod zarządzania zdarzeniami wstanie wyzwolony natychmiast po zdarzeniu Page.Load. Technika ca pozwala na przykład generować dynamicznie przycisk odpowiadający na kliknięcie użyckownika. Poniższy przykład demonstruje wszystkie te koncepcje. Generuje on trzy dynamiczne kon­ trolki serwera (dwa przyciski i pole tekscowe) i pozycjonuje je przy użyciu literalnych elementów

202

C# - Księga przykładów sterujący, służących jako separatory. Przyciski zostają połączone z różnymi programami obsługi zdaneń. Do okna tekstowego jest przydzielony unikalny identyfikator, by jego tekst można było prieszukiwać później w odpowiedzi na kliknięcia przycisku. Rysunek 7-7 przedstawia działanie tej strony. "'

"*

llWo

·-

-

-

i�) L� ;' � '°""' · ·- -r- e , . lc ·-»- · .:t-1�..,..,.�� · QGo O.O.

[



.)

Oyn1Mlc Button A

Oyn„lc Button B

. ·

t:' -�\

I

Som1 uampł1 tut

CHdl.ed A Tei«BoM contolns: Bom• •Mempl• teMł

Rysunek 7-7

Dynamicznie generowane kontrolki .

Pden kod jest przedstawiony niżej : using System ; u s ing System . Web ; using System . Web . Ul ; using System . Web . Ul . WebCont rol s ; using System . Web . Secu rity ; public cla s s DynamicCont rol s : System . Web . Ul . Page { p rotected System . Web . Ul . WebCont rol s . Label l bl Message ; p rotected System . Web . Ul . WebCont rols . Panel pnl ;

li ( Kod p roj ektanta pominięto . ) p rivate void Page_Load ( obj ect sende r , System . EventArgs e ) {

li Utwo rzenie dynamicznego p rzyci s ku . Button dynamicButton = new Button ( ) ; dynamicButton . Text = " Dynamie Button A " ;

li Połączenie z obsł ugą zdarzeń . dynamicButton . Click += new EventHandle r ( cmdDynamicA_C lic k ) ;

li Dodanie p rzycisku do panel u . pnl . Cont rols . Add ( dynamicButton ) ;

li Dodanie separatora . pnl . Cont rol s . Add ( new Lite ralCont rol ( "c b r> " ) ) ;

li Utwo rzenie d rug iego p rzycisku dynamicz nego . dynamicButton = new Button ( ) ; dynamicButton . Text = " Dynamie Button B " ;

Rozdział 7: ASP. NET i formularze sieciowe Web

203

dynamicButton . Cl i c k += new EventHandl e r ( cmdDynamicB_Click ) ; pnl . Con t rol s . Ad d ( dynamicButton ) ;

li Dodanie s epa rat o ra . pnl . Cant rol s . Add ( new LiteralCont rol ( " < b r>" ) ) ;

li Utwo rzenie dynamicz nego pola tekstowego . TextBox dynamicText = new TextBox ( ) ; pnl . Con t rol s . Add ( dynamicText ) ;

li P rzypisanie unikatowego I D , aby możl iwe było odczytanie pola li tekstowego z kolekcj i kont rolek . dynamicText . ID = " DynamicText " ; } p rivate void cmdDynamicA_Click ( obj ect sende r , System . EventArgs e ) { l blMessage . Text = " C l ic ked A " ; GetText ( ) ; } p rivate void cmdDynamicB_Click ( obj ect sende r , System . EventArgs e ) { l blMes sage . Text

=

" C l ic ked B " ;

GetText ( ) ; } p rivate void GetText ( ) { l blMessag e . Text += " < b r> " ; f o reach ( Cont rol ct rl in pnl . Cont rol s ) { if ( ct rl . ID == " DynamicText " ) { l blMes sage . Text += " TextBox contains : " + ( ( TextBox ) ct rl ) . Text ; } } } }

Jeżeli chcesz tworqć dynamicznie złożony układ strony, zawierający rozbudowane „grupy" ste­ rujące, można zdecydować sic; raczej na użycie kontrolek użytkownika i załadować je dynamicz­ nie na stronc;. Ta technika jest zademonstrowana w przepisie 7. 1 3.

7 .1 2 Dynamiczne renderowanie obrazu Problem Chcesz dynamicznie renderować grafikc; (na prqkład utworqć dane wyjściowe kontrolki gene­ rującej wykres lub diagram).

204

C#

-

Księga przykładów

Rozwiązanie Zbuduj grafikę przy użyciu GOI+ i obiektu w pamięci

System.Drawing.Bitmap.

Można następ­

nie zapisać ro w strumieniu wyjściowym strony albo zapamiętać na twardym dysku serwera i wyświetlić przy użyciu kontrolki

Image Web.

Omówienie W aplikacji internetowej można kreślić dynamiczną grafikę przy wykorzystaniu tego samego kodu GOI + , którego używają aplikacje Windows. Jedyną różnicę stanowi sposób renderowania końcowej grafiki. Zasadniczo istnieją dwa podejścia, które można wykorzystać. • Można przekształcić binarną zawartość obrazu w strumień bezpośrednio do właściwości

OutputStream

obiektu

HttpRespome. Jest to dobry sposób,

aby wygenerować szeroki zakres

obrazów, bez zapełniania dysku twardego serwera plikami, które więcej nie będą używane. Jest to również najlepszy wybór w przypadku tworzenia dynamicznych obrazów, dopasowa­ nych do danych wejściowych użytkownika. • Można zachować obraz w systemie plików na serwerze i użyć etykiety HTML

,

aby

go wyświetlić. Taki wybór jest polecany w przypadku utworzenia grafiki, która będzie użyta ponownie, co pozwała uniknąć zbędnego obciążenia jej ciągłym odtwarzaniem. Ten przepis wykorzystuje oba podejścia. Najpierw rozważymy, jak utworzyć obraz w sposób dynamiczny bez zapamiętywania go w pliku. W podanym niżej przykładzie celem jest utworze­ nie prostego banera (rysunek 7-8).

Rysunek 7-8

Dynamicznie generowany baner.

Można zauważyć, że jedyną dostarczaną przez użytkownika wartością w tym przykładzie jest sam tekst banera, który jest podawany jako łańcuch zapytania. Czcionki, kolory i rozmiary są zakodowane na stałe (chociaż mogą być również oparte na innych tego rodzaj u parametrach łub wartościach domyślnych Web.config).

Rozdział 7: ASP. NET i formularze sieciowe Web 205 A oto pełen kod strony: u s ing System ; using System . We b ; using System . Web . UI . WebCont rol s ; using System . D rawing ; using System . D rawing . D rawing2D ; public class DynamicG raphic : System . Web . Ul . Page {

li

( Kod p roj ektanta pominięto . )

p rivate void Page_Load ( obj ect sende r , System . EventArgs e ) {

li li

Odczytanie tekstu z łańcucha zapytan ia . J eżeli n ie podano tekstu , wybó r domyśl nego łańcucha .

st ring text = " " ; if ( Request . Que rySt ring [ " image" J == nul l ) { Response . Redirect ( Request . URL + " ? image= " + Se rve r . URLEncode ( "This is a test image " ) ) ; } else { text = Serve r . URLDecode ( Request . QuerySt ring [ " image" J ) ; }

li

Utwo rzenie bitmapy w pamięc i , w któ rej zostanie na rysowany o b raz .

li

Bitmapa ma 300 pikseli s z e rokoś ci i 200 wysokości .

int widt h

=

300 , height = 200 ;

Bitmap b itmap = new Bitmap ( width , height ) ;

li

Odczytanie kontekstu g raficznego dla bitmapy .

G raphics g raph i c s = G raphic s . F romimage ( bitmap ) ;

li li

Wybó r kolo ru tła i j a kości rende rowania . Kol o r ten stanie się ob ramowaniem

g raphics . Clea r ( Colo r . O rangeRed ) ; g raphics . Smoot hingMode = Smoot hingMode . AntiAlia s ;

li

Malowanie p ro stokąt a .

g raphic s . FillRectang le ( new SolidBru s h ( Colo r . Olive ) , 5 , S , widt h - 10 , height - 10 ) ;

li

Wybó r fontu i wyrównania teks t u .

Font fontBanner = new Fon t ( " Ve rdana " , 24 , FontStyle . Bold ) ; St ring Format st ringFormat = new St ringFo rmat ( ) ; s t ringFo rmat . Alignment = St ringAlignment . Cente r ; s t ringF o rmat . LineAlignment = St ringAlig nment . Cent e r ;

li

Malowanie tekstu .

g raphic s . D rawSt ring ( text , fontBanne r , new Solid B ru s h ( Colo r . LightYellow ) , new Rectangle ( 0 , 0 , width , height ) , st ring Format ) ;

206

C#

-

Księga przykładów

li

Rende rowan ie o b razu do s t rumienia wyj ściowego HTML .

bitmap . Save ( Respon se . OutputSt ream , System . D rawing . Imag ing . ImageFo rmat . Gif ) ; g raphic s . Dispose ( ) ; bitmap . Dispose ( ) ; } }

Kiedy obraz zostaje zapamiętany w strumieniu odpowiedzi, zastępuje on wszystkie inne dane wyjściowe. Nie można więc zastosować tej techniki na stronie, która zawiera również kontrolki Web albo zawartość statyczną HTML. Jeśli zatem trzeba utworzyć stronę łączącą wygenerowane dynamicznie obrazy i kontrolki Web, należy opakować owe wygenerowane dynamicznie obrazy w kontrolki lub zapisać obraz na twardym dysku przed wyświetleniem. Aby zapamiętać plik na twardym dysku, należy przenieść generujący obraz kod do oddzielnej metody, która w poniższym przykładzie została nazwana GenerateBanner. Działanie programu obsługi zdarzeń Page.Load należy więc zacząć od sprawdzenia, czy plik już istnieje przy pomocy metody statycznej File.Exists. Jeśli plik nie istnieje, należy go utworzyć w pamięci wywołując GmerateBanner i zapisać przy użyciu metody Bitmap.Save. W przeciwnym wypadku należy bezpośrednio załadować obraz. Poniższy przykład ilustruje to rozwiązanie: using System ; u sing System . IO ; using System . Web ; using System . Web . UI . WebCont rol s ; u s ing System . D rawing ; using System . D rawing . D rawing2D ; public c l a s s DynamicGraphic : System . Web . Ul . Page { p rotected System . Web . UI . WebCont rol s . Image imageCont rol ;

li

( Kod p roj ektanta pominięto . )

p rivate Bitmap GenerateBanne r ( ) {

li li

Utwo rzenie o b razu p rzy użyciu tego ; amego kodu j ak w pop rzednim p rzykładzie ( kod pominięt o ) .

} p rivate void Page_Load ( obj ect sende r , System . EventArgs e ) {

li

Ustawienie nazwy pliku w opa rciu o tekst o b ra z ka .

st ring fileName = Request . Que rySt ring [ " image " J + " . gi f " ; Bitmap bitmap = n u l l ;

li

Sp rawdzenie , czy o b ra zek z tym tekstem j uż istniej e .

if ( File . Exists ( f ileName ) ) {

li

Załadowanie istniej ącego o b raz u .

t ry { bitmap = new Bitmap ( fileName ) ;

Rozdział 7: ASP.N ET i formularze sieciowe Web 207 } catch { bitmap = Gene rateBanne r ( ) ; } } else { bitmap = Gene rateBanne r ( ) ;

li Zapisanie o b razu . bitmap . Save ( fileName , Sys tem . D rawing . Imaging . Imagefo rmat . Gif ) ; }

li Wyświet lenie obrazu poprzez u stawienie właściwości Image . imageCon t rol . ImageURL = fileName ; } }

7 .1 3 Programowe ładowanie kontrolek użytkownika Problem Chcesz zbudować dynamicznie interfejs użytkownika dla strony z użyciem jednej lub więcej kontrolek użytkownika.

Rozwiązanie Użyj metody

Page.LoadControl w celu utworzenia instancji obiektu Controls zasobnika kontrolek.

kontrolki dla pliku.ascx,

a następnie dodaj go do zbioru

Omówienie Kontrolki użytkownika stanowią zawarte w sobie grupy kontrolek. Podobnie jak formularze Web, kontrolki użytkownika składają się z części układu graficznego (layout), definiującej zawarte kontrolki (plik .ascx) i części z dołączonym kodem logiki zarządzania zdarzeniami (plik

)

. es .

Kontrolki użytkownika pozwalają na ponowne użycie wspólnych dememów na licznych stro­ nach i budowę złożonych interfejsów z mniejszych bloków. Jedną z użytecznych cech kontro­ lek użytkownika jest możliwość ładowania ich programowo,

co

umożliwia tworzenie interfejsu

dającego się w znacznym stopniu skonfigurować i dynamicznie dostosowywać do potrzeb użyt­ kownika. Ładuje się po prostu kontrolkę, konfiguruje jej właściwości, a następnie dodaje się ją do innej kontrolki zasobnikowej. Rozważmy na przykład stronę, która wygenerowała dynamiczną grafikę w przepisie

7 . 1 2.

To samo podstawowe rozwiązanie mogłoby zostać zaimplementowane w sposób bardziej wrientowany obiektowo poprzez utworzenie kontrolki użytkownika, która zawiera dyna­ miczną grafikę. Kontrolka użytkownika może pozwalać stronie określać tekst, czcionki, kolory

208

C#

-

itd.

Księga przykładów za

pośrednictwem różnych właściwości. Poniższy przykład pokazuje, jak może wyglądać

kontrolka użytkownika:

u s ing System ; using System . Web ; u s ing System . Web . UI . WebCont rol s ; u s ing System . D rawing ; using System . D rawing . D rawing2D ; public class DynamicGraph icCont rol li ( Kod p roj ektanta pominięto . ) p rivate st ring imageText

=

public st ring ImageText { get { ret u rn imageText ; } set { imageText

=

value ;

} } p rivate Font text Font ; public Font TextFont { get { ret u rn textFont ; } set { textFont

va lue ;

=

} } p rivate Size imageSiz e ; public Size ImageSize { get { retu rn imageSize ; } set { imageSize

=

value ;

} } p rivate Colo r fo reCol o r ; public Col o r ForeColo r { get { ret u rn fo reColo r ; } set { f o reColo r = val ue ; } }

u u . '

System . Web . UI . UserCont rol {

Rozdział 7: ASP.N ET i formularze sieciowe Web 209 p rivate Colo r backColo r ; public Color BackCol o r { get { ret u rn backCol o r ; } set { backColo r = val u e ; } } p rivate Col o r bo rde rCol o r ; public Colo r BorderColor { get { ret u rn bo rderCol o r ; } set { borde rColor = value ; } } p rivate void Page_ Load ( obj ect sende r , System . EventArgs e ) { if ( ImageText == " " ) ret u r n ;

li Utwo rzenie bitmapy w pamięci , w któ rej zostanie na rysowany o b raz . Bitmap bitmap = new Bitmap ( ImageSize . Widt h , ImageSiz e . Height ) ;

li Pob ranie kontekstu g rafic znego dla bitmapy . G raphics g raphics = G raphics . F romima g e ( bitma p ) ;

li O k reślenie kol o ru tła i j a kości rende rowania . li Kol o r ten będzie tworzył o b ramowanie g raphics . Cl ea r ( Bo rde rColo r ) ; g raphics . SmoothingMode = Smooth ingMode . AntiAlia s ;

li malowanie p rostokąta . g raphics . FillRectangle ( new SolidBru s h ( BackCo l o r ) , 5 , 5 , ImageSize . Width - 10 , ImageSize . Height - 1 0 ) ;

li Wybór wyrównania dla tekstu . St ringFormat s t ring Format = new St ring Fo rmat ( ) ; st ringFormat . Alignment = St ringAlignment . Cent e r ; st ringFormat . LineAlignment

=

St ringAlignment . Cente r ;

l i Malowanie tekstu . g raphics . D rawSt ring ( ImageText , TextFont , new SolidBrush ( Fo reCol o r ) , new Rectang le ( 0 , 0 , ImageSize . Widt h , ImageSize . Height ) , st ringFo rmat ) ;

li Rende rowan ie o b razu do s t rumienia wyj ściowego HTML . bitmap . Save ( Response . OutputSt ream , System . D rawing . Imaging . ImageFo rmat . G if ) ;

210

C# - Księga przykładów g raphic s . Dispose ( ) ; bitmap . Dispose ( ) ; } }

Page.Load. LoadControl przywraca ogólny

Formularz Web załadowuje cę kontrolkę użytkownika do programu obsługi zdarzeń Następnie wędruje ona do zasobnika kontrolek obiekt

Control,

Panel.

Metoda

który kod rzutuje do odpowiedniej klasy kontrolek użytkownika.

u s ing System ; using System . Web ; us ing System . Web . UI . WebCont rol s ; u s ing System . D rawing ; public class DynamicCont rolTest : System . Web . Ul . Page { p rotected System . Web . UI . WebCont rol s . Panel pnl ;

li ( Kod p roj ektanta pominięto . ) p rivate void Page_ Load ( obj ec t sende r , System . EventArgs e ) {

li Załadowanie kont rolki . DynamicG raphicCont rol ct rl ; ct rl = ( DynamicGraphicCont rol ) Page . LoadCont rol ( " DynamicG raphicCont rol . as c x " ) ;

li Konfigu rowanie właściwości kont rol ki . ct rl . ImageText = "This is a new banner test " ; ct rl . ImageSize = new Size ( 300 , 200 ) ; ct rl . Text Font = new Font ( "Ve rdana " , 24 , FontStyle . Bold ) ; ct rl . BackColor = Colo r . Ol ive ; ct rl . Fo reColo r = Col o r . LightYellow; ct rl . Bo rderCol o r = Colo r . O rangeRed ;

li Dodanie kont rol ki do st rony . pnl . Cont rol s . Add ( ct rl ) ; } } W systemie Visual Studio .NET klasa komrolek użytkownika jest zawsze dostępna, ponieważ klasy są skompilowane w pojedynczą asemblację .dll. Jeżeli jednak kontrolka użytkownika nie jest częścią projektu, kod nie dysponuje wymaganą klasą i nie będzie miał dostępu do żadnych właściwości ani metod kontrolek użytkownika. Remedium może być zdefiniowanie klasy bazo­ wej lub interfejsu, który określa podstawową funkcjonalność, do której potrzebny jest dostęp z każdej z kontrolek użytkownika.

Uwaga Demonstrację pełnych możliwości tej techniki stanowi studium portalu IBuySpy, dostępne pod adresem http://www.asp.net /IBS_Portal. Jest to układ o wielkich możliwoś­ ciach dopasowania do potrzeb użytkownika, zbudowany w całości z dynamicznie ładowa­ nych kontrolek użytkownika.

Rozdział 7: ASP. NET i formularze sieciowe Web 21 1

7 .1 4 Wykorzystywanie buforowania stron i fragmentów Problem Chcesz poprawić wydajność poprzez zapamiętywanie w buforze całkowicie przygotowanych stron.

Rozwiązanie Dodaj dyrektywę

OutputCache do strony lub kontrolki użytkownika i ustał czas (w sekundach),

przez który strona powinna być przetrzymywana w buforze.

Omówienie Rozważne wykorzystanie buforowania pozwala zredukować ryzyko pojawienia się wąskich gar­ deł, takich jak dostęp do bazy danych i znacząco zwiększyć całkowitą wydajność witryny Web. Największe znaczenie może mieć dla witryn o dużym obciążeniu ruchem. Zastanówmy się co się stanie, jeśli zapamiętasz w buforze stronę wyświetlającą wyniki zapytania do bazy danych. Jeżeli strona będzie zbuforowana przez jedną minutę, a w tym czasie zostanie odebranych I O żądań dla rej strony, obciążenie bazy danych zostanie zmniejszone dziesięciokrotnie. Zaimplementowanie buforowania jest łatwe. Należy po prostu do strony Web dodać dyrek­ tywę

OutputCache.

Musi być ona dodana do pliku .aspx, a nie do kodu

.es

w tle. Oto przykład

w którym zapamiętuje się stronę na 20 sekund:

Kolejny przykład buforuje stronę na 20 sekund, ale utrzymuje oddzielne kopie w zależności od argumentów łańcucha zapytania:

Buforowanie można przetestować przy użyciu strony, która wyświetla czas i datę serwera. Zauważmy, że kolejne odwołania do takiej strony nie powodują jej regeneracji. Tak więc będzie wyświetlany stary czas dopóki ważność buforowanej kopii się nie skończy. Buforowanie jest nieefektywne w następujących przypadkach: • Strona musi się dopasować zgodnie z ustawieniami specyficznymi dla użytkownika, takimi

User) lub stanu (obiekt Session). W tym wypadku ponowne użycie tej samej strony dla różnych użytkowników nie

jak informacje uwierzytelniające użytkownika (wbudowany obiekt jest możliwe.

• Strona zawiera kontrolki, które odsyłają dane z powrotem i wywołują zdarzenia po stronie

serwera. • Strona powinna wykonać inne działanie (takie jak wykonanie wpisu w dzienniku, wprowa­

dzenie informacji do bazy danych lub zmiana zmiennej aplikacji). Buforowana strona wyko­ rzystuje powtórnie w pełni zrenderowany zapis HTML, a cały kod strony jest pomijany.

212

C#

Księga przykładów

-

• Strona zawiera dane, które muszą być wygenerowane przy użyciu najświeższych informacji.

Może ro dotyczyć na przykład przeglądu magazynu, ale problem nie musi wystąpić w przy­ padku katalogu produktów. W omówionych przypadkach można używać bardziej elastycznych form buforowania. Można stosować zapamiętywanie danych opisane w przepisie 7. 1 5, aby przechować określony obiekt. Można reż użyć buforowania fragmentów, by zapamiętać część strony. Buforowanie fragmentów wymaga przygotowania kontrolki użytkownika, która będzie zawierała zawartość do przecho­ wania i dodać do niej dyrektywę

OutputCache.

Następnie będzie można wykorzystać kontrolkę

użytkownika na stronie Web. Kod strony Web będzie przez cały czas działał, ale zawarta w nim kontrolka użytkownika zostanie zapamiętana w buforze.

7 .1 5 Powtórne użycie danych z bufora ASP.NET Problem Chcesz użyć buforowania, ale nie można zapamiętać całej strony, ponieważ zawiera ona część kodu, który musi działać lub zawartość generowaną dynamicznie.

Rozwiązanie Użyj metody

Cache.Insert do zapamiętania wszystkich obiektów objętych regułami absolucnego

lub prolongowanego wygasania.

Omówienie Objekt

Cache umożliwia przechowywanie niemal wszystkich obiektów .NET przy użyciu klucza

w postaci łańcucha, zgodnie ze zdefiniowanymi przez projektanta zasadami wygasania. System ASP.NET automatycznie obsługuje bufor usuwając z niego te obiekty, których ważność wygasła lub gdy zaczyna brakować pamięci. Są dwa typy zasad wygasania, którymi można się posłużyć przy zapamiętywniu danych w buforze. Wygasanie absolutne niszczy zapamiętane pozycje po upływie określonego czasu, analogicznie do buforowania danych wyjściowych. Jest to najlepsza metoda do zapamiętywania informacji, która powinna być okresowo odświeżana (jak np. katalog produktów).

li Zapisanie Obj ectToCache p rzy użyciu klucza " Catalog " na 10 minut . li TimeSpan . Ze ro oznacza , że nie będzie u ż ywana metoda wygasania li p rolongowanego . Cache . I nse rt ( " Catalog " , Obj ectToCache , nul l , DateTime . Now . AddMinutes ( 10 ) , TimeSpan . Ze ro ) ; System ASP.NET wspiera również zasadę wygasania prolongowanego, która usuwa obiekty, gdy nie są używane przez określony czas

czas .

W rym wypadku przy każdym dostępie do obiekru jego

życia jest odnawiany. Wygasanie rypu prolongowanego jest odpowiednim rozwiązaniem,

kiedy dotyczy zapamiętanej informacji, której ważność nie ulega zmianie, ale nie zawsze jest

Rozdział 7: ASP. NET i formularze sieciowe Web 2 1 3 używana (np. dane archiwalne). Informacja taka nie musi być odświeżana, ale nie powinna być przechowywana w buforze, jeśli nie jest używana.

li P rzechowuj Obj ectToCache pod kluczem " Catalog " , o ile j es t używany co li naj mniej raz na 10 minut . li DateTime . MaxValue wskazuj e , że nie będ zie używane wygasanie bezwzględne . Cache . I n s e rt ( " Catalog " , Obj ectToCach e , n u l l , DateTime . MaxValue , TimeSpan . F romMinutes ( 10 ) ) ; Pozycje zapamiętane w buforze można wyszukiwać przez nazwę klucza. Jednakże przedtem zawsze należy sprawdzić, czy pozycja istnieje i dopasować ją do żądanego typu. Przy dodawaniu obiektów do bufora najlepszym wzorem projektowym jest utworzenie oddzielnej funkcji, w razie potrzeby odtwarzającej obiekt. Na przykład przy zapamiętywaniu

DataSet należy utworzyć funkcję, która sprawdza bufor, a dostępu do bazy danych żąda tylko DataSet nie można odnaleźć. Pozwala co uniknąć najbardziej czasochłonnej

wtedy, gdy obiektu

części przetwarzan ia strony: kierowania zapytań do bazy danych, przy jednoczesnym przetwa­ rzaniu dopasowującym wyświetlanie lub inne działania (takie jak sortowanie według kryteriów użytkownika) . Kolejny przykład wyświetla tabelę z informacjami o klientach, wydobytymi z DataSet. Główną składową jest klasa

DataSet i

CustomerDatabase, która zawiera funkcjonalność wymaganą do wypałnienia

zarządzania buforem. Ponieważ ta klasa nie dziedziczy po Page musi posługiwać się ,

statyczną właściwością

HttpContext. Current do wyszukiwania odniesień do obiektu Cache.

using System ; using System . Data ; u s ing System . Web ; using System . Configu ration ; using System . Diagno s t i c s ; using System . Web . Ca c h ing ; using System . Data . SqlClient ; public class Custome rDatabase { p rivate st ring connectionSt ring ;

li Odczytanie odnośników do obiektu Cache . p rivate Cache cache = Htt pContext . Cu r rent . Ca ch e ; public Custome rDataba se ( ) {

li Odczytanie łańcucha połączenia z pliku Web . config . connectionSt ring

=

Configu rationSettings . AppSettings [ " No rthwindCon " ) ;

} public DataSet GetCustome rs ( ) { DataSet custome rsDS ;

li Wys z u kanie bufo rowanych obiektów . if ( cache [ " C u stome rs " )

==

null ) {

li Odczytanie DataSet z bazy danych . cu stome rsDS

=

GetCustome rs F romDataba s e ( ) ;

li Zapisanie pozycj i w bufo rze z wygasaniem p rolongowanym 60 sekund .

214 C# - Księga przykładów cache . In se rt ( " C u stome rs " , custome rsDS , null , DateTime . MaxValue , TimeSpan . F romSecond s ( 60 ) ) ;

li Wyświetl komunikat debugowania . Debug . Writeline ( " DataSet c reated f rom data sou rce . " ) ; } else {

li Wyświetl komunikat debugowania . Debug . Writeline ( " DataSet ret rieved f rom cache . " ) ;

li Odczytaj pozycj ę . custome rsDS = ( DataSet ) ca ch e [ " C u stome rs " J ; }

li Zwróć DataSet . retu rn custome rsDS ; } p rivate DataSet GetCu stome r s F romDatabase ( ) {

li Utwó rz DataSet . DataSet cu stome rsDS = new DataSet ( ) ;

li wypełnij DataSet ( z pliku ) . SqlConnect ion con = new SqlConnect ion ( connectionSt ring ) ; SqlCommand cmd = new SqlCommand ( " SE LECT * FROM Custome rs " , con ) ; SqlDataAdapte r adapter = new SqlDataAdapt e r ( cmd ) ; t ry { con . Open ( ) ; adapte r . Fi 1 1 ( custome rsDS , " C u stome rs" ) ; } catch { custome rsDS = null ; } finally { con . Close ( ) ; }

Następnym krokiem jest utworzenie strony Web, która używa klasy

CustomerDatabase.

Poniż­

szy przykład pokazuje stronę z opcją DataGrid i pojedynczym przyciskiem Button. Po kliknięciu przez użytkownika przycisku

Database. GetCustomers.

Button

strona za każdym razem wywołuje metodę

Customer­

Informacja ta jest odczytywana z bufora, jeśli jesc dostępna, lub żądana

ponownie, jeśli od poprzedniego zapytania upłynęło więcej niż 60 sekund. Można również określić, czy

DataSet wstało uzyskane z bufora,

przeglądając dane wyjściowe w oknie Debug.

u s ing System ; u s ing System . Web ; using System . Web . UI . WebCont rol s ; public class Login Page : System . Web . Ul . Page { p rotected System . Web . UI . WebCont rol s . DataG rid DataG rid l ; p rotected System . Web . UI . WebCont rol s . Button cmdGetData ;

Rozdział 7: ASP. NET i formularze sieciowe Web 215 li ( Kod p roj ektanta pominięt o . ) p rivate void cmdGetData_Click( obj ect sende r , System . EventArgs e ) { Custome rDatabase custDB = new Custome rDatabase ( ) ; DataG rid l . DataSou rce = custD B . GetC u stome rs ( ) ; OataG rid l . DataBind ( ) ; } }

7 .1 6 Włączanie wykrywania i usuwania błędów (debugging) strony Web Problem Przy próbie usuwania błędów z aplikacji Web przy pomocy Visual Studio .NET występuje błąd " unable to start debugging on the Web server" ( " rozpoczęcie usuwania błędów na serwerze Web nie jest możliwe" ) .

Rozwiązanie Sprawdź, czy usługi Internet lnformation Services (IIS) są prawidłowo zainstalowane, czy pakiet ten był zainstalowany wcześniej, niż Microsoft .NET Framework i czy zintegrowane uwierzytel­ nienie systemu Windows jest dopuszczone dla katalogu aplikacji Web.

Omówienie Błąd "unable to start debugging" sygnalizuje, że system Visual Studio .NET był w stanie skom­ pilować aplikacje; Web, ale nie może jej wykonać w trybie poprawiania błędów (debugging). Nie­ stety, problem ten może sic; pojawiać w różnych sytuacjach, z których najczc;ściej można spotkać następujące: • I I S - komponent Windows hostujący aplikacje; Web nie jest zainstalowany lub jest zainsta­

lowany nieprawidłowo. • Użytkownik uruchamiający Visual Studio .NET nie ma uprawnienia Debug programs na

komputerze serwera Web. • Użytkownik Visual Studio .NET nie ma zezwolenia na proces debugowania ASP.NET.

Na przykład, jeżeli proces ASP.NET działa na koncie lokalnego systemu, użytkownik musi mieć przywileje administratora, aby móc wykrywać i lokalizować błędy. • Na serwerze Web zainstalowana jest wersja Windows nie udostępniająca opcji debugowa­

nia, taka jak Microsoft Windows NT lub Windows XP Home Edition (systemy Windows 2000, Windows XP Professional i Windows Server 2003 zawierają te; opcje;).

• Aplikacja Web nie posiada pliku Web.config lub zawarte w nim wpisy nie zezwalają na debu­

gowanie.

216

C#

-

Księga przykładów

• Zainstalowano Visual Studio .NET, ale brak zintegrowanego uwierzytelnienia systemu

Windows dla wirtual nego katalogu. Pierwszym krokiem, który powinien być wykonany w przypadku odmowy procedury debugo­ wania przez serwer, jest sprawdzenie, czy na serwerze Web zainstalowana została usługa IIS. Aby tego dokonać, nalciy otworzyć stronę

http:!!locaihost!localstart.asp w przeglądarce (host

lokalny

stanowi alias dla biciącego komputera) . Jeśli nie pojawi się strona testowa, ush1ga I I S nie zosta­ nie zainstalowana na serwerze Web lub jest niedostępna. Można równici spróbować uruchomić aplikację Web bez usuwania błędów wybierając Debug I Start Without Debugging z głównego menu systemu Visual Smdio .NET. Jeśli ten test zakończy się pomyślnie, usługa I I S jest pra­ widłowo zainstalowana. Jeśli usługa I I S zostanie zainstalowana później , niż Visual Studio .NET lub .NET Frame­ work, można spróbować „naprawy" .NET Framework za pomocą orygi nalnego dysku insta­ lacyjnego CD lub DVD. W tym celu nalciy wpisać podane niżej polecenie w linii poleceń {lub w oknie Run}. używając dysku DVD Visual Studio .NET {linia ta jest podzielona na dwa wiersze z powodu ograniczeń formatu strony, ale musi być wpisana jako pojedyncza).

: \wcu\dotNetF ramewo rk\dotnet fx . exe /t : c : \temp / c : "ms iexec . exe /fvecms c : \temp\netfx . ms i " Przy wersj i CD Visual Studio .NET nalciy zasrosować następującą linię poleceń dla dysku Windows Component Update:

: \dotNetF ramewo rk\dot netfx . exe /t : c : \temp / c : " ms iexec . exe /fvecms c : \temp\netfx . ms i " Po sprawdzeniu instalacji I IS , następny krok stanowi test poprawności pliku Web.config dla tej aplikacj i Web. Web.config powinien mieć strukturę pokazaną niżej:



< ! - - Inne ustawienia pominięto . - - >

compilation do automarycznie genero­ debug włączonym na true.

Domyślnie system Visual Studio .NET dołącza etykietę wanego pliku Web.config, z atrybutem

Kolejny krok stanowi weryfikacja konfiguracji I I S . Problemy występują zazwyczaj, jeśli nie zostanie utworzony wymagany wirtualny katalog aplikacji albo gdy nastąpi próba uruchomienia aplikacji Web po usunięciu lub modyfikacji wirtualnego katalogu. Aby to skorygować, nalciy

zmodyfikować ustawienia wirtualnego katalogu w programie obsługi US (menu Start I Control Panel I Administrative Tools

I

I nternet I nformation Services) . Należy równici zweryfikować

fakt, czy wirtualny katalog aplikacji istnieje i jest skonfigurowany jako aplikacja Web (ustawie­ nia wirtualnego katalogu można obejrzeć klikając katalog prawym przyciskiem myszy i wybiera­ jąc Properties). Na przykład na ekranie poniżej (rysunek

7-9)

katalog wirtualny iscnieje, ale nie

jest skonfigurowany jako aplikacja Web. Aby rozwiązać ten problem, nalciy po prostu kliknąć przycisk Create w sekcji Application Settings.

Rozdział 7: ASP.NET i formularze sieciowe Web 217

'? � I Vltuo/ Di'ect"'l' • D�. 1>-���'f!.!�P H_� n.....n ��� lhe oorteril """*l come lrom: 1 'Whon canocmv to0Ila A �ecl0f)l toc«od on 1111 "°""""" I 0 A Jhare localod on onother "°"""""

I MStocks I Propor11cs

1...uce.

l.o!;alPallt

O A 1edreclion to a lJ_RL

;_§'�����':,_,! � .�] I

8-„

0 S� IOUIC8 accooo EJ Log�· E]JndolclM 1.....,ce ElBoad O !ti!it• O p_�"'l' il



Można również poinstruować aplikację Web co do użycia tożsamości uwierzytelnionej przez usługę IIS. Jeśli nie jest używane uwierzytelnienie Windows (omówione w przepisie 7.8), użyte zostanie anonimowe konto IUSR_[ServerName] . W tym celu należy po prostu dołączyć ety­ kietę bez dostarczania poświadczeń użytkownika:

Należy pamiętać, że przy tego typu personifikacji wybrane konto użytkownika wymaga dostępu z prawem do odczytu i zapisu (read/write) do katalogu Temporary ASP.NET, w którym umiesz­ czane są skompilowane pliki ASP.NET. Domyślna lokalizacja tego katalogu to o/owindiro/o\ Microsoft.NET\Framework\ [version] \Temporary ASP.NET Files. Można również zrealizować personifikację programowo, aby zmienić konto użyte do wykona­ nia określonego fragmentu kodu. Temat ten zostanie bardziej szczegółowo omówiony w przepisie 16. 1 5. Poniższy kod przedstawia przykład takiego działania w połączeniu z uwierzytelnieniem Windows. Zakładając, że usługa IIS uwierzytelniła użytkownika (przepis 7.8), przyjmowana będzie tożsamość tego użytkownika, gdy zostanie użyta metoda WindtJwsldentity.lmpersonate. W celu wykorzystania tego kodu należy zaimportować przestrzeń nazw System.Security.Principal.

220 C#

-

Księga przykładów

if ( Use r . GetType ( ) == typeof (WindowsP rincipal ) ) { Windows identity id = (Windows identit y ) U se r . Identit y ; Windowsimpe rsonationContext impe rsonate = id . Impe rsonate ( ) ;

li (Wykonaj zadania p rzy użyciu personifikowanego I D . ) li Powrót do o ryginal nego ID . impe rsonate . Undo ( ) ; } else {

li Użyt kownik nie j est uwierzytel niony . }

8 Grafika, multimedia i dru kowanie Grafika, wideo, dźwięk i drukowanie stanowią wyróżniki tradycyjnych, rozbudowanych pro­ gramów klienckich systemu operacyjnego Microsoft Windows. Jeśli chodzi o multimedia, Microsoft .NET Framework zapewnia kompromis, udostt;pniając niektóre cechy, ale ignorując inne. Można tu na przykład znaleźć zestaw zaawansowanych narzędzi do dwuwymiarowych rysunków łub druku w oparciu o zdarzenia, korzystających z GOI+ i rypów należących do prze­ strzeni nazw System.Drawing. Te klasy opakowują własne funkcje Graphics Device Interface (GDI) należące do Windows API i ułatwiają wykreślanie złożonych kształtów, pracę ze współ­ rzędnymi i transformatami oraz przetwarzanie obrazów. Z drugiej strony - jeśli chcesz dokonać nagrania dźwięku, obejrzeć plik wideo łub uzyskać informacje o drukowanym aktualnie zada­ niu, będziesz musiał poszukać rozwiązania spoza .NET Framework. Niniejszy rozdział przedstawia przepisy umożliwiające zastosowanie wbudowanych cech .NET, a tam gdzie to niezbędne - lokalnych bibliotek Win32 za pośrednictwem mechani­ zmów P/Invoke łub COM. Niektóre te techniki znajdziesz w wyszczególnionych niżej tematach niniejszego rozdziału: •

Wyszukiwanie i użycie czcionek (przepis 8. 1 ), rysowanie przewijalnych obrazów (przepis 8.5) i miniaturek (przepis 8.8) oraz wykonywanie zrzutów ekranu przy użyciu Win32 API (przepis 8.6).



Metody pracy z kontrolkami utworzonymi przez użytkownika (przepisy 8.3 i 8.4) oraz manipulowanie obiektami graficznymi na ekranie (przepisy 8.2 i 8.7).



Odtwarzanie audio i wideo, w tym plików WAY, MP3, i MPEG, przy użyciu biblioteki typów Quartz wbudowanej w Windows Media Player (przepisy 8.9, 8. 1 0 i 8 . 1 1 ) .



Drukowanie prosrych i złożonych dokumentów (przepisy 8. 1 3 i 8 . 1 4), z wykorzystaniem zawijania tekstu (przepis 8. 1 5), przygotowywanie wydruków próbnych (przepis 8. 1 6) , prze­ glądanie informacji o drukarkach (przepis 8. 1 2) i o kolejkach wydruku przy użyciu WMI (przepis 8. 1 7) .

222

C#

-

Księga przykładów

8.1 Odnajdywanie wszystkich zainstalowanych czcionek Problem Chcesz znaleźć listę wszystkich czcionek zainstalowanych na danym komputerze.

Rozwiązanie Utwórz nową instancję klasy obiektów

FontFamily,

System.Drawing. Text .lnstalkdFontColkction,

zawierającą zbiór

reprezentujących wszystkie zainstalowane czcionkj.

Omówienie Klasa

lnstalkdFontColkction

umożliwia wyszukiwanie informacji o aktualnie zainstalowanych

czcionkach. Poniższy kod przedstawia formularz dokonujący iteracji na zbiorze czcionek po jego utworzeniu. Za każdym razem, gdy czcionka zostaje znaleziona, tworzona jest nowa etykieta wyświelająca nazwę czcionki w danym kroju (w rozmiarze czternastu punktów). Ta etykieta wstaje dodana do przewijanego panelu, umożliwiając użytkownikowi przeglądanie listy dostęp­ nych czcionek.

us ing System ; u s ing System . Windows . Fo rms ; u s ing System . D rawing ; u s ing System . D rawing . Text ; public class ListFonts : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . Panel pnl Font s ;

li ( Kod p roj ektanta pominięt o . ) p rivate void ListFont s_Load ( obj ect sende r , System . EventArgs e ) {

li Two rzenie kolekcj i c zcionek . I n stalledFontCol lection font Families = new I n stalledFontCollection ( ) ;

li Wyliczenie wszystkich rod zin c z c ionek . int offset = ie ; fo reach ( FontFamily family in font Families . Familie s ) { t ry {

li Utwo rzenie etykiety wyświetlaj ącej tekst daną c z c ionką . Label fontLabel = new Label ( ) ; fontLabel . Text = famil y . Name ; fontLabel . Font = new Font ( family , 14 ) ; font Label . Left = 10 ; fontLabel . Width = pnlFont s . Width ; font Label . Top = offset ;

Rozdział 8: Grafika, multimedia i drukowanie 223

li

Dodanie etykiety do p rzewij al nego panel u .

pnl Font s . Cont rol s . Ad d ( fontLa bel ) ; offset += 30 ; } catch {

li li

J eżeli wyb rana c z c ionka n ie obsługuj e stylu normal , wystąpi błąd . P roblem ten można z igno rować bez ryzyka .

} } } }

Rysunek 8- 1 przedstawia zrzut ekranu przy wykonaniu tej prostej aplikacji testowej .

QLQ]�

� list Fonts Q°()adltaY ET Calisto MT Cal!igraph.p.1 BT CA)""fELLAR C11rn11tC1 BT

Century Gothic Centwy Schoolbook ChelthmITC Bk BT Chl�r

Collage

Colonna. MT

Comic Sans MS Rysunek 8-1

Lista zainstalowanych czcionek.

224

C#

-

Księga przykładów

8.2 Wykonanie testu trafienia dla konturów Problem Chcesz sprawdzić, czy użytkownik klika wewnątrz konturu.

Rozwiązanie Przetestuj punkt, w którym kliknął użytkownik, metodą typu Rectangle. Contains lub Region. IsVisible (w przestrzeni nazw System.Drawing), albo GraphicsPath.lsVisible (w przestrzeni nazw System.Drawing.Drawing2D), zależnie od typu konturu.

Omówienie Przy korzystaniu z GOI+ do rysowania konturów w formularzu często zachodzi potrzeba okre­ ślenia, kiedy użytkownik klika w obrębie danego konturu . . NET Framework udostępnia trzy metody pomocne przy tym zadaniu: •

Metoda Rectangle. Contains, pobierająca punkt i zwracająca wartość true, gdy punkt znajduje się wewnątrz danego prostokąta. W wielu wypadkach można wybrać prostokąt dla kształtu innego typu. Możesz na przykład użyć Image. GetBounds do wyszukania niewidocznego pro­ stokąta przedstawiającego granice obrazu. Element strukturalny Rectangle jest członkiem przestrzeni nazw System.Drawing.



Metoda GraphicsPath.!s Visible, pobierająca punkt i zwracająca wartość true, gdy punkt znaj­ duje się wewnątrz obszaru zdefiniowanego przez zamkniętą ścieżkę GraphicsPath. Ponieważ GraphicsPath może zawierać liczne linie, kształty i rysunki, ten sposób jest użyteczny wtedy, gdy chcesz sprawdzić, czy punkt zawiera się wewnątrz obszaru nie będącego prostokątem. Klasa GraphicsPath należy do przestrzeni nazw System.Drawing.Drawing2D.



Metoda Region.lsVisible, pobierająca punkt i zwracająca wartość true, gdy cen punkt znaj­ duje się wewnątrz obszaru zdefiniowanego przez Region. Podobnie jak GraphicsPath, Region może reprezentować złożony, nieprostokątny kontur. Region należy do przestrzeni nazw

System.Drawing. Poniższy przykład pokazuje formularz, który tworzy Rectangle i GraphicsPath. Domyślnie oba ce kształty mają jasnoniebieskie cło. Program obsługi zdarzc11, który odpowiada na zdarzenie Form.MouseMove, sprawdza, czy wskaźnik myszy znajduje się na którymś z tych kształtów; jeśli tak - zmienia tło na kolor jaskrawo różowy. u s ing System ; us ing System . Windows . Fo rms ; us ing System . D rawing ; using System . D rawing . D rawing2D ; public class HitTe sting : System . Windows . Fo rms . Fo rm {

li ( Kod p roj ektanta pominięto . )

Rozdział 8: Grafika, multimedia i drukowanie 225 li Defin iowanie kształtów użytych w f o rmula rz u . p rivate G raphicsPath pat h ; p rivate Rectangle rectangle ;

li Defin iowanie f lag śledzących położenie ku rsora myszy . p rivate bool inPath = fals e ; p rivate bool inRectangle = false ;

li Definiowanie pędzli służących do malowania kształtów . B rush highlight B ru s h = B rushes . HotPin k ; B ru s h default B r u s h = Brushes . LightBlue; p rivate void HitTest ing_Load ( obj ect send e r , System . EventArgs e ) {

li Two rzenie kształtów . path = new G raphicsPat h ( ) ; pat h . Add Ellipse ( l0 , 10 , 100 , 6 0 ) ; path . AddCu rve ( new Point [ ] { new Point ( 5 0 , 50 ) , new Point ( l0 , 33 ) , new Point ( 80 , 43 ) } ) ; pat h . AddLine ( 5 0 , 120 , 250 , 80 ) ; pat h . Addline ( l20 , 40 , 110 , 5 0 ) ; pat h . CloseFigu re ( ) ; rectangle = new Rectangle ( l00 , 170 , 220 , 120 ) ; } p rivate void HitTesting_Paint ( ob j ect sende r , System . Windows . Fo rms . PaintEventArgs e ) { G raphics g = e . G raphics ;

li Malowanie kształtów stosowanie do bieżącego wybo ru . if ( inPat h ) { g . FillPa t h ( h ighlightBrush , path ) ; g . FillRectangle ( defau l t B ru s h , rectangle ) ; } else if ( inRectangl e ) { g . FillRectang l e ( highlig h t B ru s h , rectangle ) ; g . FillPath ( defaultBrush , path ) ; } else { g . FillPath ( defaultBrush , path ) ; g . FillRectangle ( default B ru s h , rectangle ) ; } g . D rawPat h ( Pens . Blac k , path ) ; g . D rawRectangle ( Pen s . Blac k , rectangle ) ; } p rivate void HitTest ing_MouseMove ( obj ect sende r , System . Windows . Fo rms . MouseEventArgs e ) { G raphics g = this . C reateGraphic s ( ) ;

li Wykonanie sp rawdzenia t rafienia dla p rostokąta . if ( rectang l e . Contain s ( e . X , e . Y ) ) { if ( ! inRectangl e ) {

226

C#

-

Księga przykładów inRectangle = t ru e ;

li Pod świetlenie p rostokąta . g . FillRectangle ( h ighlight B rush , rectangle ) ; g . D rawRectangle ( Pen s . Black , rectangle ) ; } } else if ( inRectang l e ) { inRectangle = false ;

li Odtwo rzenie niepodświetlonego p rostokąt a . g . FillRectang le( defa u l t B ru s h , rectangl e ) ; g . D rawRectangle ( Pen s . Bla c k , rectangle ) ; }

li Wykonanie testowania t rafienia ścież ki . if ( path . I sVisible ( e . X , e . Y ) ) { if ( ! inPath ) { inPath

=

t rue ;

li Podświetlenie ścieżki . g . FillPat h ( highlight B rush , pat h ) ; g . D rawPat h ( Pens . Black , path ) ; } } else if ( in Path ) { inPath

=

fal s e ;

l i Przywrócenie niepodświetlonej ścieżki . g . FillPath ( defaul t B ru s h , path ) ; g . D rawPat h ( Pens . Blac k , pat h ) ; } g . Dispose ( ) ; } } Warto zauważyć, że operacja podświetlenia zachodzi bezpośrednio wewnątrz programu obsługi zdarzeń

MouseMove.

Kolorowanie ma miejsce tylko wtedy, gdy zmienił się aktualny wybór.

W celu uproszczenia kodu można w1ieważnić cały formularz za każdym razem, gdy wskaźnik myszy jest przesuwany do wewnątrz i na zewnątrz obszaru i obsługiwać cały rysunek w progra­ mie obsługi zdarzeń

Form.Paint,

ale co podejście wymaga większej liczby operacji rysowania

i generuje dodatkowe migotanie w trakcie powtórnego barwienia całego formularza. Rysunek 8-2 przedstawia działanie aplikacj i.

Rozdział 8: Grafika, multimedia i drukowanie 227

Rysunek 8-2 Test trafiania w zastosowaniu do obiektów Rectangle i GraphicsPath.

8.3 Utworzenie kontrolki o nieprostokątnym kształcie Problem Chcesz utworzyć formularz lub kontrolkę o innym kształcie niż prostokąt.

Rozwiązanie Utwórz nowy obiekt System.Drawing.Region, którego kształt jest taki, jaki chciałbyś nadać for­ mularzowi i przypisz go do właściwości Form.Region lub Control.Region.

Omówienie Aby utworzyć nieprostokątny formularz lub kontrolkę, musisz najpierw zdefiniować pożądany kształt. Najprościej jest wykorzystać obiekt System.Drawing.Drawing2D. GraphicsPath, który może dopasowywać dowolne kombinacje elips, prostokątów i zamkniętych krzywych. Możesz dodawać kształty do instancji GraphicsPath przy użyciu takich metod jak AddElslip e, AddRec­ tangle i AddClosedCurve. Po skończonym definiowaniu kształtu możesz utworzyć obiekt Region z tejże ścieżki GraphicsPath po prostu zgłosić GraphicsPath w konstruktorze klasy Region. Na zakończenie należy przypisać Region do właściwości Form.Region lub ControLRegion. W poniższym przykładzie nieregularnie ukształtowany formularz (pokazany na rysunku 8-3) został utworzony przy użyciu dwóch krzywych złożonych z wielu punktów, które w wyniku konwersji zostały przekształcone w figurę zamkniętą przy użyciu metody GraphicsPath. Close­ -

AllFigures. using System ; using System . Windows . Fo rms ;

228 C#

-

Księga przykładów

u sing System . D rawing ; u sing System . D rawing . D rawing2D ; public cla s s I rregularForm : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . Button cmdClose ; p rivate System . Windows . Fo rms . Label label l ;

li ( Kod p roj ektanta pominięto . ) p rivate void I rregula rForm_Load (obj ect sende r , System . EventArgs e ) { G raphicsPath path = new G raphicsPat h ( ) ; Point [ ) pointsA = new Point [ ) { new Point ( 0 , 0 ) , new Point ( 40 , 60 ) , new Point ( t his . Width - 100 , 10 ) } ; path . AddCu rve ( pointsA ) ; Point [ ) pointsB = new Point [ ) { new Point ( th i s . Width - 40 , t h i s . Height - 6 0 ) , new Point ( th i s . Width , this . Height ) , new Point ( l0 , thi s . Height ) } ; path . AddCu rve ( point sB ) ; pat h . CloseAl lFig u re s ( ) ; this . Region = new Region ( path ) ; } p rivate void cmdClose_Click ( obj ect sende r , System . EventArgs e ) { this . Close ( ) ; } }

An Irregula a Form

Rysunek 8-3

Nieprostokątny formularz.

Przykład nieprosrokątnej konrrolki zawiera przepis 8.4.

Rozdział 8: Grafika, multimedia i d rukowanie 229

8.4 Utwórz ruchomego „duszka" Problem Chcesz utworzyć ksztah, który użytkownik może przekształcać na formularzu, np. poprzez przeciąganie go, zmianę wymiarów lub dzięki innym oddziaływaniom interakcyjnym.

Rozwiązanie Utwórz kontrolkę użytkownika i zastąp logikę rysowania, aby wykreślić ksztah. Przypisz kształt do właściwości

Control.Region.

Teraz możesz wykorzystać ten

Region

do wykonania

testu trafienia.

Omówienie Chcesz utworzyć złożony interfejs użytkownika, który zawiera liczne elementy o niestandardowych ksztahach. Potrzebny jest sposób śledzenia tych elementów, a także metoda interakcji użytkownika z nimi. Najprostszą metodą w .NET jest utworzenie dedykowanej kontrolki poprzez wyprowadze­ nie klasy z System. Winduws.Forms. Control Wtedy możesz dostosować sposób rysowania kontrolki w oparciu o podstawowy zestaw wywoływanych przez nią warzeń. Poniższy przykład pokazuje kontrolkę, reprezentującą ksztah elipsy na formularzu. Wszyst­ kie kontrolki są związane z prostokątnym obszarem na formularzu, dzięki czemu kontrolka

EllipseShape wygeneruje elipsę, która wypełni te granice {dostarczone przez właściwość Con­ trol. ClientRectangk). Kiedy ksztah został już wygenerowany, właściwość ControLRegion wstaje ustawiona zgodnie z obramowaniem elipsy. Spowoduje to, że takie warzenia, jak MouseMove, MouseDQWn, Click itd. wystąpią tylko wtedy, gdy mysz znajdzie się nad elipsą, a nie tylko nad całym prostokątem klienta. Pełen kod

EllipseShape jest pokazany niżej :

using System ; using System . Windows . Fo rms ; using System . D rawing ; using System . D rawing . D rawing20 ; public c l a s s EllipseShape : System . Windows . Fo rms . Cont rol { p rivate G raphics Path path = null ; p rivate void Ref reshPath ( ) {

li Utwo rzenie G raphicsPath dla kształ t u . path = new G raphicsPat h ( ) ; path . AddEllipse ( th i s . Cl ientRectangle ) ; this . Region = new Region ( path ) ; } p rotected ove r ride void OnRes ize ( System . EventA rgs e ) { bas e . OnResize ( e ) ; Ref reshPath ( ) ;

230 C#

-

Księga przykładów thi s . I nval idate ( ) ; } p rotected ove r ride void OnPaint ( System . Windows . Fo rms . PaintEventArgs e ) { ba se . OnPaint ( e ) ; if ( path ! = null ) { e . G raphic s . Smoot hingMode = SmoothingMod e . AntiAlia s ; e . G raphics . Fil lPat h ( new Solid B ru sh ( this . BackColo r ) , path ) ; e . G raphics . D rawPat h ( new Pen ( t h i s . Fo reCol o r , 4 ) , path ) ; } }

}

Możesz zdefiniować kontrolkę EliipseShape w oddzidnej asemblacji biblioteki klas, żeby mogła zostać dodana do Microsoft Visual Studio .NET Toolbox, a następnie wykorzystana pod­ czas projektowania. Jednakże nawet bez takich działań łatwo można utworzyć prostą aplika­ cję testującą. Pokazany niżej formularz Windows tworzy dwie elipsy, a następnie umożliwia użytkownikowi przeciąganie ich dokoła formularza poprzez zwykłe wciśnięcie przycisku myszy i przesuwanie wskaźnika. public class Sp riteTest : System . Windows . Fo rms . Form {

li ( Kod p roj ektanta pominięt o . ) li śledzenie t rybu ciągnięcia . p rivate bool isDraggingA = fals e ; p rivate bool isDraggingB = false ; p rivate EllipseShape ell ipseA , ellipseB ; p rivate void Sp riteTest_Load ( obj ect sende r , System . EventArgs e ) {

li Two rzenie i skonfigu rowanie obu elips . ellipseA = new E l l ipseShape ( ) ; ell ipseA . Width = ellipseA . Height = 100 ; ell ipseA . Top = ell ipseA . Left = 30 ; ellipseA . BackColor = Colo r . Red ; this . Cont rol s . Add ( el l ipseA ) ; el lipseB = new Ell ipseShape ( ) ; el lipseB . Width = ell ipseB . Height = 100 ; ellipseB . Top = ell ipseB . Left = 130 ; ell ipseB . BackColo r = Col o r . Az u re ; this . Con t rol s . Add ( ellipseB ) ;

li Dodanie obu elips do tego samego zestawu obsługi zda rzeń . ell ipseA . MouseDown += new MouseEventHandle r ( El l ipse_MouseDown ) ; ell ipseA . MouseUp += new MouseEventHandle r ( Ellipse_MouseUp ) ; ell ipseA . Mou seMove += new MouseEventHandle r ( Ellipse_MouseMove ) ; ell ipseB . Mou seDown += new MouseEventHandle r ( El l ipse_MouseDown ) ; ell ipseB . MouseUp += new MouseEventHandl e r ( El l ipse_MouseUp ) ; ellipseB . MouseMove += new MouseEventHandl e r ( El l ipse_MouseMove ) ; }

Rozdział 8: Grafika, multimedia i drukowanie 231 p rivate void E l l ipse_Mou seDown ( o b j ect sende r , Mou seEventA rg s e) { Cont rol cont rol = ( Cont rol ) sende r ; i f ( e . Button == MouseButton s . Left ) { cont rol . Tag = new Point ( e . X , e . Y ) ; if ( cont rol == ellipseA ) { isD raggingA = t rue ; } e l se { isD raggingB = t ru e ; } } } p rivate void Ell ipse_Mou seUp ( obj ect send e r , MouseEventA rgs e ) { isD raggingA = false ; isD raggingB = false ; } p rivate void Ellipse_Mou seMove ( obj ect send e r , Mou seEventA rg s e) { Cont rol cont rol = ( Cont rol ) sende r ; if ( ( i s D raggingA && cont rol == ellipseA) I I ( is D raggingB && cont rol == ellipseB ) ) { Point point = ( Point ) cont rol . Tag ;

li P rzesunięcie kont rol ki . cont rol . Left = e . X + cont rol . Left - point . X ; cont rol . Top = e . Y + cont ro l . Top - point . V ; } } } Rysunek 8-4 pokazuje przeciąganie elipsy przez użytkownika.

Rysunek 8-4

Przeciąganie kontrolek na formularzu jako kształtów użytkownika.

232

C#

-

Księga przykładów

8.5 Utworzenie obrazu dającego się przewijać Problem Chcesz utworzyć obraz o dynamicznej zawartości, dający się przewijać.

Rozwiązanie Wykorzystaj automatyczne właściwości przewijania kontrolki System. Windows.Fonns.Panel poprzez ustawienie PaneLAutoScroll na true i umieszczenie wewnątrz kontrolki Panel obiektu System. Windows.Forms.PictureBox z zawartością obrazu.

Omówienie Kontrolka Panel ma wbudowaną funkcję przewijania. Jeśli umieścisz na niej jakieś kontrolki, tak by wystawały poza jej granice i ustawisz wartość Panel.AutoScroll na true, na panelu pojawią się paski przewijania, które umożliwią użytkownikowi poruszanie się po jego zawartości. Ten mechanizm działa zazwyczaj dobrze dla dużych obrazów. Możesz załadować lub utworzyć obraz w pamięci, przypisać do niego pole obrazu (nie dysponujące wewnętrznymi możliwościami przewijania), a następnie wyświetlić je wewnątrz panelu. Jedynym wymogiem jest tutaj ustawie­ nie wymiarów pola obrazu równych całkowitej wielkości obrazu, który chcesz przedstawić. Poniższy przykład utworzy obraz przedstawiający dokument. Obraz wstał wygenerowany w pamięci jako bitmapa, a kilkanaście linii tekstu zostało dodanych przy użyciu metody Grrzphics. DrawString. Następnie obraz został ograniczony do okna obrazu wyświetlanego w panelu prze­ wijania, co wstało pokazane na rysunku 8-5. us ing System ; u s ing System . Windows . Fo rms ; u s ing System . D rawing ; public c l a s s Pictu reSc roll : System . Windows . Fo rms . F o rm { p rivate System . Windows . Fo rms . Pictu reBox pictu reBox l ; p rivate System . Windows . Fo rms . Panel panel l ;

l i ( Kod p roj ektanta pominięto . ) p rivate void Pictu reSc roll_ Load ( obj ect sende r , System . EventArgs e ) { st ring text = "The quick b rown fox j umps ove r the lazy dog . " ; Font font = new Font ( "Tahoma " , 28 ) ;

li Two rzenei bitmapy w pamięc i . Bitmap b = new Bitmap ( 608 , 608 ) ; G raphics g = G raphic s . F romimage ( b ) ; g . FillRectangle ( B rushes . Wh i t e , new Rectangle ( 0 , 8 , b . Widt h , b . Height ) ) ;

li Rysowanie kil ku lini tekstu na bitmapie . fo r ( int i=0 ; i < 1 0 ; i++ ) {

Rozdział 8: Grafika, multimedia i d ru kowanie 233 g . D rawSt ring ( text , font , B ru shes . Bl a c k , 50 , 50 + i*60 ) ; }

li Wyświetlenie bitmapy . pictu reBoxl . Backg roundimage = b ; pictu reBoxl . Size

=

b . Size ;

} }

The q u ick brown

�,

The q u ick brown The q u ick brown _ : The q u ick brown Rysunek 8-5

Dodanie cechy przewijania do niestandardowej zawartości.

8.6 Wykonanie zrzutu ekranu Problem Chcesz wykonać zrzut bieżącego pulpitu.

Rozwiązanie Wykorzystaj Win32 API, wywołując GetDesktop Window, GetDC i RekaseDC z user32.dll. Użyj ponadto GetCurrentObject z gdi32.dłl.

Omówienie .NET Framework nie udostępnia żadnych klas, które można by wykorzystać do wykonania zrzutu cał�o ekranu (zwanego często oknem pulpitu). Możesz jednakże mieć dostęp do tych cech, używając mechanizmu P/lnvoke z Win32 API. Pierwszym krokiem jest ucworzenie klasy zawierającej funkcje Win32 API, które chcesz wykorzystać. Poniższy przykład przedstawia klasę deklarującą te funkcje i wykorzystanie ich w metodzie public Capture, aby zwrócić obiekt .NET Image, zawierający okno pulpitu. using System ; using System . D rawing ;

234

C#

-

Księga przykładów

u sing System . Runtime . In t e ropSe rvice s ; us ing System . Windows . Fo rms ; public cla s s DesktopCapt u re { [ Dll impo rt ( " u s e r32 . dl l " ) J p rivate extern static I ntPt r GetDesktopWindow ( ) ; [ Dl l impo rt ( " u s e r32 . d ll " ) J p rivate extern static I ntPt r GetDC ( I ntPt r windowHandle ) ; [ Dll impo rt ( "gdi32 . dll " ) J p rivate exte rn static Int Pt r GetC u r rentOb j ect ( In t Pt r hdc , u s h o rt objectType ) ; [ Dllimport ( "u s e r32 . dl l " ) J p rivate extern static void ReleaseDC ( I ntPt r hdc ) ; const int OBJ_ BITMAP

7;

=

public static Bitmap Capt u re ( ) {

li

Pobranie kontekstu u rządzenia dla okna pulpit u .

IntPt r desktopWindow

=

GetDes ktopWindow ( ) ;

IntPtr desktopDC = GetDC ( desktopWindow ) ;

li

Pobranie uchwytu GOI dla o b raz u .

I nt Pt r desktopBitmap = Get C u r rentObj ect ( desktopDC , OBJ_BITMAP ) ;

li

Wyko rzystanie uchwytu do utwo rzenia obiektu . NET Image .

Bitmap desktopimage

li

=

Image . F romHbitmap ( desktopBitmap ) ;

Zwolnienie kontekstu u rządzenia i powrót do o b ra z u .

ReleaseDC ( de s ktopDC ) ; ret u rn desktopimage ; } }

Następnym krokiem jest utworzenie aplikacji klienckiej, która wykorzysta tę funkcjonalność. Poniższy kod tworzy formularz uwidoczniony na rysunku 8-6, który wyświetla przechwycony obraz w polu znajdującym się wewnątrz przewijanego panelu {przepis 8.5). public class E k ranCaptu re : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . Pictu reBox pictu reBox l ; p rivate System . Windows . Fo rms . Panel panel l ;

li

( Kod p roj ektanta został pominięty . )

p rivate void cmdCaptu re_C l ic k ( obj ect sende r , System . EventArgs e ) { pict u reBox l . Image = DesktopCapt u re . Capt u re ( ) ; pictu reBox l . Size = pictu reBoxl . Image . Size ; } }

Rozdział 8: Grafika, multimedia i drukowanie 235 ::'.� Screen (dpture My Cof11Xter 3'11 Aoppy (A:) · � Applcatlons (C:) Documenl:s (O:) -· .._ Temp(E:) ...:,l. D'YO Drive (F:) .. ,;l. C!HlW Drive (G:) Forlo on 'Forlo' (Z:) cf) jj- Corool Panel

:-:;-!@LR A._

ib

A

. b

: u

D

--

Ct

L:i D

cai-e

I

Rysunek 8-6 Wykonywanie zrzutu zawartości ekranu.

8. 7 Zastosownie podwójnego buforowania w celu przyspieszenia przerysowywania Problem Chcesz wptymalizować wykreślanie często odnawianego formularza i zredukować migotanie.

Rozwiązanie Zapisz grafikę jako bitmapę w pamięci, a następnie skopiuj ją do formularza.

Omówienie W niektórych aplikacjach musisz często przemalowywać formularz lub kontrolkę. Jest to typowy przypadek przy wykonywaniu animacji. Możesz na przykład używać timera, żeby co sekundę unieważniać swój formularz. Kod rysowania może następnie odtwarzać obraz w nowym poło­ żeniu, stwarzając iluzję ruchu. W tym wypadku pojawia się następujący problem: za każdym razem, gdy unieważniasz formularz, Windows przemalowuje tło okna (czyszcząc formularz), a następnie uruchamia kod, który wyrysowuje grafikę - element po elemencie. Może to powo­ dować znaczne migotanie ekranu. Technika Double buffering umożliwia zminiejszenie migotania. W tym wypadku logika rysowania zapisuje bitmapę w pamięci wewnętrznej, która jest pod koniec operacji rysowania kopiowana do formularza, jako pojedyncze, ciągłe przemalowywanie. Dzięki temu migotanie zostaje znacznie zredukowane. Pierwszym krokiem przy implementacji podwójnego buforowania jest zapewnienie, że tło formularza nie będzie przemalowywane automatycznie po unieważnieniu formularza. To auto­ matyczne czyszczenie jest główną przyczyną migotania, ponieważ wymienia obraz na białą

236

C#

-

Księga przykładów

ramkę (nawec jeśli crwa co cylko ułamek sekundy). Aby się przed tym uchronić, należy nad­ pisać mecodę formularza OnPaintBackground w cakj sposób, żeby nie zoscało przedsięwzięce żadne działanie. Nascępnym krokiem jest zmiana kodu malowania cak, żeby zapisywał obraz w pamięci wewnęcrznej w poscaci bitmapy. Komplecna bicmapa zostanie nascępnie skopiowana do formularza. Ta metoda sprawia, że odświeżanie stanowi ceraz pojedynczą operację przemalo­ wania, a czasochłonna realizacja logiki wykreślania nie powoduje dodackowego migotania. Poniższy przykład pokazuje animację obrazu (w danym wypadku chodzi o logo Windows), który na scronie raz się powiększa, raz zmniejsza. Logika wykreślania jesc wykonywana w ramach menagera zdarzeń Form. Pain, a timer unieważnia formularz co 10 msek, dzięki czemu obraz może być przerysowany ponownie. W gestii użytkownika leiy decyzja, czy uruchomić podwójne buforowanie z okna check box na formularzu, czy nie. Bez buforowania formularz zauważalnie migocze. Kiedy zoscaje ono włączone, obraz się powiększa i kurczy w gładkiej, wolnej od migo­ cania animacji. u s ing System ; u sing System . D rawing ; u s ing System . D rawing2 D ; u s ing System . Windows . Fo rms ; public class DoubleBu ffe ring : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . CheckBox chkUseDoubleB u f f e rin g ; p rivate System . Windows . Fo rms . Time r tm rRef res h ;

l i ( Kod p roj ektanta został pominięty . ) li Ś ledż rozmiar o b razu i typ animacj i li ( rozszerzanie się lub ku rczenie ) . p rivate bool isSh rinking = fals e ; p rivate i n t imagesize = e ;

l i Zapamiętaj logo , któ re ma być namalowane n a formula rzu . p rivate Image image ; p rivate void DoubleBu f f e ring_ Load ( obj ect sende r , System . EventA rg s e ) {

li Załaduj z pliku ob raz logo . image = Image . F romFile ( " image . bmp " ) ;

li U ruchom t ime r , który u n ieważnia f o rmu l a rz . tmrRef resh . St a rt ( ) ; } p rivate void tmrRef resh_Tic k ( obj ect sende r , System . EventArgs e ) {

li Zmień wymagany rozmia r o b razu stosownie do t rybu animacj i . if ( isSh rinking ) { imagesize - - ; } else { imagesize ++ ; }

li Zmień kie runek zmiany rozmia ru , j eś l i rozmia r z bl iża się do li g ranicy f o rmu l a rza . if ( imagesize

>

( th i s . Width

-

150 ) ) {

Rozdział 8: Grafika, multimedia i dru kowanie 237 isSh rinking = t rue ; } else if ( imagesize < 1 ) { isSh rinking = f a l s e ; }

li P rzemaluj f o rmul a rz . t h i s . I nvalidate ( ) ; } p rivate void Dou bleBu f fe ring_ Paint ( obj ect sende r , System . Windows . Fo rms . PaintEventArgs e ) { G raphics g ; Bitmap d rawing = nul l ; if ( chkUseDoubleBu f f e ring . Checked ) {

li Podwó j ne bufo rowanie j es t używane . li Utwó rz nową bitmapę w pamięci wewnęt rznej , rep rezent uj ącą li powie rzchnię f o rmula rza . d rawing = new Bitmap ( th i s . Width , t h i s . Height , e . G raphics ) ; g = G raphics . F romimage { d rawing ) ; } else {

li Podwó j ne bufo rowan ie n ie j es t używane . li Wrysuj bezpoś rednio do f o rmula rza . g = e . G raphic s ; } g . Smoot hingMode = SmoothingMode . HighQual it y ;

li Rys uj tło . g . FillRectangl e ( B ru s hes . Yell ow , new Rectangl e ( new Point ( 0 , 0 ) , t h i s . ClientSize ) ) ;

li Rys u j ob raz logo . g . D rawimage{ image , 50 , 50 , 50 + imagesize , 50 + imagesize ) ;

li Jeśli używa s z podwój nego bufo rowania , kopiuj kompletną bitmapę li w pamięci wewnęt rznej do f o rmula rza . if ( chkUseDoubleBuffe ring . Checked ) { e . G raphics . D rawimageUnscaled ( d rawing , 0 , 0 ) ; g . Dispose ( ) ; } } p rotected ove r ride void OnPaintBackg round ( System . Windows . Fo rms . PaintEventArgs pevent ) {

li Nic nie rób . } }

238

C#

-

Księga przykładów

8.8 Pokazanie miniatur obrazów Problem Chcesz wyświetlić miniatury (zmniejswne kopie) obrazów zawarrych w danym w katalogu.

Rozwiązanie Wczytaj obraz z pliku przy użyciu statycznej metody

FromFile

klasy

System.Drawing.lmage.

Teraz możesz odtworzyć miniaturę przy użyciu metody lmage. Get1humbnaillmage.

Omówienie Klasa

Image

zapewnia funkcjonalność generowania m1111atur metodą

Get1humbnaillmage.

Należy po prostu przekazać wymaganą szerokość i wysokość miniatury (w pikselach) , a klasa

Image

utworzy nowy obiekt

Image,

spełniający dane kryteria. Przy redukowaniu wielkości

obrazu używana jest metoda anryalisingu w celu zapewnienia jego możliwie najlepszej jakości, chociaż pewne rozmycia i utrata szczegółów są nieuchronne

(antialiasing jest procesem usuwa­

nia poszarpanych brzegów, które często pojawiają się w przeskalowanej grafice, poprzez cienio­ wanie pośrednim kolorem) . Ponadto możesz zapewnić sprzężenie zwrotne z powiadomieniem, umożliwiające asynchroniczne tworzenie miniatur. Przy generowaniu miniatur ważne jest zapewnienie niezmieności proporcji. Na przykład: przy redukcji obrazu 200

x

1 00 do miniatury 50

czwartej, a wysokość do połowy,

co

x

50, szerokość zostanie zmniejswna do jednej

zniekształci obraz. Zapewnienie stałych proporcji oznacza,

że należy zmienić wysokość lub szerokość do założonego rozmiaru, a następnie dopasować pro­ porcjonalnie drugi wymiar. Poniższy przykład pokazuje odczyt bitmapy i wygenerowanie miniatury nie większej niż

50

x

50 pikseli, przy zachowaniu oryginalnej proporcj i.

u sing System ; u s ing System . D rawing ; u s ing System . Windows . Fo rms ; public class Thumbnails : System . Windows . Fo rms . F o rm {

li ( Kod p roj ektanta został pominięty . ) Image thumbnail ; p rivate void Thumbnail s_Load ( obj ect sende r , System . EventArgs e ) { Image img = Image . F romFile ( " test . j pg" ) ; int thumbnailWidt h = 0 , t h umbnailHeight = 0 ;

l i Dopa suj naj większy wymia r d o 5 0 piksel i . Zapewnia t o wielkość

/I

miniatu ry nie p rzekraczaj ącą wa rtości 50x50 piksel i .

l i Jeśli wizual i z u j e s z liczne miniat u ry , dla każdego z a rezerwuj li kwad rat 50x50 piksel i . if ( img . Width > img . Height ) { thumbnailWidth = 50 ;

Rozdział 8: Grafika, multimedia i drukowanie 239 thumbnailHeight

=

I

Convert . Tolnt32 ( ( ( 50F

img . Widt h ) *

img . Height ) ) ; } else { thumbnailHeight = 50 ; t humbnailWidth = Conve rt . Tolnt32 ( ( ( 50F

I

img . Height ) *

img . Width ) ) ; } thumbnail

=

img . GetThumbnailimag e ( th umbnailWidt h , th umbnailHeight ,

nul l , IntPt r . Ze ro ) ; } p rivate void Thumbnails_Paint ( ob j ect sende r , System . Windows . Fo rms . Paint Even tArgs e ) { e . G raphics . D rawimage ( thumbnail , 10 , 1 0 ) ; } }

8.9 Wygenerowanie prostego dźwięku 'beep' Problem Chcesz utworzyć prosty dźwięk - taki jak systemowy 'beep'.

Rozwiązanie Użyj niezarządzanej funkcji Win32 API, takiej jak Beep albo sndP/aySound lub wywołaj funkcję Beep Microsoft Visual Basic .NET.

Omówienie .NET Framcwork nic zawiera zarządzanych klas dotyczących odtwarzania plików audio, ani nawet odtwarzania dźwięków systemowych. Możesz jednak łatwo pokonać tę przeszkodę, uży­ wając Win32 API lub Visual Basic .NET, który udostępnia standardową funkcję Beep, wywo­ dzącą się z klasy Microsoft. VisualBasic.lnteraction. W drugim przypadku należy do kodu włączyć odnośnik do biblioteki Microsoft.Visua!Basic.dll (zawartej we wszystkich wersjach .NET Fra­ mework). W poniższym przykładzie zastosowano obie funkcje:

Beep API

i Visual Basic

Beep.

Warto

zwrócić uwagę, że funkcja API wykorzystuje wewnętrzny głośnik komputera i emituje ton ze

wskazaną częstotliwością (od 37 do 32,767 Hz) i czasem trwania (podanym w milisekun­

dach) . Oczywiście spowoduje to wyemitowanie dźwięku jedynie wtedy, gdy komputer

ma

wewnętrzny głośnik. Z drugiej strony funkcja Visual Basic Beep implikuje wykonanie standar­ dowego zdarzenia 'beep', zdefiniowanego przez system (powodującego odtworzenie pliku audio WAV) . Nie spowoduje to powstania dźwięku, o ile komputer nie będzie wyposażony w kartę dźwiękową, dołączoną do zewnętrznych głośników, a także wtedy, gdy system Windows zosta-

240 C#

-

Księga przykładów

nie skonfigurowany tak, by nie odtwarzał dźwięków (poprzez sekcję urządzeń dźwiękowych i audio w panelu sterującym) .

us ing System ; us ing System . Runtime . In t e ropServices ; us ing Mic rosoft . VisualBasic ; public class BeepTest { [ Dll !mport ( " ke rnel32 . dl l " ) ) p rivate static exte rn bool Beep ( int f req , int d u r ) ; [ STATh read ) p rivate static void Main ( st ring [ J a rg s ) {

li Wyemit u j ton 440 Hz p rzez 100 msek z wewnęt rznego głośn ika . Console . Writeline ( "Win32 API beep test . " ) ; Beep ( 440 , 100 ) ; Console . Readline ( ) ;

li Wyemituj dźwięk ' beep ' j ako domyślny dźwięk s y stemowy li ( plik audio WAV ) . Console . W riteline ( " VB beep test . " ) ; I n t e raction . Beep ( ) ; Console . Readline ( ) ; } } Możesz również użyć funkcji Win32 API do odtwarzania wybranego przez ciebie pliku audio. Ta technika jest przedstawiona niżej w przepisie 8. 1 O.

8.1 O Odtworzenie pliku WAY lub MP3 Problem Chcesz odtworzyć plik WAY lub MP3.

Rozwiązanie Posłuż się niezarządzaną funkcją

sndP/aySound API

dla podstawowej obsługi pliku WAY lub

użyj składnika COM ActiveMovie, włączonej w Windows Media Player, który obsługuje WAY i MP3 audio.

Omówienie Dla odtworzenia dźwięku w aplikacji .NET powinieneś uzyskać pomoc zewnętrznej biblioteki lub wywołania systemowego. Na szczęście na podorędziu są dwie łatwe opcje:

Rozdział 8: Grafika, multimedia i d ru kowanie 241 •

Plik winmm.dll, zawarcy w systemie Windows, zawiera funkcję o nazwie sndPlaySound, która akceptuje nazwę pliku WAY oraz parametr wskazujący na sposób odtwarzania. Można wybrać następujące opcje odtwarzania dźwięku: synchroniczną (przerywając wykonanie pro­ gramu do momentu zakończenia odtwarzania), asynchroniczną lub w ciągłej pętli w tle.



Biblioteka cypów Quartz udostępnia składnik COM, który umożliwia odtwarzanie plików audio różnych typów, w cym takich formatów jak WAY i MP3. Biblioteka typów Quartz jest dostarczana przez quartz.dll i stanowi część Microsoft DirectX, instalowanego razem z Windows Media Player i systemem operacyjnym Windows.

W poniższym przykładzie użyjemy drugiego sposobu. Pierwszym krokiem jest wygenerowanie klasy międzyoperacyjnej, zarządzającej oddziaływaniem między twoją aplikacją .NET, a nieza­ rządzaną biblioteką Quartz. Możesz wygenerować klasę C# zapewniającą międzyoperacyjność kodu, posługując się narzędziem Type Library Importer (tlbimp.exe) oraz linią poleceń, gdzie [WindowsDir] jest ścieżką i nstalacji systemu Windows: tl bimp [WindowsDi r ] \ system32\qua rtz . dl l l out : Qua rt zTypeLi b . d l l

Alternatywnie możesz wygenerować klasę międzyoperacyjną przy użyciu Visual Studio .NET, dodając odnośnik. Wystarczy prawym przyciskiem myszy kliknąć projekt w Solution Explorer i z menu kontekstowego wybrać Add Reference. Następnie należy wybrać ecykierę o nazwie COM i przewinąć menu w dół, aby wybrać bibliotekę sterującą ActiveMovie. Po wygenerowaniu klasy międzyoperacyjnej możesz zacząć pracować z interfejsem !Media­ Control.s. Wskaż plik, który chcesz odtwarzać przy pomocy RenderFil.e i kontroluj odtwarzanie przy użyciu metod Run, Stop i Pause. Wykonywanie odtwarzania zachodzi w oddzielnym wątku i nie blokuje kodu aplikacji. W poniższym przykładzie pokazano prostą aplikację konsoli, która odtwarza plik audio wskazany jako pierwszy argument w linii poleceń. using System ; c l a s s PlayAudio { public static void Main ( s t ring [ ] a rg s ) { li Odczytaj nazwę p l i ku wyspecyfikowaną w pierwszym pa ramet rze . st ring filename = a rg s [ 0 ] ; li Z real izuj dostęp do inte rfej s u IMediaCont rol . QuartzTypeLi b . Filg raphManage r g raphManager = new Qua rtzTypeLib . Filg raphManage r ( ) ; QuartzTypeLib . IMediaCont rol me = ( Qua rtzTypeLib . IMediaCont rol ) g raphManage r ; l i Wys pecyfikuj plik . mc . Rende rFil e ( f ilename ) ; li Zacznij odtwa rzan ie audio a s y n c h ronicz nie . me . Ru n ( ) ; Console . WriteLine ( " P ress Enter to continue . " ) ; Console . Read Line ( ) ; me . Stop ( ) ; } }

Biblioteka Quarrz umożliwia również wyświetlenie plików filmu (przepis 8. 1 1 ) .

242 C#

-

Księga przykładów

8.1 1 Przedstawienie animacji z DirectShow Problem Chcesz odtworzyć plik wideo (tak.i jak MPEG, AVI lub WMV) wewnątrz formularza Windows.

Rozwiązanie Użyj komponentu COM ActiveMovie, zawartego w Media Player. Dołącz dane wyjściowe wideo do pola obrazu na twoim formularzu poprzez ustawienie właściwości !Video Window. Owner na właściwość PictureBox.Handk.

Omówienie Chociaż .NET Framework nie zawiera zarządzanych klas oddziaływujących z plikami wideo, możesz wykorzystać funkcjonalność DirectShow, używając opartej na COM biblioteki Quartz, zawartej w Windows Media Player oraz w systemie operacyjnym Windows. Informacje o two­ rzeniu międzyoperacyjnej asemblacji dla biblioteki rypów Quartz zawiera przepis 8. 1 O. Po utworzeniu międzyoperacyjnej asembłacji należy wykorzystać interfejs IMediaControl do załadowania i odtworzenia filmu. Jest to w zasadzie ca sama technika, co zademonstrowana w przepisie 8. 1 O dla plików audio. Jednakże, jeśli zechcesz pokazać okno wideo wewnątrz inter­ fejsu twojej aplikacji (nie zaś w oddzielnym, wolnostojącym oknie), musisz użyć także interfejsu !Video Window. Sam obiekt Fi/graphManager może być rzutowany do jednego z interfejsów IMediaControl i !video Window, a także do któregoś z innych dostępnych interfejsów - takich jak /BasicAudio (umożliwiający skonfigurowanie ustawień regulacji i poziomu dźwięku). Przy pomocy interfejsu !Video Window można przypisać dane wyjściowe wideo do obiektu na for­ mularzu, takiego jak panel łub pole obrazu. Aby to wykonać, ustaw właściwość !Video Window. Owner na uchwyt kontrolki. Wartość cę można uzyskać przy użyciu właściwości Controi.Handk. Następnie wywołaj !Video Window.SetWindowPosition, by ustawić rozmiar i położenie okna. Ta metoda służy również do zmiany rozmiarów wideo podczas odtwarzania (na przykład przy zmianie wielkości formularza) . Poniższy przykład pokazuje prosty formularz, który umożliwia użytkownikom otwieranie dowolnych plików wideo i ich odtwarzanie w odpowiednim polu obrazu. Pole to jest zamoco­ wane ze wszystkich stron formularza, dzięki czemu zmienia rozmiar przy modyfikacjach wiel­ kości formularza. Kod odpowiada na zdarzenie PictureBox.SizeChanged, sygnalizujące zmianę wielkości odpowiedniego okna wideo. using System ; u sing Qua rtzTypeLib ; u s ing System . Windows . Fo rm s ; public c l a s s ShowMovie : System . Windows . Fo rm s . Form { p rivate System . Windows . Fo rms . Pictu reBox pictu reBox l ; p rivate System . Windows . Fo rm s . Button cmdOpen ;

Rozdział 8: Grafika, multimedia i drukowanie 243

li li

( Kod p roj ektanta został pominięty . ) Zdefiniuj stałe używane do specyfikac j i stylu okna .

p rivate const int WM_APP = 0x8090 ; p rivate const int WM_GRAPHNOTIFY = WM_APP + 1 ; p rivate const int EC_COMPLETE = 0x0 1 ; p rivate const int WS_CHILD = 0x49000000 ; p rivate const int WS_CLIPCHILDREN = 0x2000000 ;

li li li

Podt rzymaj odniesienie na poziomie f o rmularza do inte rfej su kon t roluj ącego media , żeby kod mógł zarządzać odtwa rzaniem aktualnie załadowanego filmu .

p rivate IMediaCont rol me = nul l ;

li li

Podt rzymaj odniesienie n a poziomie f o rmula rza d o okna wideo w wypad ku , gdy wymaga ono zmiany rozmiaru .

p rivate IVideoWindow videoWindow = n u l l ; p rivate void cmdOpen_Click ( ob j e c t sende r , System . EventArgs e ) {

li

Pozwól u żyt kownikowi na wybór pliku .

OpenFileDialog openFileDialog = new OpenFileDialog ( ) ; open FileDialog . Fi l t e r = " Media Files l * . mpg ; * . avi ; * . wma ; * . mov ; * . wav ; * . mp2 ; * . mp3 1 Al l Files l * · * " ; if ( DialogResul t . OK == open F ileDialog . ShowDialog ( ) ) {

li

Zat rzymaj odtwa rzanie dla bieżącego f ilmu , j eś l i istniej e .

i f ( m e ! = nul l ) me . Stop ( ) ;

li

Załaduj plik filmu .

Filg raphManager g raphManage r = new Filg raphManage r ( ) ; g raphManage r . Rende rFile ( openFileDialog . FileName ) ;

li

Dołącz widok do okna ob razu na fo rmul a rzu .

t ry { videoWindow = ( IVideoWindow ) g raphManage r ; videoWindow . OWne r = ( in t ) pictu reBox l . Handle ; videoWindow . WindowStyle = WS_CHILD I WS_CLIPCHILDREN ; videoWindow . SetWindowPosition ( pictu reBoxl . C lientRectangle . Left , pictu reBox l . C l ientRectangle . Top , pictu reBox l . ClientRectangl e . Widt h , pictu reBox l . C lientRectangle . Height ) ; } catch {

li li li li

Może wystąpić błąd , j eś l i plik nie zawiera ź ródła wideo ( na p rzykład plik MP3 ) Możesz z igno rowć ten błąd i umożl iwić dal sze odtwa rzanie ( bez wizualizacj i ) .

}

li

Zapoczątkuj odtwa rzanie ( async h ronicznie ) .

244

C#

-

Księga przykładów me = ( IMediaCon t rol ) g raphManage r ; me . Run ( ) ; } } p rivate void pictu reBox l_SizeChanged ( ob j ect sende r , System . EventArgs e ) { if ( videoWindow ! = null ) { t ry { videoWindow . SetWindowPosition ( pictu reBoxl . Cl ientRectangle . Left , pictu reBox l . C l ientRectangle . Top , pictu reBoxl . Cl ientRectangle . Width , pictu reBoxl . Cl ientRectangle . Height ) ; } catch { li Zignoruj wyj ątek wywoł any podczas zmiany rozmia ru li formu l a rz a , gdy plik nie zawie ra ź ródła wideo . } } } p rivate void ShowMovie_Closin g ( obj ect sende r , System . ComponentModel . CancelEventArgs e ) { if (me ! = nul l ) me . Stop ( ) ; }

}

Przykład danych wyjściowych przedstawia rysunek 8-7. ;Jti\ Show Movie

Rysunek 8-7

[:l@JrEJ

Odtwarzanie pliku wideo.

Rozdział 8: Grafika, multimedia i drukowanie 245

8.1 2 Wyszukiwanie informacji o zainstalowanych drukarkach Problem Chcesz wyszukać listę dostępnych drukarek.

Rozwiązanie Wczytaj nazwy ze zbioru

lnstalledPrinters klasy System.Drawing.Printing.PrinterSettings.

Omówienie Klasa

PrinterSettings zawiera ustawienia

dla drukarki i informacje o niej . Możesz na przykład

użyć klasy PrinterSettings w celu określenia dostępnych rozmiarów papieru, źródeł papieru oraz rozdzielczości i sprawdzić możliwość wydruku w kolorze lub druku dwustronnego (duplekso­ wego). Możesz ponadto odczytać domyślne ustawienia marginesów, orientacji stron itd. Klasa

PrinterSettings

dostarcza zbiór statyczny

lnstalledPrinters,

zawierający nazwę każdej

drukarki zainstalowanej na komputerze. Chcąc znaleźć więcej informacji o ustawieniach dla określonej drukarki, należy utworzyć instancję wość

PrinterSettings,

jenocześnie ustawiając właści­

PrinterName.

Poniższy kod przedstawia aplikację konsolową, wyszukującą wszystkie drukarki zainstalo­ wane na komputerze i wyświetlającą informacje o rozmiarze papieru i rozdzielczości dla każdej z nich.

using System ; using System . D rawing . D ru king ; publ i c class ListP rinters { p rivate static void Main ( st ring ( ] a rg s ) { f o reach ( s t ring p rinte rName in P rinte rSetting s . I n stalledPrinters ) {

li Wyświetl nazwę d ruka rki . Consol e . Writeline ( " P rint e r : { 0 } " , p rinte rName ) ;

li P rzeglądaj u stawienia d ru ka rk i . P rinte rSettings p rinte r = new P rinte rSettings ( ) ; p rinte r . P rinte rName = p rinte rNam e ;

li li li li

Sp rawdź ważność d ru ka rki . (Ten k rok może być wymagany j eś l i odczyt u j es z nazwę d ru ka rki z dosta rczonej p rzez użyt kownika wa rtośc i , rej es t ru l u b pliku konfigu racyj nego . )

if ( p rinte r . I sValid ) {

li Wyświetl listę waż nych rozdziel czości . Con s ole . Writeline ( "Suppo rted Resol utions : " ) ;

246

C#

-

Księga przykładów f o reach ( P rinte rResol ution resolution in p rinte r . P rinte rResol utions ) { Console . WriteLine ( "

{0} " , resolut ion ) ;

} Console . WriteLine ( ) ;

li Wyświet l listę dostępnych rozmia rów papie ru . Console . W riteLine ( " Suppo rted Pape r Sizes : " ) ; fo reach ( Pape rSize s ize in p rint e r . Pape rSizes ) { if ( Enum . I sDefined ( s ize . Kind . GetType ( ) , size . Kind ) ) { Console . WriteLine ( "

{ 0 } " , size ) ;

} } Console . W riteLine ( ) ; } } Console . ReadLine ( ) ; } }

Oto przykład danych wyjściowych tego narzędzia: P rinte r : HP Lase rJet SL Suppo rted Resolution s : ( P rinte rResolution Hig h ) ( P rinte rResolution Medium ) [ P rinte rResolution Low) ( P rinte rResolution D ra f t ) [ P rinte rResolution X=600 Y=600 ) ( P rinte rResolution X=360 Y=300 ) Suppo rted Pape r Sizes : [ PaperSize Let t e r Kind=Lette r Height=ll00 Widt h=8S0 ) ( PaperSize Legal Kind=Legal Height=l400 Width=8S0 ) ( PaperSize Executive Kind=Execut ive Height=l0S0 Widt h=72S ) ( PaperSize A4 Kind=A4 Height=l l69 Widt h=827 J ( PaperSize Envelope #10 Kind=Numberl0Envelope Height=9S0 Widt h=412 ) ( PaperSize Envelope DL Kind=DLEnvelope Height=866 Width=433 ) ( PaperSize Envelope CS Kind=CSEnvelope Height=902 Width=638 ) [ PaperSize Envelope BS Kind=BSEnvelope Height=984 Width=693 ) ( PaperSize Envelope Mona rch Kind=MonarchEnvelope Height=7S0 Width=387 ) P rinte r : Gene ric PostSc ript P rinte r

Jeśli tworzysz aplikację udostępniającą możliwość drukowania, nie musisz wykorzystywać tego sposobu. Jak zobaczysz w przepisie 8. 1 3, możesz wykorzystać opcję PrintDialog do monitowa­ nia użytkownika przy wyborze drukarki i jej ustawień. Klasa PrintDialog automatycznie zasto­ suje odpowiednie ustawienia funkcji PrintDocument, bez dodatkowego kodu.

Rozdział 8: Grafika, multimedia i drukowanie 247

Uwaga Możesz drukować dokument w niemal każdym typie aplikacji. Jednakże twoja aplikacja musi zawierać odniesienie do asemblacji System. Drawing.dll. Jeśli używasz takiego typu projektu w Visual Studio .NET, który normalnie tego odniesienia nie posiada Oak np. aplikacje konsoli), musisz je dodać.

8.1 3 Drukowanie prostego dokumentu Problem Chcesz wydrukować tekst lub obrazy.

Rozwiązanie Uruchom zdarzenie PrintDocument.PrintPage i użyj metody DrawString i Drawlmage klasy Graphics, aby wydrukować dane na stronie.

Omówienie .NET wykorzystuje asynchroniczny modeł drukowania, oparty o zdarzenia. Aby wydrukować dokument, utwórz instancję System.Drawing.Printing.PrintDocument, skonfiguruj jego właści­ wości, a następnie wywołaj metodę Print, która umieści zadanie wydruku w kolejce. Moduł runtime CLR wywoła wtedy zdarzenia BeginPrint, PrintPage i EndPrint klasy PrintDocument w nowym wątku. Zarządzaj tymi zdarzeniami i posłuż się dostarczonym obiektem System.Dra­ wing. Graphics dla wyprowadzenia danych wyjściowych na stronę. Grafika i tekst są zapisywane na stronie w taki sarn sposób, jak podczas kreślenia okna przy użyciu GOI+. Może jednak zacho­ dzić potrzeba śledzenia położenia na stronie, ponieważ każda metoda klasy Graphics wymaga jawnych współrzędnych, wskazujących gdzie obiekt należy wykreślić. Ustawienia drukarki są konfigurowane przez właściwości PrintDocument.PrinterSettings i PrintDocument.DefaultPageSettings. Właściwość PrinterSettings zwraca pełen obiekt PrinterSet­ tings (co omówiono w przepisie 8. 1 2), identyfikujący drukarkę, która ma być użyta. Właściwość DefaultPageSettings przekazuje pełen obiekt PageSettings, który określa rozdzielczość, marginesy, orientację itd. Możesz skonfigurować te właściwości w swoim kodzie albo możesz użyć klasy System. Winduws.Forms.PrintDialog, co pozwoli użytkownikowi na dokonywanie zmian przy użyciu standardowego dialogu drukowania Windows (pokazanego na rysunku 8-8). W oknie dialogu drukowania użytkownik może wybrać drukarkę i ilość drukowanych egzemplarzy. Może również kliknąć przycisk Properties, aby skonfigurować bardziej zaawansowane ustawie­ nia, takie jak układ stron czy rozdzielczość drukarki. Na koniec może albo zaakceptować usta­ wioną operację drukowania, klikając OK, albo ją anulować, klikając Cancel. Przed wykorzystaniem klasy PrintDialog należy ją jawnie dołączyć do obiektu PrintDocu­ ment poprzez ustawienie właściwości PrintDialog.Document. Wtedy wszystkie dokonywane

248 C#

-

Księga przykładów

przez użytkownika zmiany w oknie dialogowym drukowania zostaną automatycznie zastoso­ wane do obiektu PrintDocument.

Prirtor

!lmM:

HP

L•?;,J•I �-------·

-

Stetus: Re* Tl'I* HP u....Jel 9. Whoro: LPT1: eo....--.:

r

I

'

r Prirt to lte t i;--:3:1 ł'

I · -'

OK

Rysunek 8-8

Cancel

Okno dialogowe Print.

W poniższym przykładzie zastosowano formularz z pojedynczym przyciskiem. Gdy użytkownik

kliknie ten przycisk, aplikacja utworzy nowy PrintDocument, umożliwi użytkownikowi skonfi­ gurowanie ustawień drukowania, a następnie uruchomi asynchroniczną operację drukowania. Programu obsługi zdarzeń, odpowiadając na zdarzenie PrintPage, zapisuje kilka linii tekstowych i obraz. u sing System ; u s ing System . Windows . Fo rm s ; u s ing System . D rawing ; u sing System . D rawing . Printing ; public class P rostyDruk : System . Windows . Fo rm s . Fo rm { p rivate System . Windows . Fo rm s . Button cmdPrint ;

li ( Kod p roj ektanta został pominięty . ) p rivate void cmdP rint_Click ( o bj ect sende r , System . EventArgs e ) {

li Utwórz document i dołącz menedże ra zda rzeń . P rintDocument doc = new P rintDocument ( ) ; doc . P rintPage += new PrintPageEventHandle r ( th i s . Doc_ P rint Page ) ;

li Zezwól użyt kownikowi n a wybór d ruka rki i zmianę innych ustawień . PrintDialog dlgSettings = new PrintDialog ( ) ; dlgSetting s . Document = doc ;

li Jeśli użytkown ik kliknął OK , d rukuj dokument . if ( dlgSett ing s . ShowDialog ( ) == DialogResult . OK ) {

li Ta metoda powraca natychmiast , zanim wysta rtu j e zadanie li wyd ruku . Zda rzenie PrintPage zapali się a s y n c h ronicznie . doc . P rint ( ) ; } }

Rozdział 8: Grafika, multimedia i drukowanie 249 p rivate void Doc_P rintPage ( object sende r , P rintPageEventArgs e ) {

li

Zdefiniuj font . Font font = new Font ( 11 Arial 11 , 39) ;

li li li

O k reśl pozycj ę na s t ronie . W tym wypadku wczytuj emy ustawienia m a rginesu ( chociaż kod nie j es t ch roniony p rzed wyj ściem poza margines ) .

float x = e . Ma rginBounds . Left ; float y = e . Ma rginBounds . Top ;

li

O k reśl wysokość l inii ( w opa rciu o używany font ) .

float l ineHeight = font . GetHeight ( e . G raphics ) ;

li

D ru kuj pięć linii tekst u .

for ( int i=0 ; i < 5 ; i++ ) {

li li

Wypisz tekst cza rnym pędzlem , używa j ą c wcześniej określonej c z c ionki i współ rzędnych .

e . G raphics . D rawSt ring ( 11This is l ine 11 + i . ToSt ring ( ) , font , B rushes . Black , x , y ) ;

li

P rzej dź w dół o odpowiednik odstępu o j edną l inię .

y += lineHeight ; } y += l ineHeight ;

li

Rysuj ob raz .

e . G raphic s . D rawimage ( Image . F romFile ( Applicat ion . Sta rtupPath + 11 \\te st . bmp11 ) , x , y ) ; } }

Ten przykład ma poważne ograniczenie: może wydrukować rylko pojedynczą stronę. Aby dru­ kować bardziej złożone dokumenty i objąć rym zadaniem więcej stron, należy utworzyć wyspe­ cjalizowaną klasę, zawierającą informacje o dokumencie, bieżącej stronie itd. Ta technika wstała pokazana w przepisie 8. 14.

8.1 4 Drukowanie dokumentu wielostronicowego Problem Chcesz drukować złożone dokumenty o wielu stronach, po kilka dokumentów jednocześnie.

Rozwiązanie Umieść informację, którą chcesz wydrukować, w klasie użytkownika, wyprowadwnej z PrintDo­ cument, a następnie ustaw właściwość PrintPageEventArgs.HasMorePages na wartość true, na czas drukowania powstałych stron do końca.

250 C#

-

Księga przykładów

Omówienie Zdarzenie

PrintDocument.PrintPage

umożliwia drukowanie pojedynczej strony. Jeśli chcesz

drukować więcej srron, musisz ustawić właściwość PrintPageEventArgs.HasMorePages w progra­ mie obsługi zdarzeń PrintPage na wartość true. Dopóki HasMorePages ma wartość true, klasa PrintDocument będzie nadal uaktywniać zdarzenia PrintPage, po jednym dla każdej strony. Jed­ nakże śledzenie, na której jesteś stronie, albo jakie dane powinny być umieszczone na kolejnych stronach zależy od ciebie. Aby w tym momencie ułatwić sobie życie, warto utworzyć klasę użytkownika.

TextDocument. Ta klasa dziedziczy po PrintDocu­ Text zapamiętuje macierz złożoną z linii tekstu, PageNumber ostatnią wydrukowaną stronę, a Offiet wskazuje ostatnią linię wydrukowaną z macie­

Niżej podajemy przykład klasy o nazwie

ment i

dołącza trzy właściwości:

zawiera rzy Text.

public cla s s TextDocument : D rukDocument { p rivate s t ring ( ] text ; p rivate int pageNumbe r ; p rivate int o f f set ; public st ring [ ] Text { get { return text ; } set {text = value ; } } public int PageNumber { get { retu rn pageNumbe r ; } set { pageNumber } public int O f f set {

=

value ; }

get { retu rn o f f set ; } set { o f f set = val ue ; } } public TextDocument ( st ring [ ] text ) { this . Text = text ; } } W zależności od typu drukowanego materiału, możesz tę klasę modyfikować. Możesz na przy­ kład zapamiętać macierz z danymi obrazu, zawartość, która posłuży jako nagłówek lub stopka na każdej sronie, informację o czcionkach albo nazwę pliku, z którego chcesz odczytywać infor­ mację. Umieszczanie informacji w pojedynczej klasie ułatwia jednoczesne drukowanie więcej niż jednego dokumentu. Kod inicjujący drukowanie jest taki sam, jak w przepisie 8. 1 3, ale teraz utworzy on instancję

TextDocument, zamiast instancji PrintDocument. Program obsługi zdarzeń PrintPage śledzi aktu­ alną linię i sprawdza, czy na stronie jest jeszcze wolna przestrzeń przed próbą wydruku następnej linii. Jeśli jest wymagana nowa strona, właściwość HasMorePages jest ustawiona na nie

PrintPage uaktywni

true i zdarze­

się znowu dla następnej strony. Jeżeli nie, operacja drukowania zostaje

sklasyfikowana jako ukończona.

Rozdział 8: Grafika, multimedia i d rukowanie 251 Oto pełen kod tej aplikacji: using System ; using System . Windows . Fo rms ; using System . D rawing ; using System . D rawing . P rinting ; public class MultiPagePrint : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . Button cmd P rint ;

li

( Kod p roj ektanta został pominięty . )

p rivate void cmd P rint_Cl ick ( obj ect sende r , System . EventArgs e ) {

li

Utwó rz dokument zawie raj ący 100 l inii .

st ring [ ] PrintText = new s t ring [ 10 1 ] ; fo r ( int i=0 ; i < 10 1 ; i++ ) { p rintText [ i ] = i . ToSt ring { ) ; printText [ i ] += " : The quick b rown fox j umps over the lazy dog . " ; } P rintDocument doc = new TextDocument ( d rukText ) ; doc . P rintPage += new P rintPageEventHand le r ( t his . Doc_ P rintPage ) ; P rintDialog dlgSettings = new P rintDialog ( ) ; dlgSetting s . Document = doc ; // J e ś l i użytkowni k kl iknął OK , d rukuj dokument . if ( dlgSettings . ShowDialog ( ) == DialogRes ult . OK ) { doc . P rint ( ) ; } } p rivate void Doc_ P rintPage ( obj ec t sende r , PrintPageEventArgs e ) { // Wys z u kaj dokument , któ ry s powodował to zda rzenie . TextDocument doc = ( TextDocument ) sende r ; / / Zdefiniuj font o k reślaj ący wysokość linii . Font font = new Font ( "A rial " , 1 0 ) ; float l ineHeight = font . GetHeight ( e . G raphic s ) ;

li

Utwó rz zmienne do ut rzymywania pozycj i na st ronie .

float x = e . Ma rginBound s . Left ; float y = e . Ma rginBound s . To p ; // I n k rementuj licznik s t ron ( wynik dotyczy następnej s t rony do // d ru ku ) . doc . PageNumber += l ; / / D rukuj pełną info rmac j ę dotyczącą st rony . // Ta pętla kończy się , gdy następna l inia może p rzekroczyć g ranice // ma rg inesu , albo j eśli nie ma więcej linii do d ru kowania . while ( ( y + l ineHeigh t ) < e . Ma rginBound s . Bottom && doc . O f f set J 3\Hloppy (A:) 1ł1

A

�t!lfa&·MW

(' "- Docl.11lents (O:)

't' .....

TOfl'4J(E:) ,;J. CD Dtlve (F:) „ � CD Dtlve (Gr) 'Y My Network Ploces 'łJ

1"'

I Make ,_ Folder I Rysunek 9-3

Ol(

l1

Cancel

I

Okno dialogowe FolderBrowserDialog.

Poniżej zamieszczono kod aplikacji Windows, umożliwiający użytkownikowi załadowa­ nie dokumentów do okna

Rich TextBox,

a następnie edytowanie zawartości i zapamiętanie

zmodyfikowanego dokumentu. Podczas otwierania i zapamiętywania dokumentu używane są klasy

OpenFileDiawg i SaveFileDiawg.

u s ing System ; using System . D rawing ; us ing System . Windows . Fo rms ; public class SimpleEdit F o rm : System . Windows . Fo rms . Form { p rivate System . Windows . Fo rms . Menuitem mnuFil e ; p rivate System . Windows . Fo rms . Menu!tem mnuOpen ; p rivate System . Windows . Fo rms . Menu! tem mnuSave ; p rivate System . Windows . Fo rms . Menuitem mnuExit ; p rivate System . Windows . Fo rms . RichTextBox rtDoc ;

li ( Kod p roj ektanta pominięt o ) . p rivate void mnuOpen_ C l i c k ( obj ect sende r , System . EventArgs e ) { OpenFileDialog dlg = new OpenFileDialog ( ) ; dlg . Filte r = " Rich Text Files ( * . rt f ) l * . RTF I " + "All files ( * . * l i * · * " ; dlg . CheckFileExis t s = t ru e ; dlg . I nitialOi rec t o ry = Application . StartupPath ; if ( dlg . ShowDialog ( ) == DialogResu l t . OK ) { rtDoc . LoadFile ( dlg . FileName ) ; rtDoc . Enabled = t rue ; } } p rivate void mnuSave_ C l ic k ( obj ect sende r , System . EventArgs e ) { SaveFileDialog d l g = new SaveFileDialog ( ) ; dlg . Filter = " RichText Files ( * . rt f ) l * . RTF I Text Files ( * . txt l l * . TXT" + " I All files ( * . * l i * · * " ; dlg . CheckFileExis t s = t ru e ; dlg . I nitialDi rectory = Applicat ion . Sta rtupPat h ;

Rozdział 9: Pliki, katalogi i operacje wejścia/wyjścia (I/O)

291

if ( dlg . ShowDialog ( ) == DialogResu l t . OK ) { rtDoc . SaveFile ( d lg . FileName ) ; } } p rivate void mnu Exit_Click ( obj ect sende r , System . EventArgs e ) { t h i s . Close ( ) ; } }

9.1 8 Używanie izolowanego magazynu Problem Chcesz zapamiętać dane w pliku, ale twoja aplikacja nie posiada wymaganego uprawnienia FiklOPermission na dostęp do lokalnego dysku twardego.

Rozwiązanie Użyj klas lsolatedStorageFik i lsolatedStorageFikStream z przestrzeni nazw System.10.lsolated­ Storage. Te klasy umożliwiają aplikacji zapis danych do pliku w katalogu właściwym dla użyt­ kownika bez potrzeby posiadania uprawnienia bezpośredniego dostępu do lokalnego dysku twardego.

Omówienie System .NET Framework zawiera opcję umożliwiającą dostęp do izolowanego magazynu, co pozwala na odczytywanie i zapisywanie w specyficznym dla użytkownika wirtualnym syste­ mie plików, zanądzanym przez moduł Common Language Runtime (CLR). Przy tworzeniu plików w magazynie izolowanym dane są automatycznie serializowane do unikatowego położe­ nia w ścieżce profilu użytkownika (typowa ścieżka to c:\Documents i settings\[username] \Local settings\Application data\lsołated storage\ [guid_identifier]) . Jednym z powodów korzystania z magazynu izolowanego jest stworzenie ograniczonej moż­ liwości zapamiętania danych dla częściowo zaufanej aplikacji (przepis 1 3 . l podaje więcej infor­ macji dotyczącej kodu zaufanego częściowo). Na przykład domyślne zasady zabezpieczeń CLR dają lokalnemu kodowi nielimitowany dostęp Fik!OPermission, co umożliwia otwieranie i zapis w każdym pliku. Kod uruchomiony ze zdalnego serwera w lokalnym intranecie ma automatycz­ nie przyznawane mniejsze uprawnienia - nie ma uprawnienia Fik!OPermission, posiada jednak lsolatedStoragePermission, co daje mu możliwość używania magazynu izolowanego (zasady bez­ pieczeństwa limitują także maksymalną ilość przestrzeni używanej przez magazyn izolowany). Innym powodem korzystania z tego typu pamięci może być poprawienie bezpieczeństwa danych. Na przykład dane składowane w izolowanym magazynie jednego użytkownika nie są dostępne dla innego użytkownika, nie będącego administratorem komputera.

292 C#

-

Księga przykładów

Poniższy przykład ilustruje dostęp do magazynu izolowanego:

u s ing System ; us ing System . IO ; us ing System . IO . I solatedSto rage ; public cla s s I solatedSto reTest { p rivate s tatic void Main ( ) {

li Two rzenie nowego magazynu dla bieżącego użyt kownika . I solatedStorageFile store = I solatedSto rageFile . GetUs e rStoreFo rAs sembl y ( ) ;

li Two rzenie folderu w ko rzeniu magazynu izolowanego . sto re . C reateD i rectory ( "MyFolde r" ) ;

li Utwo rzenie pliku w magazynie i zolowanym . St ream fs = new I solatedStorageFileSt ream ( " MyFile . txt " , FileMode . C reate , s t o re ) ; St reamWrite r w = new St reamWrite r ( f s ) ;

li Do tego pliku można no rmalnie zapisywa ć . w . Wri te Line ( "Test " ) ; w . Flush ( ) ; f s . Close ( ) ; Con sole . WriteLine ( " C u r rent size : Conso le . WriteLine ( " Scope :

"

+

sto re . C u r rentSize . Tost ring ( ) ) ;

+ sto re . Sc o pe . To St ring ( ) ) ; Console . W riteLine ( " Contained files include : " ) ; st ring [ ) files = s t o re . Get FileNames ( " * . * " ) ; "

fo reach ( s t ring fil e in file s ) { Console . WriteLine ( file ) ; } Con sole . ReadLine ( ) ; } } Domyślnie każdy izolowany magazyn jest przydzielany ze względu na użytkownika i asem­ blację. Oznacza to, że jeśli ten sam użytkownik uruchamia tę samą aplikację, to będzie ona miała dostęp do danych w tym samym magazynie izolowanym. Jednakże można zdecydować się na dalszą segregację ze względu na domenę aplikacji, tak że wiele instancji tej samej aplikacji otrzymuje różne magazyny izolowane, jak to pokazano niżej:

li Dostęp do izolowanego magazynu dla bieżącego użytkownika i a semblacj i li ( równoważne pie rwszemu p rzykładowi ) . store = I s olatedSto rageFile . GetSto re ( I solatedSto rageScope . User I solatedStorageScope . As sembly , n ul l , n u ll ) ;

li Dostęp do izolowanego magazynu dla bieżącego użyt kown ika , asemblac j i li i domeny apl ikac j i . I n nymi słowy , dane s ą dostępne tylko dla bieżącej li instanc j i apl ikacj i . s t o re = I solatedSto rageFile . GetSto re ( I solatedSto rageScope . Us e r I

Rozdział 9: Pliki, katalogi i operacje wejścia/wyjścia ( I/O)

293

I s olatedSto rageScope . As sembly I IsolatedSt o rageScope . Domain , null , null ) ;

Te pliki są przechowywane jako część profilu użytkownika; dzięki temu użytkownicy pro­ fili mobilnych mogą mieć dostęp do swoich plików w izolowanych magazynach, logując się na dowolnej stacji roboczej. W tym wypadku musi wstać jawnie przypisany magazyn mobilny (roamint) przez użycie flagi lsolatedStorageFile.Roamingw momencie jego tworzenia. Pozwalając systemowi .NET Framework i modułowi CLR na zapewnienie odpowiednich poziomów izo­ lacji można scedować na system odpowiedzialność za utrzymywanie separacji między plikami i wstawić na uboczu kwestię ewentualnych przeoczeń lub niedociągnięć, które mogłyby spowo­ dować utratę krytycznych danych.

9.1 9 Monitorowanie zmian w systemie plików Problem Chcesz reagować, gdy w systemie plików na określonej ścieżce za.istniały zmiany (takie jak np. modyfikacja lub utworzenie pliku).

Rozwiązanie Użyj komponentu System.10.FileSystem Watcher w celu określenia ścieżki lub pliku, który chcesz monitorować i obsłuż zdarzenia Created, Deleted, Renamed i Changed.

Omówienie Łącząc razem wiele aplikacji i procesów biznesowych często zachodzi potrzeba stworzenia programu, który oczekuje bezczynnie i przechodzi w stan aktywny tylko wtedy, gdy zostanie utworzony nowy lub zmieniony istniejący plik. Tego typu program można utworzyć poprzez okresowe skanowanie katalogów, ale rozwiązanie takie ma podstawową wadę: im częstsze ska­ nowanie, tym więcej zasobów wstaje zmarnowanych. Z drugiej strony rzadkie skanowanie powoduje, że wykrycie zmiany wymaga dużo czasu. Rozwiązaniem jest użycie klasy FileSystem­ Watcher, co pozwala na bezpośrednie reagowanie na zdarzenia systemu plików Windows. W celu użycia FileSystem Watcher należy utworzyć instancję klasy i ustawić następujące właś­ ciwości: • • •

Path wskazuje na katalog, który ma być monitorowany. Fi/ter wskazuje typy plików, które są monitorowane. Noti/JFilter wskazuje typ zmian, które są monitorowane.

FileSystem Watcher wyzwala cztery klucwwe zdarzenia: Created, Deleted, Renamed i Changed. Wszystkie te zdarzenia dostarczają informacje poprzez parametr FileSystemEventArgs, w tym nazwę pliku (Name), pełną ścieżkę (FullPath) oraz typ zmiany (Change Ijpe). Zdarzenie Renameddostarcza instancję RenamedEventArgs, dziedziczącą z FileSystemEventArgs i dodaje informacje o oryginalnej

294 C#

-

Księga przykładów

OUFu!LPath). W razie potrzeby można zabronić występowania tych FileSystem Watcher.EnableRaisingEvents nafa/se. Zdarzenia Crrated, Deleted i &named są łatwe do obsłużenia. Aby użyć zdarzenia Changed, należy skorzystać z właściwości NotifyFi/Jer, aby wskazać typy oczekiwanych zmian. W innym wypadku program nazwie pliku (OUName i

zdarzeń poprzez ustawienie właściwości

może zostać zamulony przez niekończące się serie zdarzeń w trakcie modyfikacji plików. Właściwość

NotifyFi/Jer

może być ustawiona przy użyciu dowolnej kombinacj i podanych

niżej wartości typu wyliczeniowego • • • • • • • •

System.IO.NotifyFilters:

Attributes Crration Time DirectoryName FileName LastAccess LastWrite Security Size

Poniższy przykład pokazuje zastosowanie aplikacji konsolowej zarządzającej zdarzeniami tworze­ nia

(Created) i usuwania (Deleted) i

testuje te zdarzenia poprzez utworzenie pliku testowego.

u s ing System ; u s ing System . IO ; u s ing System . Windows . Fo rms ; public class FileWatcherTest { p rivate static void Main ( ) {

li Kon figu rowanie FileSystemWatch e r . FileSystemWatche r watch = new FileSystemWatche r ( ) ; watch . Path = Appl icat ion . Sta rtupPath ; watch . Fil ter = " * . * " ; watch . I ncludeSubd i rectories = t ru e ;

li Dołączenie p rog ramu obsługi zdarzeń . wat ch . C reated += new FileSystemEventHandle r ( On C reatedO rDeleted ) ; watch . Deleted += new FileSystemEventHandl e r ( On C reatedO rDeleted ) ; watch . EnableRaisingEvents = t rue ; Console . WriteLine ( " P ress Enter to c reate a file . " ) ; Console . Readline ( ) ; if ( File . Exist s ( " test . bi n " ) ) { File . Delete ( " test . bi n " ) ; } FileSt ream fs = new FileSt ream ( " test . bin " , FileMode . C reate ) ; f s . Close ( ) ; Conso le . WriteLine ( " P ress Ente r to te rminate the appl ication . " ) ; Console . WriteLine ( ) ; Console . Readline ( ) ; }

Rozdział 9: Pliki, katalogi i operacje wejścia/wyjścia ( I/O)

295

li Wyzwolenie zda rzenia , gdy nowy plik zostanie utwo rzony w monito rowanym li katalogu . p rivate static void OnC reatedOrDeleted ( obj ect sende r , FileSystemEventArgs e ) {

li Wyświetlenie monit u . Console . Writeline ( " \ tNOTI FICATION : " + e . FullPath + " ' was " + e . ChangeType . ToSt ring ( ) ) ; Console . W riteline ( ) ; } }

9.20 Dostęp do portu COM Problem Chcesz przesłać dane bezpośrednio do portu szeregowego.

Rozwiązanie Win32 API udostępnia niezarządzane metody do bezpośredniego odczytu i zapisu bajtów do i z portu szeregowego (w bibliotece kernel32.dll). Funkcje ce można zaimponować do apli­ kacji C# lub zastosować kontrolkę ActiveX wyższego poziomu Microsoft Communications (MSComm.ocx), stanowiącą licencjonowany moduł dołączony do Microsoft Visual Studio 6.

Omówienie System .NET nie udostępnia zarządzanego interfejsu dla portów szeregowych. W rezultacie projektanci potrzebujący takiej funkcjonalności muszą zajmować się względnie skomplikowa­ nym kodem międzyoperacyjnym. Jednym z rozwiązań jest wygenerowanie opakowania .NET dla Microsoft Communications Control (MSComm.ocx). Dostarcza on modelu obiektu wyższego poziomu do pracy z portami szeregowymi . W tym celu trzeba jednak uzyskać kontrolkę z Visual Studio 6, gdyż jest to moduł licencjonowany. (Można wykonać niestandardową instalację pakietu Visual Studio 6, instalując tylko komponenty ActiveX, co wymaga około 5 MB przestrzeni dyskowej). Więcej informacji na ten temat można znaleźć w dokumentacji Visual Studio 6. Inną opcję stanowi zaimponowanie funkcji API z kernel32.dll. Ta metoda wymaga trochę więcej uwagi, ponieważ należy użyć odpowiednich typów danych C# i zabezpieczyć plan struktur pamięci. Na szczęście Justin Harrell ([email protected]) udostępnia rozwiązanie tego problemu w postaci niestandardowej klasy C# o nazwie ComPort, oferującej tę funkcjonalność. Pełny kod dla tej klasy jest stosunkowo długi. Jest on zawarty w przykładowym kodzie dla tego przepisu.

296

C#

-

Księga przykładów

Można dołączyć tę klasę do własnych aplikacji i użyć następującego kodu do interakcji tem COM:

ComPort po rt = new ComPort ( ) ; t ry {

li Kon figu rowanie p rędkośc i t ransmis j i , parzystości itp . port . BaudRate = 9609 ; port . Pa rity = 0 ; port . PortNum = 1 ; port . ReadTimeout = 10 ; port . StopBits = 1 ; port . ByteSize = 1 ;

li Otwa rcie port u . port . Open ( ) ;

li Wpisanie pewnej liczby danych . port . Write ( new byte [ l ] ) ;

li Zamknięcie port u . port . Close ( ) ; } catch ( Applicat ionException e r r ) { Console . WriteLine ( e rr . Mes sage ) ; }

z

por­

10 Dostęp do baz danych Dostęp do wielu zróżnicowanych zasobów danych w środowisku M icrosoft .NET Framework jest realizowany przez grupę klas o zbiorczej nazwie M icrosoft ADO.NET. Każdy typ źródła danych jest obsługiwany za pośrednictwem dostawcy danych

(data provider).

Każdy dostawca

danych zawiera zestaw klas, który nie rylko implementuje standardowy zestaw interfejsów, ale zapewnia również funkcjonalność unikatową dla obsługiwanego źródła danych. Klasy te obej­ mują reprezentacje połączeń, poleceń, właściwości, adapterów danych oraz czytników danych, przez które odbywa się interakcja

ze

źródłem danych. Tabela l 0- 1 przedstawia dostawców

danych, zawartych jako standard w systemie .NET Framework.

Tabela 1 0-1

Implementacje dostawcy danych NET Framework .

Dostawca danych Opis .NET Framework

Zapewnia możliwość połączenia z dowolnym źródłem danych stosują­

Data Provider dla

cym interfejs ODBC, w tym baz danych Microsoft SQL Server, Oracle

ODBC

i Microsoft Access . Klasy dostawcy danych znajdują się w przestrzeni nazw

System.Data. Odbc i mają prefiks Odbc. . NET Framework

Zapewnia możliwość połączenia z dowolnym źródłem danych stosującym

Data Provider dla

interfejs OLE DB, w tym baz danych M icrosoft SQL Server, MSDE,

OLE DB

Oracle i Jer. Klasy dostawcy danych znajdują się w przestrzeni nazw

System.Data. OleDb i

mają prefiks

OleDb.

. NET Framework

Zapewnia możliwość optymalnych połączeń z bazami danych Oracle.

Data Provider dla

Klasy dostawcy danych znajdują się w przestrzeni nazw

Oracle

OracleClient i

mają prefiks

System.Data.

Oracle.

. NET Framework

Zapewnia możliwość połączenia z bazami danych Microsoft SQL Server

Data Provider dla

wersji 7 i późniejszymi (w tym MSDE), poprzez bezpośrednią komuni­

SQL Server

kację z SQL Server, bez potrzeby używania ODBC lub OLE DB. Klasy dostawcy danych znajdują się w przestrzeni nazw i mają prefiks

System. Data.SqlClimt

Sql.

.NET Compact

Zapewnia możliwość połączenia z bazami danych Microsoft SQL Server

Framework Data

CE. Klasy dostawcy danych znajdują się w przestrzeni nazw

Provider dla Ser­

SqlServerCe i mają prefiks SqlCe.

wera SQL CE

System.Data.

298

C#

Księga przykładów

-

Niniejszy rozdział opisuje niektóre z często używanych aspektów ADO.NET. W istocie jednak ADO.NET jest rozszerzalnym podzbiorem biblioteki klas systemu .NET Framework i zawiera wiele zaawansowanej funkcjonalności. Dążąc do lepszego posługiwania się ADO.NET, warto przeczytać książkę Davida Sceppa Microsoft ADO.NET Core Reference (Microsoft Press, 2002). Przepisy zawarte w tym rozdziale dotyczą następujących zagadnień: •

Tworzenie, konfigurowanie, otwieranie i zamykanie połączeń z bazami danych (przepis 1 0. 1 ).



Wykorzystanie pul połączeń w celu poprawienia wydajności i skalowalności aplikacji (prze­ pis 1 0.2).



Wykonywanie poleceń SQL i procedur składowanych oraz wykorzystanie parametrów w celu poprawienia elastyczności (przepisy 1 0.3 i 1 0.4).



Przedstawianie wyników zwróconych przez zapytania do bazy danych (przepisy I 0.5 i I 0.6).



Odszukanie wszystkich instancji SQL Server dostępnych w sieci (przepis I 0.7).

Uwaga Do zademonstrowania omawianych w przepisach technik użyto przykładowej bazy danych Northwind, przygotowanej przez Microsoft.

1 0.1 Połączenia z bazą danych Problem Chcesz otworzyć połączenie z bazą danych.

Rozwiązanie Utwórz obiekt połączenia odpowiedni do typu bazy danych, z którą chcesz się połączyć; inter­ fejs System.Data.!DbConnection implementuje wszystkie obiekty połączeń. Skonfiguruj obiekt połączenia poprzez ustawienie właściwości ConnectionString. Otwórz połączenie poprzez wywo­ łanie metody Open obiektu połączenia.

Omówienie Pierwszym krokiem przy dostępie do baz danych jest otwarcie połączenia z konkretną bazą danych. Interfejs !DbConnection reprezentuje połączenie z bazą danych, a każdy dostawca danych zawiera unikatową implementację. Niżej podajemy listę implementacji IDbConnection dla pięciu standardowych dostawców danych. • • • •

System. Data. Odbc. OdbcConnection System.Data. OleDb. OleDbConnection System. Data. OradeClient. OracleConnection System. Data.SqlServerCe. SqlCeConnection

Rozdział 1 O: Dostęp do baz danych •

299

System.Data.Sq/Climt.Sq/Connection

Należy skonfigurować obiekt połączenia przy użyciu łańcucha połączenia. Jest to zestaw par wartości nazw rozdzielonych średnikami. Łańcuch połączenia można dostarczyć albo jako argu­ ment konstruktora, albo poprzez ustawienie właściwości obiektu połączenia ConnectionString przed otwarciem połączenia. Każda implementacja połączenia wymaga zapewnienia różnych informacji w łańcuchu połączenia. Szczegóły można znaleźć w dokumentacji dotyczącej właści­ wości ConnectionString dla każdej implementacji. Niżej pokazano wartości, które można specy­ fikować. Ewentualne ustawienia zawierają: •

nazwę docelowego serwera bazy danych,



nazwę bazy danych otwieranej na początku,



wartości limitów czasowych połączenia,



zachowanie się puli połączeń (wbacz przepis 1 0.2.),



mechanizmy uwierzytelnienia, używane przy połączeniach z zabezpieczonymi bazami danych, w tym możliwość podania nazwy i hasła użytkownika.

Po skonfigurowaniu należy wywołać metodę Open dla obiektu połączenia, aby otworzyć połą­ czenie z bazą danych. Później można wykorzystać obiekt połączenia do wykonywania poleceń dotyczących źródła danych (co omówiono w przepisie 1 0.3). Właściwości obiektu połącze­ nia umożliwiają również wyszukiwanie informacji o stanie połączenia i ustawieniach użytych do jego otwarcia. Po zakończeniu wykorzystywania połączenia należy zawsze wywołać metodę Ciose, aby zwolnić blokowane połączenie z bazą danych niższej warstwy i zasoby systemowe. Klasa !DbConnection rozszerza System.!Disposab/e, co oznacza, że każda klasa połączenia imple­ mentuje metodę Dispose. Ta metoda automatycznie wywołuje Ciose, co czyni z instrukcji using klarowny i wydajny sposób korzystania z połączonych obiektów w kodzie projektanta. Optymalną skalowalność zapewnia możliwie najpóźniejsze otwieranie połączenia z bazą danych i zamykanie go natychmiast po ukończonym zadaniu. Dzięki temu połączenia z bazą danych nie są blokowane na dłuższy okres czasu i cały kod ma zapewnione najlepsze możliwości połączenia. Ma to szczególne znaczenie w przypadku, gdy używane są pule połączeń. Poniższy kod przedstawia wykorzystanie klasy Sq/Connection do otwierania połączeń z bazą danych SQL Server pracującą na lokalnym komputerze z użyciem zintegrowanych zabezpieczeń Windows. Aby uzyskać dostęp do zdalnego komputera, należy po prostu zmienić nazwę źródła danych z Loca/host na nazwę instancji używanej bazy danych.

li Utwo rzenie pustego obiektu SqlConnec t ion . using ( SqlConnection con = new SqlConnection { ) ) {

li Skonf igowanie łańcucha połączenia obiektu SqlConnection . con . Connect ionSt ring = " Data Source = localhost ; " + li lokalna instanc j a SQL Server " Database = Northwind ; " + li P rzykł adowa baza No rthwind " I nteg rated Secu rity=SSPI " ; li zinteg rowane zabezpieczenia Windows

li Otwa rcie połączenia z bazą danych . con . Open ( ) ;

li Wyświetlenie informacj i o połączeniu . if ( con . State == Connect ionState . Open ) {

300

C#

-

Księga przykładów Consol e . WriteLine ( " SqlConnection I n f o rmation : " ) ; Consol e . WriteLine ( "

Connection State = " + con . State ) ;

Consol e . WriteLine ( "

Connection St ring = " +

con . ConnectionSt ring ) ; Consol e . WriteLine ( "

Database Sou rce = " + con . DataSou rce ) ;

Consol e . WriteLine ( "

Database = " + con . Database ) ;

Con sol e . WriteLine ( " Con sol e . WriteLine ( "

Se rver Version " + con . ServerVe rsion ) ; Wo rkstation Id = " + con . Wo rkstation ld ) ;

Console . WriteLine ( "

Timeout = " + con . ConnectionTimeou t ) ;

Console . WriteLine ( "

Packet Size = " + con . PacketSize ) ;

=

} else { Con sole . W riteLine ( " SqlConnection failed to open . " ) ; Console . W riteLine ( "

Connection State = " + con . State ) ;

}

li Na zakończenie korzystania z bloku Dispose ( ) wywoł u j e Close ( ) . }

Następny przykład stanowi fragment przykładowego kodu dla tego przepisu, ukazujący łańcuch połączenia używany do otwarcia połączenia z tą samą bazą danych, jeżeli dla zapewnienia połą­ czenia zostanie wykorzystany dostawca danych OLE DB. li Utwo rzenie pustego obiektu OleDbConnection . u s ing ( OleDbConnect ion con = new OleDbConnection ( ) ) {

li Kon figu rowanie łańcucha połączenia obiektu OleDbConnect ion . con . ConnectionSt ring = " P rovide r = SQLDLEDB ; " + " Data Source = lokalnyhos t ; " +

11 OLE DB P rovid e r f o r SQL Se rve r

li lokalna instanc j a SQL Se rve r

" I nitial Catalog = No rthwind ; " + li p rzykładowa baza Northwind " I nteg rated Sec u ri ty=SSPI " ; I I z integ rowane zabezpieczenia Windows

li Otwa rcie połączenia z bazą danych . con . Open ( ) ; }

1 0.2 Tworzenie pul połączeń Problem Chcesz wykorzystać pulę połączeń z bazą danych w celu podniesienia wydajności i skalowalno­ ści aplikacji.

Rozwiązanie Skonfiguruj pulę połączeń, używając ustawień w łańcuchu połączenia dla obiektu połączenia.

Rozdział 1 O: Dostęp do baz danych

301

Omówienie Tworzenie pul połączeń znacznie redukuje przeciążenie związane z ustanawianiem i anulowa­ niem połącze11 z bazą danych. Zapewnia ono także skalowalność rozwiązań poprzez redukcję liczby współbieżnych połączeń, które baza danych musi utrzymywać - wiele z nich znajduje się w stanie jałowym przez większą część czasu. Przy zastosowaniu pul połączeń zamiast tworzenia i otwierania nowych obiektów połączenia, można wykorzystać już otwarte połączenie z puli, gdy zachodzi potrzeba. Po zakończeniu korzystania z połączenia, zamiast je zamykać, zwraca się je do puli połączeń, co umożliwia wykorzystanie go przez inny kod. Dostawcy danych SQL Server i Oracle zawierają funkcjonalność pul połączeń jako funkcję domyślną. Dla każdego unikatowego łańcucha połączenia, który wstaje wyspecyfikowany przy otwarciu nowego połączenia, działa jedna pula połączeń. Za każdym razem, gdy zostaje otwarte nowe połączenie przy pomocy łańcucha połączenia, który był już używany poprzednio, połącze­ nie wstaje pobrane z działającej puli. Tylko wtedy, gdy wstaje wyspecyfikowany łańcuch nowego połączenia, dostawca danych utworzy nową pulę połączeń. Można kontrolować niektóre charakte­ rystyki puli połączeń przy użyciu ustawień łańcucha połączenia. przedstawionych w tabeli 1 0-2.

Ważne

Raz wykreowana pula działa aż do momentu zakończenia procesu.

Tabela 1 0-2

Ustawienia łańcucha połączenia sterujące pulami połączeń

Ustawienie

Opis

Czas trwania połączenia

Podaje maksymalny czas w sekundach, określający istnienie połączenia w puli przed zamknięciem. Wiek połączenia jest testowany tylko w momencie zwrotu połączenia do puli. To ustawienie ułarwia minimalizację rozmiaru puli, jeżeli nie jest ona zbyt często używana. Zapewnia również optymalne zrównoważenie obciążenia w środowisku klastrowych baz danych. Wartością domyślną jest zero, co oznacza, że połączenia istnieją tylko podczas trwania bieżącego procesu.

Resetowanie połączenia (Reset)

Dostarczane tylko przez dostawcę danych SQL Server. Określa, czy połączenia są resetowane w momencie ich pobierania z puli. Wartość „True" oznacza, że stan połączenia jest resetowany, ale wymaga dodatkowej komunikacji z bazą danych. Wartością domyślną jest „True".

Max Pool Size

Określa maksymalną liczbę połączeń, które mogą znajdować się w puli. Połą­ czenia są tworzone i dodawane do puli na żądanie, dopóki ta liczba nie wsta­ nie osiągnięta. Jeśli nastąpi żądanie połączenia, ale brakuje wolnych połączeń, funkcja wywołująca wstanie zablokowana do momentu, gdy połączenie stanie się dostępne. Wartością domyślną jest 1 00.

Min Pool Size

Określa minimalną liczbę połączeń, które mogą znajdować się w puli. Podczas tworzenia puli ca liczba połączeń wstaje ucworwna i dodana do puli. W trak­ cie okresowej konserwacji bazy, a także po żądaniu połączenia, do puli doda­ wane są połączenia aż do osiągnięcia tej liczby. Wartością domyślną jest O.

Poołing

Dla połączenia nie należącego do puli ustawia się na „False". Wartością domyślną jest „True".

302

C#

-

Księga przykładów

Poniższy fragment kodu dla tego przepisu demonstruje konfigurację puli połączeń, która zawiera minimalnie 5 i maksymalnie 1 5 połączeń. Czas życia połączeń wynosi 1 0 minut (600 sekund) ; jest on resetowany za każdym razem, gdy połączenie jest pobierane z puli. li Uzyskanie połączenia z puli . u sing ( SqlConnection con = new SqlConnection ( ) ) { li Kon figu rowanie łańcucha połączenia obiektu SqlConnection . con . Connect ionSt ring = " Data Source = lokalnyhos t ; " +

li lokalna instan c j a SQL Serve r li p rzykładowa baza Nort hwind " I nteg rated Secu rity = SSP! ; " + li z integ rowane zabezpieczenia Windows " Database = Nort hwind ; " +

"Min Pool Size = 5 ; " + "Max Pool S i z e = 15 ; " +

li minimalna wiel kośt p u l i

" Connection Reset = True ; " +

li resetowanie łącza za każdym razem li maksymalny czas życia połączenia

"Connection Lifet ime = 600 " ;

li maksymalna wielkośt puli

li Otwa rcie połączenia do bazy danych . con . Open ( ) ; li Dostęp do bazy danych . . .

li Na zakończenie korzystania z bloku Dispose wywołu j e Close , co zwraca li połączenie do puli i umożl iwia j ego ponowne wyko rzystanie . }

Ten fragment kodu demonstruje wykorzystanie ustawienia Pooling w celu otrzymania obiektu połączenia nie należącego do puli. Jest to użyteczne, gdy aplikacja wymaga pojedynczego, ale długotrwałego połączenia z bazą danych. li Uzyskiwanie połączenia spoza pul i . u s ing ( SqlConnection con = new SqlConnect ion ( ) ) { li Kon f igu rowanie łańcuc ha połączenia obiektu SqlConnection . con . ConnectionSt ring = " Data Source = lokalnyhost ; " +

li lokalna instanc j a SQL Se rve r l i p rzykładowa baza No rthwind " I nteg rated Secu rity = SSP! ; " + li zinteg rowane zabezpieczenia Windows " Database = Northwind ; " +

" Pooling = False" ; li wskaznie połączenia spoza puli li Otwa rcie połączenia do bazy danych . con . Open ( ) ; li Dostęp do bazy danych . . .

li Na zakończenie korzystania z bloku Dispose wywołu j e Close, co zamyka li połączenie . }

Rozdział 1 O: Dostęp do baz danych

303

Dostawcy danych ODBC i OLE DB mają również możliwość tworzenia pul połączeń, ale nie implementują ich w obrębie zarządzanych klas .NET. Konfiguracja puli połączeń jest w tym wypadku inna, niż dla dostawcy SQL Server lub Oracle. Tworzenie pul połączeń ODBC jest dokonywane przez ODBC Driver Manager i konfigurowane przy użyciu narzędzia ODBC Data Source Administrator w panelu sterującym Control Panel. Dodawanie połączeń OLE DB jest dokonywane przez implementację lokalną OLE DB; tu pozostaje jedynie możliwość zakazu tworzenia pul poprzez włączenie ustawienia " OLE DB Services= - 4 ; " w łańcuchu znaków połą­ czenia. Dostawca danych SQL Server CE nie umożliwia tworzenia pul połączeń, gdyż ta wersja oprogramowania wspiera tylko jedno współbieżne połączenie.

1 0.3 Wykonanie polecenia SQL lub procedury składowanej Problem Chcesz wykonać polecenie SQL lub składowaną procedurę dotyczącą bazy danych.

Rozwiązanie Utwórz obiekt polecenia odpowiedni dla wybranego typu bazy danych; wszystkie obiekty pole­ ceń implementują interfejs System.Data.!DbCommand. Skonfiguruj obiekt polecenia poprzez ustawienie właściwości CommandTjpe i CommandText. Wykonaj polecenie przy użyciu jed­ nej z następujących metod: ExecuteNonQuery, Execute&ader lub ExecuteScalar, w zależności od typu polecenia i spodziewanych wyników.

Omówienie Interfejs IDbCommand reprezentuje polecenie bazy danych, a każdy dostawca danych zawiera unikatową implementację. Niżej przedstawiona jest lista implementacji !DbCommand dla pię­ ciu standardowych dostawców danych. • • • • •

System.Data. Odbc. OdbcCommand System.Data. OleDb. OleDbCommand System.Data. OmcleClient. OracleCommand System.Data.SqlServerCe.SqiCeCommand System.Data.SqiClient. SqiCommand

Aby wykonać polecenie dotyczące bazy danych, musi zostać otwarte połączenie (co zostało omówione w przepisie 1 0 . 1 ) i prawidłowo skonfigurowany obiekt polecenia, właściwy dla typu bazy danych. Można utworzyć obiekty polecenia bezpośrednio przy użyciu konstruktora, ale prostszym sposobem jest użycie metody fabrykującej CreateCommand dla obiektu połączenia. Metoda CreateCommand zwraca obiekt polecenia dla właściwego typu dostawcy danych i kon­ figuruje go przy użyciu podstawowej informacji otrzymanej z połączenia, które zostało użyte

304

C#

-

Księga przykładów

do utworzenia polecenia. Przed wykonaniem polecenia należy skonfigurować właściwości opi­ sane w tabeli 1 0-3, które są wspólne dla wszystkich implementacji poleceń.

Tabela 1 0-3 Wspólne właściwości obiektów poleceń

Włudwość

Opis

CommandText

Łańcuch zawierający tekst polecenia SQL do wykonania lub nazwę skła­ dowanej procedury. Zawartość właściwości CommandText musi być kom­ patybilna z wartością wyspecyfikowaną we właściwości CommandTjpe.

CommandTimeout Wartość int (liczba całkowita}, określająca liczbę sekund oczekiwania na powrót wywołanego polecenia przed wyczerpaniem się limitu czasu i zgłoszeniem wyjątku. Domyślnie do 30 sekund.

Command1jpe

Wartość typu wyliczeniowego System.Data. Command1jpe, określająca typ polecenia reprezentowanego przez obiekt polecenia. Dla większo­ ści dostawców danych uprawnionymi wartościami są StoredProcedure, kiedy ma być wykonywana składowana procedura oraz Text, kiedy ma być wykonywane polecenie tekstowe SQL. Kiedy jest używany dostawca danych OLE DB, można wyspecyfikować TableDirect w celu zwrócenia całej zawarcosci jednej łub więcej tablic; więcej szczegółowych infor­ macji znajduje się w dokumentacji SOK .NET Framework. Wartością domyślną jest Text.

Connection

Instancja !DbConnection, udostępniająca połączenie z bazą danych, w której to polecenie będzie wykonywane. Jeżeli polecenie zostało utwo­ rzone przy użyciu metody !DbConnection. CreateCommand, ta właściwość będzie automatycznie ustawiana dla instancji IdbConn.ection, z której zostało utworzone polecenie.

Parameters

Instancja System.Data.ldataParameterCollection, zawierająca zestaw para­ metrów do podstawienia w poleceniu (więcej szczegółów dotyczących użycia parametrów podaje przepis 1 0.4).

Transaction

Instancja System.Data.!Db Transaction reprezentuje transakcję obejmu­ jącą dane polecenie (więcej szczegółów dotyczących transakcji znajduje się w dokumentacji SOK .NET Framework).

Po skonfigurowaniu obiektu polecenia można prowadzić działania na szereg sposobów, w zależ­ ności od natury polecenia, typu danych zwracanych przez polecenie oraz formatu, w którym dane mają być utworzone. Aby wykonać polecenia, takie jak INSEIIT, DELETE lub CREATE TABLE, które nie zwracają danych z bazy danych, należy wywołać ExecuteNonQuery. Dla poleceń UPDATE, INSERT i DELETE metoda ExecuteNonQuery zwraca wartość int, określającą liczbę wierszy, na które ma wpływ polecenie. Dla innych poleceń, takich jak CREATE TABLE, metoda Exe­ cuteNonQuery zwraca wartość - 1 . Niżej przedstawiono przykład, w którym UPDATE służy do modyfikacji rekordu. public static void ExecuteNonQue ryExample ( IDbConnect ion con ) {

li Two rzenie i kon figu rowanie nowego polecenia .

Rozdział 1 O: Dostęp do baz danych

305

IDbCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . Text ; com . CommandText = " UPDATE Employees SET Title = ' Sales D i recto r ' " " WHERE Employee!d = ' 5 ' " ;

+

li Wykonanie polecenia i p rzetwo rzenie wyników . int result = com . ExecuteNonQuery ( ) ; if ( result == 1 ) { Consol e . Writeline ( " Employee t itle updated . " ) ; } else { Console . W riteline ( " Employee title not u pdated . " ) ; } } Aby wykonać polecenie, które zwraca zestaw wyników, takich jak instrukcja SELECT lub skła­ dowana procedura, należy użyć metody

!DataReader

ExecuteReader.

Ta metoda zwraca instancję obiektu

(omówionego w przepisie l 0.5), zapewniającą dostęp do danych wynikowych.

Więkswść dostawców danych umożliwia również wykonywanie wielokrotnych poleceń SQL w pojedynczym wywołaniu metody ExecuteReader. W poniższym fragmencie kodu zastosowano metodę

ExecuteReader

do wykonania składowanej procedury



TenMostExpensiveProducts"

z bazy danych Northwind i wyświetlenia wyników na konsoli.

public static void ExecuteReaderExample ( IDbConnection con ) {

li Utwo rzenie i s konfigu rowanie nowego polecenia . IDbCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . St o redP roced u re ; com . CommandText = " Ten Most Expen sive P roduct s " ;

li Wykonanie polecenia i p rzetwo rzenie wyników us ing ( IDataRead e r reade r = com . ExecuteReade r ( ) ) { Console . Writeline ( " P rice of the Ten Most Expens ive P roducts . " ) ; while ( reade r . Read ( ) ) {

li Wyświetlenie s z czegółów p rodukt u . Console . Writeline ( "

{ 0 } = { l} " , reade r [ "TenMos t ExpensiveP rod uct s " ) ,

read e r [ " UnitPrice " ) ) ; } } } Jeżeli potrzebna jest tylko wartość z pierwszej kolumny pierwszego wiersza danych wyniko­ wych, w zapytaniu należy wykorzystać metodę ExecuteScalar. Wartość ta zostanie zwrócona jako odniesienie

object,

które należy dopasować do właściwego typu. A oto przykład.

public static void ExecuteScala rExamp l e ( IDbConnection con ) {

li Utwo rzenie i s konfigu rowanie nowego polecenia . IDbCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . Text ;

306

C#

Księga przykładów

-

com . CommandText = "SE LECT COUNT( * ) FROM Employee s " ;

li Wykonanie polecenia i zwrócenie wyników . int result = ( int ) com . ExecuteScala r ( ) ; Console . WriteLine ( " Employee count = "

+

resul t ) ;

}

Uwaga Implementacja IDbCommand w dostawcach danych Oracle i SQL implemen­ tuje również dodatkowe metody wykonania poleceń. Przepis 10.6 opisuje wykorzystanie metody ExecuteXm/Reader, dostarczonej w klasie Sq/Command. Szczegóły dotyczące dodatkowych metod ExecuteOracleNonQuery i ExecuteOracleScalar, zawartych w klasie OracleCommand, można znaleźć w dokumentacji . NET Frameworks SOK.

1 0.4 Wykorzystanie parametrów w poleceniu SQL lub procedurze składowanej Problem Chcesz przekazać argumenty składowanej procedurze lub użyć parametrów w poleceniu SQL, aby poprawić elastyczność realizacji.

Rozwiązanie Utwórz obiekt parametru właściwy dla typu obiekni polecenia, które chcesz wykonać; wszyst­ kie obiekty parametru implementują interfejs

System.Data.!DataParameter.

Skonfiguruj typy

danych obiektu parametru, ich wartości i kierunki, po czym dołącz je do zbioru parametrów obiektu polecenia przy użyciu metody

!DbCommand.Parameters.Add.

Omówienie Wszystkie obiekty polecenia nadają się do parametryzacji, można więc wykonywać następujące czynności: • ustawiać argumenty składowanych procedur, • otrzymywać zwrócone wartości składowanych procedur, • podstawiać wartości w poleceniach tekstowych podczas wykonywania programu.

Interfejs

!DataParameter reprezentuje

parametr, a każdy dostawca danych zawiera unikatową

implementację. Poniżej widnieje lista implementacji dostawców danych. • •

System. Data. Odbc. OdbcParameter System.Data. OkDb. OkDbParameter

!DataParameter

pięciu standardowych

Rozdział 1 O: Dostęp do baz danych • • •

307

System.Data. OracleClient. OracleParameter System. Data.SqlServerCe. Sq/CeParameter System. Data.Sq/Client.Sq/Parameter

Właściwości obiektu parametru opisują wszystko, co dotyczy parametru i co jest potrzebne do użycia obiektu parametru podczas wykonywania polecenia, dotyczącego danych ze źródła. Tabela 1 0-4 przedstawia właściwości najczęściej używane przy konfigurowaniu parametrów.

Tabela 1 0-4 Właściwości parametrów

Właściwość

Opis

DbTjpe

Wartość typu wyliczeniowego

System.Data.DbTjpe,

określająca typ danych

zawartych w parametrze. Powszechnie używane wartości obejmują

String,

lnt32, Date Time i Currency. Direction

Wartość typu wyliczeniowego

System.Data.ParameterDirection,

wskazująca

kierunek parametrycznego przekazywania danych; wartości uprawnione to:

lnput, lnputOutput, Output i Return Value. IsNullable ParameterName Value

Wartość boolowska, wskazująca czy parametr akceptuje wartości zerowe. Łańcuch zawierający nazwę parametru. Obiekt zawierający wartość parametru.

Aby użyć parametrów w poleceniu tekstowym, należy określić, gdzie podstawić wartości para­ metrów w obrębie polecenia·. Dostawcy danych ODBC, OLE DB i SQL Server CE udo­ stępniają parametry pozycjonowane; położenie każdego argumentu jest identyfikowane przy pomocy znaku zapytania (?). Na przykład poniższe polecenie określa dwa położenia, w które mają być podstawione wartości parametryczne.

UPDATE Employees SET Titl e

=

? WHERE Employee!d

=

?

Dostawcy danych SQL Server i Oracle zapewniają wsparcie dla nazwanych parametrów, które umożliwiają identyfikację każdego położenia parametru przy użyciu nazwy poprzedzonej przez symbol (@). Oto odpowiednie polecenie z zastosowaniem nazwanych parametrów.

UPDATE Employees SET Title

=

@title WHERE Employee!d

=

@id

Aby określić wartości parametru i użyć ich do zamiany w poleceniu, należy utworzyć obiekty parametru właściwego typu i dołączyć je do zbioru parametrów obiektu polecenia, dostępnego za pośrednictwem właściwości

Parameters.

Nazwane parametry można dołączyć w dowolnym

porządku, ale parametry pozycjonowane muszą być umieszczone w takiej kolejności, w jakiej występują w poleceniu tekstowym. Podczas wykonywania polecenia wartość każdego parametru jest wstawiana w tekst polecenia, zanim to polecenie zostanie wykonane przez źródło danych. Metoda ParameteriudCommandExampk ilustruje użycie parametrów w zdaniu UPDATE dla SQL Server. Argumenty metody

Sq/Connection

ParameterizedCommandExampk zawierają otwarte połączenie

i dwa łańcuchy. Wartości łańcuchów zostaną zastąpione w poleceniu

UPDATE

przez te parametry. Poniższy przykład zawiera dwa sposoby tworzenia obiektów parametru: metodą

IDbCommand. CreateParameter i

metodą

!DbCommand.Parameters.Add.

Można także

tworzyć obiekty parametru przy użyciu konstruktorów i konfigurować je przy użyciu argumen­ tów konstruktora lub przez ustawianie ich właściwości.

308 C#

-

Księga przykładów

public static void Pa ramete rizedCommandExample ( SqlConnect ion con , st ring employeeI D , st ring t i t l e ) { li Utwo rzenie i skonfigu rowan ie nowego polecenia zawie raj ącego dwa nazwane li pa ramet ry . SqlCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . Text ; com . CommandText = " UPDATE Employees SET Title = @t itle" " WHERE Employeeld = @id " ;

+

li Utwo rzenie obiektu SqlPa ramete r dla pa ramet ru title . SqlPa rameter p l = com . C reatePa ramete r ( ) ; pl . Pa ramete rName = "@tit le" ; p l . SqlDbType = SqlDbType . Va rCha r ; pl . Value = t i t l e ; com . Parameters . Add ( p l ) ; li Użycie s k róconej s kładni do dodania pa ramet ru id . com . Paramete rs . Add ( "@id " , SqlDbType . In t ) . Value = employeeID ; li Wykonanie polecenia i p rzetwo rzenie wyników . int result

=

com . ExecuteNonQuery ( ) ;

}

Przy używaniu parametrów do wykonywania procedur składowanych konieczne jest dostarczenie parametrów dla wszystkich argumentów wymaganych przez tę procedurę, tak wejściowych, jak i wyjściowych. Należy ustawić właściwość Direction każdego parametru zgodnie z tabelą 1 0-4; parametry są domyślnie typu lnput. Jeżeli składowana procedura zwraca wartości, parametr przej­ mujący zwrotną wartość {z właściwością Direction równą &tum Value) musi być pierwszym para­ metrem zbioru. Oto przykład wykorzystania parametrów do wykonania procedury składowanej: public static void Stored P rocedu reExample ( SqlCon nection con , st ring catego ry , st ring yea r ) { li Utwo rzenie i skonfigu rowan ie nowego polecenia . SqlCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . St o redP roced u re ; com . CommandText = " SalesByCatego ry " ; li Two rzenie obiektu SqlPa ramete r dla pa ramet ru categ o ry . com . Pa rameters . Add ( "@Catego ryName " , SqlDbType . NVa rCha r ) . Value=category ; li Two rzenie obiektu SqlPa ramete r dla pa ramet ru yea r . com . Pa rameters . Add ( "@O rdYea r " , SqlDbType . NVa rCha r ) . Value = yea r ; l i Wykonanie polecenia i p rzetwo rzenie wyników us ing ( IOataReader read e r = com . Execu teReade r ( ) ) { } }

Rozdział 1 O: Dostęp do baz danych

309

1 0.5 Przetwarzanie wyników zapytania SQL przy użyciu czytnika danych Problem Chcesz przerwarzać dane zawarte w instancji System.Data.IDataReader, zwracanej podczas wykonywania metody IDbCommand.ExecuteReader (omówionej w przepisie 1 0.3) .

Rozwiązanie Użyj pola instancji IdataReader, umożliwiającego przechodzenie pomiędzy wierszami w zesta­ wie wyników i dostęp do indywidualnych danych, zawartych w każdym wierszu.

Omówienie Interfejs IDataReader reprezentuje czytnik danych typu 'tylko do odczytu' i 'tylko do przodu'. Umożliwia on dostęp do wyników zapytania SQL. Każdy dostawca danych zawiera unikatową implementację IDataReader. Oto lista implementacji IDataReader dla pięciu standardowych dostawców danych. • • • • •

System.Data. Odbc. OdbcDataReader System. Data. OkDb. OkDbDataReader System.Data. OrackClient. OrackDataReader System. Data.Sq/ServerCe.SqlCeDataReader System.Data.SqlClient.SqlDataReader

Interfejs IDataReader rozszerza interfejs systemowy System.Data.!DataRecord. Oba te interfejsy łącznie wykazują funkcjonalność, która umożliwia dostęp zarówno do samych danych, jak i do ich struktury, zawartej w zestawie danych. Tabela 1 0-5 przedstawia niektóre powszechnie używane pola interfejsów !DataReader i IDataRecord.

Tabela 1 0-5

Pole

Powszechnie używane pola klas czytników danych

Komentarz

Wła.kiwość FieldCount Pobiera liczbę kolumn z bieżącego rzędu. lsC/osed Zwraca wartość true, jeżeli IDataReader jest zamknięty; faire, jeżeli jest w danym momencie orwarty.

Item

Zwraca object reprezentujący wartość określonej kolumny w bieżącym rzędzie. Kolumny mogą być określone przy użyciu indeksu w postaci liczby całkowitej, liczonej od zera, lub łańcucha zawierającego nazwę kolumny. Zwróconą wartość należy dopasować do odpowiedniego typu. Jest to mechanizm indeksowania dla klas czytnika danych.

Ciqg dalszy na nastrpnej stronie

310

C# - Księga przykładów Tabela 1 0-5

Pole Metoda GetData­ TypeName GetField­ Type

Powszechnie używane pola klas czytników danych

z

Ciqg dalszy poprzedniej strony

Komentarz Podaje nazwę źródła danych i typu danych dla określonej kolumny. Podaje instancję

System. Type,

reprezentującą typ danych dla wartości zawartej

w kolumnie, określonej przy użyciu indeksu w postaci liczby całkowitej, liczo­ nej od zera.

GetName

Podaje nazwę kolumny, wskazanej przy użyciu indeksu w postaci liczby całko­ witej, liczonej od zera.

GetOrdinal

Podaje liczbę porządkową kolumny, liczoną od zera, dla kolumny o określonej nazwie.

GetSchema­ Table lsDBNull

Zwraca instancję

System.Data.DataTable,

zawierającą metadane opisujące

kolumny zawarte w !DataReader. Zwraca

true,

jeżeli w określonej kolumnie w źródle danych wartość jest pusta;

w przeciwnym wypadku zwraca fa/se.

NextResult

Jeżeli !DataReaderzawiera wielokrotne zestawy wyników (ponieważ zostało wyko­ nanych wiele instrukcji),

NextResult przechodzi do następnego zestawu wyników.

Domyślnie IDataReader jest pozycjonowany na pierwszy zestaw wyników.

Read

Przesuwa czytnik do następnego rekordu. Odczytywanie zawsze zaczyna się od początku pierwszego rekordu.

Oprócz pól wyszczególnionych w tabeli 1 0-5 czytnik danych dodatkowo udostępnia zestaw metod do wyszukiwania określonych danych z bieżącego rzędu. Każda z wymienionych niżej metod pobiera argument (liczbę całkowitą), który identyfikuje indeks kolumny, z której dane

GetBoolean, GetByte, GetBytes, GetChar, GetChars, GetDateTime, GetDecimal, GetDouble, GetF!oat, GetGuid, Get!ntl6, Get!nt32, Get!nt64, Get­ String, GetValue i GetValues. powinny być zwrócone (liczony od zera) :

Czytnik danych SQL Server i Oracle zawieraja także metody wyszukiwania danych jako

SqlDataReader zawiera takie metody, jak GetSqlByte, GetSqlDecimal i GetSqlMoney, a OracleDataReader- GetOracleLob, GetOracleNum­ ber i GetOracleMonthSpan. Więcej szczegółów na ten temat można znaleźć w dokumentacji typów specyficznych dla źródła danych. Na przykład

.NET Framework SOK. Po zakończeniu korzystania z czytnika danych należy zawsze wywołać jego metodę

Close,

dzięki czemu będzie można ponownie użyć połączenia z bazą danych. Klasa !DataReader rozsze­ rza klasę System.!Disposable, co oznacza, że każda klasa czytnika danych implementuje metodę Dispose. Metoda Dispose automatycznie wywołuje Close, dzięki czemu instrukcja using jest przej­ rzystym i wydajnym sposobem na posługiwanie się czytnikami danych. Poniższy przykład pokazuje użycie czytnika danych do przetwarzania zawartości dwóch zesta­ wów wyników, zwróconych przez wykonanie wsadowego zapytania realizującego dwa zapytania SELECT. Pierwszy zestaw wyników zostaje wyliczony i wyświeclony na konsoli. Drugi zestaw jest sprawdzany pod kątem metadanych, które są następnie wyświetlane.

u s ing System ; using System . Data ;

Rozdział 1 O: Dostęp do baz danych using System . Data . SqlClient ; public c l a s s DataReaderExample { public static void Main ( ) {

li Two rzenie obiektu SqlConnection . using ( SqlConnection con = new SqlConnection ( ) ) {

li Konfigu rowanie łańcucha połączenia obiektu SqlConnection . con . ConnectionSt ring = " Data Sou rce = localhost ; " + " Database = No rthwind ; I nteg rated Secu rity=SSPI " ;

li Utwo rzenie i s konfigu rowanie nowego polecenia . SqlCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . Text ; com . CommandText = " SELECT B i rt h Date , Fi rstName , La stName FROM " + " Employees ORDER BY B i rt hDat e ; SELECT * FROM Employee s " ;

li Otwa rcie połączenia do bazy danych i wykonanie poleceń . con . Open ( ) ;

li Wykonanie polecenia i odeb ran ie obiektu SqlReade r . u sing ( SqlDataRead e r reade r

=

com . ExecuteReade r ( ) ) {

li P rzetwo rzenie pie rwszego z bio ru wyników i wyświet lenie li j ego zawa rtośc i . Console . WriteLine ( " Employee Bi rthdays ( Ęly Age ) . " ) ; while ( reade r . Read ( ) ) { Console . W riteLine ( " { 0 , 18 : D } - { l } { 2 } " , reade r . GetDateTime ( 0 ) , reade r [ " F i rstName " ] , reade r [ 2 ] ) ;

li Ret rieve typed data li Use st ring index li Use o rdinal index

}

li P rzetwo rzenie d rugiego z bio ru wyników i wyświetlenie li szczegółów o kolumnach i typach danych . read e r . NextResult ( ) ; Consol e . WriteLine ( " Employee Tabela Metadata . " ) ; f o r ( int field = 0 ; f ield < reade r . FieldCount ; field++) { Console . WriteLine ( "

Col umn Name : {0 }

read e r . GetName ( field ) , read e r . GetDataTypeName ( field ) ) ; } } }

li Oczekiwanie na kontynuacj ę . Consol e . ReadLine ( ) ; } }

Type : { l } " ,

31 1

312

C# - Księga przykładów

1 0.6 Uzyskanie dokumentu XML w zapytaniu SQL Problem Chcesz wykonać zapytanie dla serwera SQL Server 2000 lub MSDE i uzyskać wyniki sforma­ towane jako XML.

Rozwiązanie Wyspecyfikuj klauzulę FOR XML w zapytaniu SQL, aby zwrócić wyniki jako XML. Wykonaj polecenie przy pomocy metody Sq!Command.ExecuteXmlReader, która XmL.XmL&ader, zapewniający dostęp do zwróconych danych XML.

zwróci obiekt

System.

Omówienie SQL Server 2000 i MSDE umożliwiają bezpośrednie użycie XML . Należy jedynie dopisać kałuzulę FOR XML AUTO na końcu zapytania SQL, aby zaznaczyć, że wyniki powinny być zwrócone jako XML. Domyślnie reprezentacja XML nie stanowi pełnego dokumentu XML. Zamiast tego zwraca każdy rekord wyników jako oddzielny element, przy czym pola rekordu stają się atrybutami. Na przykład zapytanie:

SELECT Custome rID , CompanyName FROM C u s tome rs FOR XML AUTO zwraca XML o następującej strukturze:



Alternatywnie można dołączyć słowo kluczowe ELEMENTS na końcu zapytania, aby zestrukru­ ryzować wyniki w postaci zagnieżdżonych elementów, a nie atrybutów. Na przykład zapytanie:

SELECT Cus tome rlD , CompanyName FROM Cus tome rs FOR XML AUTO , ELEMENTS zwraca XML o następującej strukturze:

ALFKl Alf reds Futterkiste ANTON Antonio Mo reno Taque ria GOURL Gou rmet Lanchonetes

Rozdział 1 O: Dostęp do baz danych

31 3

Uwaga

Można bardziej szczegółowo dopasować format przy użyciu składni FOR XML EXPLICIT. Umożliwi to np. konwersję niektórych pól na atrybuty, a innych na elementy.

Więcej informacji na ten temat można znaleźć w SQL Server Books Online.

Poniższy przykład demonstruje użycie zdania FOR XML i metody ExecuteXml&ader do uzyski­ wania wyników w postaci XML. Można zauważyć, że aktywne połączenie nie może być użyte dla żadnego innego polecenia, gdy jest otwarty czytnik Xml&ader. Wyniki należy przetwarzać tak szybko, jak to jest tylko możliwe i zawsze zamykać czytnik Xmf&ader (więcej szczegółów dotyczących użycia klasy Xml&ader zawiera rozdział 5). using System ; using System . Xml ; using System . Data ; using System . Data . SqlClient ; public c l a s s XmlQueryExample { public static void Main ( ) {

li Two rzenie nowego obiektu SqlConnection . using ( SqlConnection con = new SqlConnection ( ) ) {

li Konfigu rowanie łańcucha połączenia obiektu SqlConnect ion . con . ConnectionSt ring = " Data Sou rce = localhost ; "

+

" Database = No rthwind ; I nteg rated Secu rity=SSPI " ;

li Two rzenie i konfigu rowanie nowego polecenia , zawie raj ącego li wa runek FOR XML AUTO . SqlCommand com = con . C reateCommand ( ) ; com . CommandType

=

CommandType . Text ;

com . CommandText = " SELECT C ustome rID , CompanyName" " FROM Custome rs FOR XML AUTO " ;

+

li Zadekla rowanie XmlReade r , aby można było s ię doń odwołać li w ostat nim wy rażeniu i sp rawd z i ć , czy j est zamknięt y . XmlReade r reader = n u l l ; t ry {

li Otwa rcie połączenia do bazy danyc h . con . Open ( ) ;

li Wykonanie polecenia i odtwo rzenie XmlReader w celu dostępu li do wyników . reade r = com . ExecuteXmlReade r ( ) ; while ( reade r . Read ( ) ) { Consol e . Write ( " Element :

" +

if ( reade r . HasAt t ributes ) {

reade r . Name ) ;

314 C# - Księga przykładów f o r ( int i = 0 ; i

<

reade r . Att ributeCount ; i++ ) {

reade r . MoveToAtt ribute ( i ) ; Console . Write ( "

{0} : { l} " , reade r . Name , reade r . Value ) ;

} li P rzeniesienie XmlReade r do węzła element u . reade r . MoveToElement ( ) ; Console . WriteLine ( ) ; } } } catch ( Exception ex ) { Console . W riteLine ( ex . ToSt ring ( ) ) ; } finally { li Sp rawdzenie , czy czytnik został zamknięty . if ( reade r ! = nul l ) reade r . Close ( ) ; } } li Oczekiwanie na kontynuacj ę . Console . ReadLine ( ) ; } } Niżej pokazano niekcóre dane wyjściowe cescowej aplikacji:

Element : C ustome rs

Cu stome rI O : ALFKI

CompanyName : Alf red s Futte rkiste

Element : C ustome rs

Cu stomerID : ANTON

CompanyName : Antonio Mo reno Taque ria

Element : Cu stome rs

Cu stome rI D : GOURL

CompanyName : Gou rmet Lanchonetes

Zarniasc wykorzystania

Xml&ader

odczytu danych XML za pomocą

i sekwencyjnego doscępu do danych, można dokonywać

System.XmLXmlDocument.

W ten sposób wszysckie dane

są wczytywane do pamięci, a połączenie z bazą danych może zostać zamknięte. Następnie można kontynuować interakcję z dokumentem XML (rozdział 5 zawiera szereg przykładów dotyczących użycia klasy Xm/Document). Oto kod, kcóry może się przydać w tym wypadku.

XmlOocument doc = new XmlDocument { ) ; li Two rzenie nowego obiektu SqlConnection . u s ing ( SqlConnection con = new SqlConnect ion ( ) ) { li Konfigu rowanie łańcucha połączenia obiektu SqlConnection . con . ConnectionSt ring = " Data Source = localhost ; " + " Database = Northwind ; Integ rated Sec u rity=SSPI " ; li Two rzenie i kon figu rowanie nowego polecenia , zawie raj ącego wy rażenie li FOR XML AUTO . SqlCommand com = con . C reateCommand ( ) ; com . CommandType = CommandType . Text ;

Rozdział 1 O: Dostęp do baz danych

31 5

com . CommandText = "SELECT Custome rID , CompanyName FROM Cus tome rs FOR XML AUTO " ;

li Otwa rcie połączenia do bazy danych . con . Open ( ) ;

li Załadowanie danych XML do XmlDocument . Na j pierw t rzeba utwo rzyć li element korzenia , w któ rym zostaną umiesz czone wie rsze wyników . XmlReade r reader

=

com . ExecuteXmlReade r ( ) ;

doc . LoadXml ( ''< resul t s> " ) ;

li Utwo rzenie XmlNode z kolej nego elementu XML odczytanego p rzez czytnik . XmlNode newNode

=

doc . ReadNod e ( reade r ) ;

while ( newNode ! = null ) { doc . Document Element . AppendChild ( newNode ) ; newNode = doc . ReadNode ( reade r ) ; } }

li P rzetwa rzanie odłączonego XmlDocument . Console . W riteLine ( doc . Oute rXml ) ;

1 O.7 Wyszukiwanie wszystkich instancji SQL Server 2000 w sieci Problem Chcesz otrzymać listę instancji SQL Server 2000, dostępnych w twojej sieci.

Rozwiązanie Wykorzystaj mechanizm COM, aby uzyskać dostęp do funkcjonalności biblioteki Microsoft

Application i wywołaj jego metodę ListAvailable­ ListAvailableSQLServers zwraca obiekt NameList, który jest uporząd­

SQLDMO Object Library. Utwórz obiekt

SQLServers.

Metoda

kowanym zbiorem łańcuchów, zawierającym nazwy wszystkich instancji SQL Server 2000 odnalezionych w sieci.

Omówienie Biblioteka klas . NET Framework nie zawiera funkcjonalności koniecznej do odnalezienia niezna­ nych serwerów SQL, ale to zadanie można wykonać przy użyciu biblioteki Microsoft SQLDMO Object Library, dostępnej za pośrednictwem COM. Przepis 1 5.6 pokazuje szczegółowo, jak utworzyć asemblację międzyoperacyjną (interop), umożliwiającą dostęp do komponentu COM. Przy korzystaniu z Microsoft Visual Studio .NET należy dodać odniesienie do biblioteki

31 6

C# - Księga przykładów Microsoft SQLDMO Object Library, wymienionej w zakładce COM okna dialogowego Add Reference. W razie braku Visual Studio .NET należy użyć Type Library Importer (Tlbimp.exe) w celu utworzenia asemblacji międzyoperacyjnej dla pliku sqldmo.dll, który zazwyczaj znajduje się w folderze Tools\Binn wewnątrz folderu instalacji serwera SQL.

Uwaga Istnieje znany problem związany z oryginalną wersją biblioteki SQLDMO Object Library, który spowoduje zakończenie się błędem przykładowego projektu. Aby urucho­ mić ten projekt, należy zainstalować SQL Server Service Pack (wersji 2 lub wyższej).

Zakładając użycie ustawień domyślnych przy generowaniu asemblacji międzyoperacyjnej, naj­

SQLDMO. Aby otrzymać listę dostępnych ser­ SQLDMO.Application i wywołać jego metodę ListAvailableSQLServers. Każdy łańcuch w zwróconym obiekcie SQLDMO.Namelist jest nazwą pierw należy zaimportować przestrzeń nazw

werów SQL, należy utworzyć instancję obiektu

dostępnego serwera SQL. Można wykorzystać te nazwy w łańcuchach połączenia albo można wyświetlić je jako listę, aby umożliwić dokonanie wyboru przez użytkownika. Oto przykład, który wyświetla na konsoli nazwy wszystkich dostępnych serwerów SQL.

u s ing System ; u s ing SQLDMO ; public class SQLDMOExample { public static void Main ( ) {

li Uzyskanie listy wszystkich dostępnych s e rwe rów SQL . SQLDMO . Application app = new SQLDMO . Application ( ) ; SQLDMO . NameList names = app . ListAvailableSQLSe rve rs ( ) ;

li P rzetwa rzanie z bio ru NameList . if ( names . Count == 0 ) { Consol e . Write Line ( " No SQL Serve rs vis ible on the netwo rk . " ) ; } else {

li Wyświet lenie listy dostępnych s e rwe rów SQL . Console . WriteLine ( " SQL Se rve rs visible

:

" + names . Count ) ;

f o reach ( st ring name in names ) { Console . WriteLine ( "

Name

} }

li Oczekiwanie na kontynuacj ę . Console . ReadLine ( ) ; } }

:

" + name ) ;

11 Programowanie sieciowe i międzysieciowe System Microsoft .NET Framework zawiera pełen zestaw klas dla programowania sieciowego w dwóch przestrzeniach nazw:

System.Net

i

System.Net.Sockets.

Klasy te wspierają wszystkie

opcje, zaczynając od programowania opanego na gniazdach TCP/IP, a na pobieraniu plików oraz stron HTML z sieci Web poprzez HTTP kończąc. Sieciowe przestrzenie nazw tworzą fim­ dament, na którym oparto dwie platformy sieciowe wyższych warstw - zdalny dostęp (Remo­ ting) oraz usługi XML w sieci Web. Materiał niniejszego rozdziału nie obejmuje tych platform, ale omawiane są one szczegółowo w Rozdziale 1 2. W tym rozdziale są przedstawione przepisy zawierające następujące koncepcje: • Przeszukiwanie zasobów w sieci Web przez HTTP (przepisy

1 1 . 1 , 1 1 .2 i 1 1 .3).

• Wyświetlanie stron Web w aplikacji Windows przy użyciu kontrolki Web Browser (przepis

1 1 .4). • Uzyskanie adresu I P oraz informacji DNS dotyczącej bieżącego komputera oraz innych

domen w sieci World Wide Web (przepisy 1 1 .5 i 1 1 .6) . • Przesyłanie komunikatów ping (przepis

1 1 .7) i łączność za pośrednictwm protokołów TCP

i UDP (przepisy 1 1 .8 do 1 1 . 1 3) . • Przesyłanie i odbieranie wiadomości e-mail (przepisy

1 1 . 1 4 i 1 1 . 1 5) .

318 C#

-

Księga przykładów

1 1 .1 Pobranie pliku poprzez HTTP Problem Chcesz mieć szybki i prosty sposób na pobranie pliku z domeny Web poprzez HTTP.

Rozwiązanie Użyj metody static Downl.oadFile klasy System.Net. WebC/ient.

Omówienie Syscem .NET Framework udostępnia kilka mechanizmów przesyłania danych poprzez HTTP.

System.Net. WebC/ient. Upl.oadFile. Te metody nie

Jednym z łatwiejszych sposobów jest wykorzystanie pomocniczej klasy Dostarcza ona metod wyższego rzędu, takich jak

Downl.oadFile

i

zawierają wbudowanych możliwości komunikacji asynchronicznej ani uwierzytelnienia. W razie potrzeby należy użyć bardziej zaawansowanej fimkcjonalności, dostarczanej p rzez klasy

Web­

Request i WebResponse, jak to podają przepisy 1 1 .2 i 1 1 .3. Poniższa aplikacja pobiera plik graficzny o nazwie banner.gif z witryny Microsoft i zapisuje go lokalnie.

u s ing System ; u sing System . Net ; us ing System . IO ; public class Download { p rivate static void Main ( ) { st ring remoteU ri = " http : llwww . mi c rosoft . comlmspresslimageslbanne r . gi f " ; st ring localFileName = " banne r . g if " ; WebClient client = new WebClient ( ) ; Console . Writeline ( " Downloading file " + remoteU ri + " to " + Path . GetFullPath ( local FileName ) ) ; li Wykonanie pob rania . cl ient . DownloadFile( remoteU ri , localFileName ) ; Console . Writeline ( " Download complete . " ) ; Console . Readline ( ) ; } }

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

319

1 1 .2 Pobranie pliku i przetwarzanie go przy użyciu strumienia Problem Chcesz uzyskać plik z witryny Web, ale nie chcesz zapamiętywać tego pliku bezpośrednio na twardym dysku. Chciałbyś natomiast przetwarzać jego dane w swojej aplikacji.

Rozwiązanie WebRequest, aby utworzyć żądanie, klasy WebResponse do odebrania odpowiedzi ser­ StreamReader dla HTML i danych tekstowych lub BinaryReader dla pliku binarnego), aby dokonać analizy danych odpowiedzi.

Użyj klasy

wera Web oraz jakiegoś czytnika (standardawo

Omówienie Pobranie pliku z sieci Web wymaga wykonania czterech podstawowych krok6w:

static Create

1 . Użycia metody

klasy

System.Net. WebRequest, aby wyspecyfikować żądaną WebRequest, w zależności od użytego typu

stronę. Ta metoda zwraca obiekt wywiedziony z

wskaźnika Uniform Resource ldentifier (URI) . Na przykład, jeżeli został użyty wskaźnik HTTP URI (ze schematem http://), zostanie utworzona instancja

HttpWebRequest.

Jeżeli

jest używany system plik6w URI (ze schematem ://), zostanie utworzona instancja Fik­

WebRequest. Dzięki właściwości WebRequest. Timeout można 2.

Użycia metody

określić limit czasowy.

GetResponse oraz obiektu WebRequest do zwrócenia obiektu WebResponse dla WebException.

strony. Jeżeli czas żądania zostanie przekroczony, wywołany będzie wyjątek

3. Utworzenia StreamReader lub BinaryReader dla strumienia WebResponse. 4. Wykonania dalszych czynności dotyczących strumienia, takich jak zapis do pliku, wyświetle­ nie w aplikacji itd. Poniższy kod pobiera i wyświetla grafikę oraz zawartość HTML strony Web. Pokazuje to rysu­ nek 1 1 - 1 . using System ; using System . Net ; using System . IO ; using System . D rawing ; using System . Windows . Fo rms ; publ i c class Download Fo rm : System . Windows . Fo rms . Fo rm { p rivate System . Windows . Fo rms . Pictu reBox picBox ; p rivate System . Windows . Fo rms . TextBox textBox ;

li ( Kod p roj ektanta pominięto ) . p rivate void Download Fo rm_ Load ( object sende r , System . EventArgs e ) {

320

C#

-

Księga przykładów st ring picUri = " htt p : llwww . mi c rosoft . comlms p re s s l imageslbanne r . gif " ; st ring htmlUri = " http : llwww . mic rosoft . comldefault . as p " ; li Two rzenie żądania . WebRequest requestPic

=

WebReques t . C reate ( picUri ) ;

WebRequest requestHtml = WebRequest . C reate ( htmlUri ) ; li Pobie ranie odpowiedz i . l i Może t o t rwać dość długo , s z czególnie w p rzypadku dużych pl ików , li gdyż odbie rana j est cała odpowiedź . WebResponse responsePic = requestPic . GetResponse ( ) ; WebResponse responseHtml = requestHtml . GetRespon s e ( ) ; li Czytanie st rumieni odpowiedzi . Image downloadedimage = Image . F romSt ream ( responsePic . GetRespon seSt ream ( ) ) ; St reamReader r = new St reamReade r ( responseHtml . GetRespon seSt ream ( ) ) ; st ring htmlContent = r . ReadToEnd ( ) ; r . Close ( ) ; li Wyświetlenie ob raz u . picBox . Image = downloadedimage ; li Wyświetlenie zawa rtości tekstowej . textBox . Text = htmlContent ; } }

MicfOsoft· Press

Rysunek 1 1 -1

Pobranie zawartości z sieci Web.

Gdy mamy do czynienia z dużymi zbiorami pobieranymi z sieci Web, można zastosować tech­ niki asynchroniczne w celu poprawienia efektywności, takie jak opisane w rozdziale 4. Można również zastosować

WebRequest.BeginGetResponse, dzięki czemu kod nie jest blokowany i wywo­

łuje procedurę wywołania zwrotnego (callback) po odebraniu odpowiedzi.

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

321

1 1 .3 Uzyskanie strony HTML z witryny wymagającej uwierzytelnienia Problem Chcesz pobrać plik z witryny Web, ale ta domena wymaga uwierzytelnienia.

Rozwiązanie Użyj klas WebRequest i WebResponse, jak to opisuje przepis 1 1 .2. Przed wystosowaniem zgłosze­ nia skonfiguruj właściwość WebR.equest. Credentials, posługując się informacją uwierzytelnienia.

Omówienie Niektóre witryny Web wymagają dostarczenia informacji uwierzytelniającej użytkownika. Przy łączeniu się przy użyciu przeglądarki informacja ta może zostać dostarczona w sposób przezro­ czysry {na przykład w lokalnej strefie intranetowej, która używa zintegrowanego uwierzytelnienia Windows), albo przeglądarka może zażądać tej informacji poprzez okno dialogowe logowania. Przy dostępie do strony w sposób programistyczny kod musi sam przekazać ce informacje. Użyta metoda zależy od typu uwierzytelnienia używanego przez witrynę Web. •

Jeśli witryna Web wymaga zastosowania uwierzytelnienia podstawowego (basie) lub skró­ conego (digest), można przesłać kombinację nazwy użytkownika i hasła, ręcznie tworząc obiekt System.Net.NetworkCredential i przypisując go do właściwości WebR.equest. Creden­ tials. Przy uwierzytelnieniu skróconym można również dołączyć nazwę domeny.



Jeżeli domena Web używa zintegrowanego uwierzytelnienia Windows, należy wykorzystać to samo podejście i ręcznie utworzyć nowy obiekt System.Net.NetworkCredential. Alcerna­ rywnie można odczytać informację o poświadczeniach bieżącego użytkownika z obiektu



Jeżeli witryna Web wymaga certyfikatu klienta, można go załadować z pliku przy użyciu klasy System.Security. Cryptography.X509Certijicates.X509Certificate i dodać do zbioru Http­

System.Net. CredentialCache.

WebR.equest. ClientCertificates. Niżej jest zamieszczony przykładowy kod, ilustrujący wszystkie trzy wyżej wymienione podejścia: using System ; using System . Net ; using System . Secu rit y . C ryptog raphy . X509Certificates ; public class DownloadWithAuthent icat ion {

li ( P rzypisanie t rzech URI ) p rivate static void Main ( ) { st ring u riBa s ic , u rilnteg rated , u riCertificate ;

li Uwie rzytelnienie użyt kownika p rzy użyciu nazwy i hasła metodą Basic . WebRequest requestA

=

WebRequest . C reate ( u riBas i c ) ;

322

C#

-

Księga przykładów requestA . C redentials

=

C redentialCache . DefaultC redentia l s ;

requestA . P reAuthenticate = t rue ;

li Logowan ie bieżącego użyt kownika p rzy użyciu zinteg rowanego li uwie rzytel niania Windows . WebRequest requestB requestB . C redential s

WebRequest . C reate ( u riinteg rated ) ;

=

=

new NetworkC redential ( " u s e rName" , " pa s sword " ) ;

requestB . P reAuthenticate

=

t ru e ;

li Uwie rzytel nienie użyt kownika za pomocą ce rtyfikatu klienta . HttpWebRequest requestC

=

( HttpWebRequest )

WebRequest . C reate ( u riC e rtificate ) ; X509Certificate cert

=

X509Certificate . C reateF romCe rtFil e ( @" c : \u se r . c e r " ) ; requestC . Cl ientCe rtificates . Add ( ce rt ) ;

li ( Pobranie i p rzetwo rzenie odpowied zi ) . } }

Uwaga Należy pamiętać, że uwierzytelnienie przy użyciu certyfikatu wymaga załado­ wania tego certyfikatu z pliku. Nie ma możliwości utworzenia obiektu X509Certificate z certyfikatu przechowywanego w magazynie certyfikatów w komputerze.

1 1 .4 Wyświetlenie strony Web w aplikacji Windows Problem Chcesz wyświetlić stronę HTML (lub dokument innego typu, wspieranego przez Microsoft Internet Explorer) w aplikacji Windows.

Rozwiązanie Użyj przeglądarki ActiveX zawartej w przeglądarce Internet Explorer, używając opakowania Runtime Callable Wrapper (RCW) .

Omówienie System .NET Framework nie zawiera kontrolek pozwalających udostępnić zawartość HTML. Jednakże taka funkcjonalność jest przydatna. Można ją wykorzystać, by wyświetlić lokalną zawartość HTML (na przykład rozbudowany dokument pomocy) lub porcję informacj i z sieci

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

323

Web Qak strona Web podająca listę plików do pobrania, które użytkownik może wykorzystać do zaktualiwwania aplikacji) . Aby pokazać stronę HTML, można dodać okno przeglądarki Internet Explorer do aplikacji Windows. Okno takie wyświetli nie tylko HTML, ale również JavaScript oraz kod Microsoft Visual Basic Scripting Edition (VBScript), kontrolki ActiveX oraz różne rozszerzenia plug-in, zależnie od konfiguracji systemu (dotyczy to takich aplikacji, jak Microsoft Word, Microsoft Excel łub Adobe Reader). Można nawet wykorzystywać kontrolkę przeglądarki Web do prze­ glądania folderów na lokalnym dysku albo wyświetlania plików w witrynie FTP (File Transfer Prococol). Aby dodać przeglądarkę Web do projektu w Microsoft Visual Studio .NET, należy prawym przyciskiem myszy kliknąć okno narzędziowe Toolbox i wybrać pozycję Add/Remove. Następ­ nie należy wybrać zakładkę COM Components i zaznaczyć kontrolkę Microsoft Web Browser (shdocvw.dll). Pozwoli to dołączyć przeglądarkę Microsoft Web Browser do okna narzędziowego Toolbox. Po przeniesieniu tej kontrolki na formularz, niezbędne asemblacje międzyoperacyjne wstaną wygenerowane i dołączone do projektu. Jeżeli nie korzysta się ze środowiska Visual Stu­ dio .NET, można wygenerować opakowanie przy użyciu Type Library Importer (Tlbimp.exe), jak co pokazano w przepisie 1 5.6. Przy korzystaniu z kontrolki przeglądarki Web dostępne są następujące metody: • • •

Navigate (i Navigate2) przekierowuje stronę do wskazanego adresu URL. GoBack i GoForward pozwala na nawigację po liście historii przeglądarki. GoHome przekierowuje na stronę domową {zdefiniowaną lokalnie), zaś GoSearch udostępnia stronę wyszukiwania.

Ponadto użytkownik może uaktywnić nawigację między stronami, klikając odsyłacze (o ile ist­ nieją). Bieżący URL można odczytać dzięki właściwości

LocationURL i określać, czy kontrolka

nadal przetwarza stronę, sprawdzając właściwość Busy. Można również reagować na różnorodne zdarzenia, w tym takie, które wstają wyzwolone przy starcie i zatrzymaniu się nawigacji. Poniższy kod przedstawia aplikację, umożliwiającą użytkownikowi odwiedzenie dwóch stron i podążanie za udostępnianymi przez nie łączami (rysunek 1 1 -2).

using System ; using System . D rawing ; using System . Windows . Fo rms ; publ ic class WebBrowser : System . Windows . Fo rms . Fo rm { p rivate AxSHDocVw . AxWebB rowse r explo re r ; p rivate System . Windows . Fo rms . Button cmdBack ; p rivate System . Windows . Fo rms . Button cmdHome ; p rivate System . Windows . Fo rms . Button cmdF o r.ia rd ; li ( Kod p roj ektanta pominięto ) . p rivate void WebBrowser_Load ( obj ect send e r , System . EventA rg s e ) { obj ect n u l lObj ect obj ect u ri

=

=

null ;

" http : llwww . mi c rosoft . com" ;

exp l o re r . Navigate2 ( ref u ri , ref nullObj ect , ref nullObj ect , ref nullOb j ect , ref nullObject ) ; }

324 C# - Księga przykładów p rivate void cmdHome_Click ( obj ect sende r , System . EventArgs e ) { explore r . GoHome ( ) ; } p rivate void cmdFo rwa rd_Click ( obj ect sende r , System . EventArgs e ) { t ry { expl o re r . GoFo rwa rd ( ) ; } cat ch { MessageBox . Show ( "Al ready on last page . " ) ; } } p rivate void cmdBack_Click ( ob j ect sende r , System . EventArgs e ) { t ry { expl o re r . GoBack ( ) ; } catch { Mes sageBox . Show ( "Al ready on f i rst page . " ) ; } } }

msdn

Mictosott



1 . r, „ , „,.., „

1.„ � . i.. .1 ,„ ( „ , 1. , „

1 . 1.,_..,

1:..,..,„1.,„.1�

l..,.1. 1 „, , ,, �

„„,, „ł' 1..,. �

...t.i „ • .k

1 ,0 . w

New T h i9 Week �JILtl!UJWlb..ttUl

�:'::d� ��'e"r!:n"!ifh!�'!f����c tor Mlcrosoft .Hn' il Micrasolt'1 entry �:�=::::o":fr:�d"af

1n

tntno to comp„w the 1alablity al the .NfT •nd l2EE eppAi�

pl��J.



, · 1 „ : 1 1-„.1• ••

w;rpames.nga.c:1ntcctwąi a,us�

;.

l(

«_J

_ _ _

Tecłricłl Ntldlt � Coct.•� � Blde lllPll & WW81"1Ce "' ....... -

:.i .

t :•

A11nouncen1ents

----------�- ...------·

Rysunek 1 1 -2

-

J

Wykorzystanie kontrolki przeglądarki Web.

Więkswść metod kontrolki przeglądarki Web wymaga całego szeregu parametrów. Ponie­ waż te metody nie są przeciążone, a język C# nie obsługuje opcjonalnych parametrów, należy dostarczyć wartość dla każdego parametru. Nie można po prostu użyć wyrażenia

null, ponieważ

są to parametry typu ref. Zazwyczaj najłacwiej jest ucworzyć zmienną obiektu zawierającą odnie­ sienie

null i dostarczyć ją dla parametrów, które nie mają być używane. Ta technika szczegółowo

zostanie zademonstrowana w przepisie 1 5.8.

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

325

1 1 .5 Uzyskanie adresu IP aktualnego komputera Problem Chcesz odczytać adres IP aktualnego komputera, na przykład w celu późniejszego użycia w kodzie połączeń sieciowych.

Rozwiązanie Użyj klasy System.Net.Dm, udostępniającej metody statyczne

GetHostName i GetHostByName.

Omówienie Dns udostępnia usługi rozwiązywania nazw domenowych. Można przywołać jej metodę GetHostName w celu wydobycia nazwy hosta dla bieżącego komputera. Uzyskaną nazwę hosta należy przetłumaczyć na adres IP przy użyciu GetHostByName. Oto przykład: Klasa

using System ; using System . Net ; public class Get I PAd d ress { p rivate static void Main ( ) {

li Wys z u kiwanie nazwy i ad resu IP host a . st ring hostName

Dns . GetHostName ( ) ;

=

li Pob ranie pierwszego pas u j ącego ad resu I P . st ring ipAdd ress Dn s . GetHostByName ( hostName ) . Ad d re s sList [ 0 ] . ToSt ring ( ) ; =

Consol e . WriteLine ( " Host name : • + hostName ) ; Console . WriteLine ( " I P add res s : Console . Read Line ( ) ;

• +

ipAdd res s ) ;

} } Należy zauważyć, że metoda

GetHostByName zwraca

listę możliwych do wykorzystania adre­

sów IP. W więkswści przypadków lista ta będzie zawierać tylko jedną pozycję. Uruchomienie tego kodu da efekt podobny do następującego:

Host name : fa riamat IP add res s : 24 . 1 14 . 13 1 . 70

Uwaga Przy użyciu adresów IP w komunikacji sieciowej w celu odwołania się do bieżą­ cego komputera można zawsze się posłużyć adresem zwrotnej pętli (loopback) 127.0.0.1, zamiast adresu IP przydzielonego danej maszynie.

326

C#

-

Księga przykładów

1 1 .6 Uzyskanie nazwy hosta dla adresu IP Problem Chcesz określić adres IP dla komputera w oparciu o jego nazwę domenową przez wykonanie zapytania DNS (Domain Name Sysrem) .

Rozwiązanie Użyj merody GetHostByName klasy System.Net.Dm i prześlij nazwę domeny jako paramerr.

Omówienie Publicznie dosrępne adresy I P w Internecie częsro są mapowane na nazwy hosrów, króre są łarwiejsze do zapamięrania. Na przykład adres I P 207 . 1 7 1 . 1 75.29 jast mapowany na nazwę domeny

www.amazon.com.

Aby określić adres I P dla danej nazwy, kompurer kontakruje się

z serwerem DNS. Całkowiry proces rozpoznawania nazwy odbywa się w sposób przezroczysry, jeżeli korzysta się z klasy wywołanie

System.Net.Dm, króra umożliwia wyszukiwanie adresu IP dla nazwy hosra poprzez GetHostByName. Niżej pokazano, jak można przeszukiwać lisrę adresów I P, mapo­

wanych na www.microsofr.com:

u s ing System ; u s ing System . Net ; public class ResolveIP { p rivate static void Main ( ) { fo reach ( I PAdd ress ip in Dns . GetHostByName ( "www . mi c rosoft . com" ) . Add res slist ) { Consol e . Write ( ip . Add ressFamily . ToSt ring ( ) Console . W riteline ( ip . ToSt ring ( ) ) ;

+

} Console . Readline ( ) ; } } Ten rest wytwarza dane wyjściowe podobne do pokazanych niżej:

Inte rNetwo rk : 297 . 46 . 249 . 222 I nte rNetwo r k : 207 . 46 . 134 . 222 Inte rNetwo rk : 207 . 46 . 249 . 27 I nte rNetwo rk : 207 . 46 . 134 . 155 Inte rNetwo r k : 207 . 46 . 249 . 190

": ");

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

327

1 1 . 7 Pingowanie adresu IP Problem Chcesz sprawdzić, cr,y komputer pracuje w trybie on-line i zmierzyć jego czas odpowiedzi.

Rozwiązanie Wyślij komunikat ping. Przesłanie go wymaga użycia protokołu Internet Control Message Pro­ tocol (ICMP) z surowym gniazdem.

Omówienie Ping łąCLy się z urządzeniem pod określonym adresem IP, przesyła komunikat testowy i oczekuje na odpowiadź w postaci pakietu echa. Aby dokonać pomiaru opóźnienia dla połączenia między dwoma komputerami, można zmierzyć czas zużyty na uzyskanie odpowiedzi ping. Mimo prostoty komunikatów ping w porównaniu z innymi typami komunikacji sieciowej, implementacja tej funkcjonalności w środowisku .NET wymaga znacznej ilości skomplikowa­ nego kodu sieciowego niskiego poziomu. Biblioteka klas .NET nie zawiera wstępnie przygoto­ wanego rozwiązania - konieCLne jest użycie surowych gniazd i stosunkowo długiego kodu. Na szczęście co najmniej jeden programista rozwiązał już problem ping: jest nim Lance Olson, projektant z Microsoft. Udostępnia on kod języka C#, który umożliwia pingowanie hosta poprzez nazwę lub adres IP i mierzy czas oczekiwania na odpowiedź (w milisekundach). Kod ten został zaadaptowany do poniższej klasy Pinger (przykład pomija część szczegółów i zarządzanie błędami). Kompletny kod jest dostępny w plikach przykładowych do tej książki.

using System ; using System . Net ; using System . Net . Socket s ; public class Pinge r { public s tatic int GetPingTime ( st ring hos t ) { int dwStart = 0 , dWStop = 0 ;

li

Utwo rzenie s u rowego gniazda .

Socket socket = new Socket ( Ad d re s s Family . InterNetwo rk , SocketType . Raw , P rotocolType . Icmp ) ;

li

Pob ranie IPEndPoint dla s e rwe ra i p rzetwo rzenie go na EndPoint .

IPHostEntry se rve rHE = Dns . GetHostByName ( host ) ; I PEndPoint ipepSe rve r = new IPEndPoint ( se rve rHE . Ad d re s s List [ 0 ] , 0 ) ; EndPoint epSe rve r = ( ipepServe r ) ;

li

Ustawienie odbio rczego punkt u końcowego dla komputera klienckieg o .

IPHostEnt ry f romHE = Dns . GetHostByName ( Dns . GetHostName ( ) ) ; IPEndPoint ipEndPoint F rom = new I PEndPoint ( f romHE . Ad d re s s List [ 0 ] , 0 ) ; EndPoint EndPointF rom = ( ipEndPoint F rom ) ;

328

C#

-

Księga przykładów

li Konst ruowanie pakietu do wysłania . int PacketSize = 0 ; IcmpPacket packet = new I cmpPacket ( ) ; for ( int j = 0 ; j

<

l ; j ++ ) {

packet . Type = ICMP_ ECHO ; packet . SubCode = 0 ; packet . CheckSum = Uintl6 . Pa rse ( " 0 " ) ; packet . Ident ifier

Uintl6 . Pa rse ( " 4 5 " ) ;

packet . SequenceNumbe r int PingData = 32 ;

= Uint l6 . Pa rse ( " 0 " ) ;

packet . Data = new Byte [ PingData ] ; for ( int i = 0 ; i

<

PingData ; i++ )

packet . Data [ i ) = ( byte ) ' # ' ; PacketSize = PingData + 8 ; Byte [ ] icmp_pkt_ buffer = new Byte [ PacketSize J ; int index = 0 ; index = Serialize ( packet , icmp_pkt_b u f fe r , PacketSiz e , PingData ) ;

li Wyl iczanie s umy kont rol nej dla pakietu . double double_length = Conve rt . ToDoubl e ( index ) ; double dtemp = Math . Ceiling ( d ou bl e_length I 2 ) ; int cksum_ b u f f e r_length = Conve rt . Toint32 ( dtemp ) ; Uintl6 [ ) cksum_buffer = new Uin t l6 [ cksum_ b u f f e r_lengt h J ; int icmp_head e r_buffer_ index = 0 ; for ( int i = 0 ; i

<

c ksum_ b u f f e r_length ; i++ ) {

cksum_bu f fe r [ i ) = BitConve rte r . ToUin t l6 ( icmp_pkt_ buffe r , icmp_heade r_ b u f f e r_ index ) ; icmp_head e r_ buffer_ index += 2 ; } Uint l6 u_ cksum = checksum ( c ks um_ b u f fe r , cksum_ bu f f e r_ length ) ; packet . CheckSum

= u_ cksum ;

li Po uzyskaniu sumy kont rolnej ponowna serializac j a pakiet u . byte [ J sendbuf = new byte [ Pac ketSize J ; index = Serialize ( packet , sendbu f , PacketSize , PingData ) ;

li Początek odliczania . dwSta rt = System . Env i ronment . TickCou n t ; socket . SendTo ( sendbuf , PacketSize , 0 , epServe r ) ;

li Odeb ranie odpowiedzi i zat rzymanie odliczania . byte [ J ReceiveBu f f e r = new byte [ 256 ] ; socket . ReceiveF rom ( ReceiveBuf f e r , 2 5 6 , 0 , ref EndPoint F rom ) ; dwStop = System . Envi ronment . T ickCount - dwS t a rt ; }

li Wyczyszczenie i zwrócenie wyliczonego czasu ping w sekundach socket . Close( ) ;

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

329

ret u rn ( in t ) dwStop ; } p rivate static int Serialize ( I cmpPacket packet , byte [ ] b u f fe r , i n t packetSize , int pingDa t a ) {

li

( Prywatną metodę s e rial izac j i pakietu pominięto ) .

} p rivate static Uintl6 checksum ( Ui n t l6 [ ] buffe r , int size ) {

li

( P rywatną metodę wylicznia sumy kont rolnej pominięt o ) .

} } public class I cmpPac ket { public byte Type ;

li

typ komunikatu

public byte SubCode ; li typ kodu pod rzędnego public Uintl6 CheckSum ; li dopeł nienie s umy kont rol nej dla st ruktu ry public Uintl6 Identifie r ;

li

public Uintl6 SequenceNumbe r ;

identyfikato r

li

numer sekwencyj ny

public byte [ J Data ; } Można użyć metody statycznej

GetPingTime zwraca

czas

Pinger. GetPingTime z adresem IP

lub nazwą domeny. Metoda

(w milisekundach), który upłynął do otrzymania odpowiedzi. Oto

kod testujący trzy strony Web:

public c l a s s PingTest { p rivate static void Main ( ) { Console . W riteline ( "Milliseconds to contact www . yahoo . com : " + Pinge r . GetPingTime ( "www . yahoo . com" ) . ToSt ring ( ) ) ; Conso le . Writeline ( "Mil l iseconds to contact www . set i . o rg :

"

+

Pinge r . GetPingTime ( "www . set i . o rg " ) . Tost ring ( ) ) ; Console . Writeline ( " Milliseconds to contact the local compute r : " + Pinge r . Get PingTime ( " 127 . 9 . 0 . 1 " ) . ToSt ring ( ) ) ; Console . Readline ( ) ; } } Test ping umożliwia sprawdzenie, czy inne komputery pracują on-line. Może być także użyteczny,

jeśli aplikacja wymaga sprawdzenia kilku różnych komputerów zdalnych, które dostarczają tę samą zawartość i określenia, który z nich zapewni najmniejsze opóźnienie komunikacji.

Uwaga Rozwiązanie oparte o ping może nie być skuteczne, jeśli zostanie zabronione przez firewall. Wiele domen obciążonych dużym ruchem sieciowym ignoruje komunikaty ping, ponieważ zabezpieczają się przed zalewem tego typu komunikatów (ping flood), co mogłoby spowodować zablokowanie serwera (a w rezultacie odmowę usługi).

330

C#

-

Księga przykładów

1 1 .8 Komunikacja przy użyciu TCP Problem Chcesz przesłać dane między dwoma kompucerami w sieci przy użyciu połączenia TCP/IP.

Rozwiązanie Jeden z komputerów (serwer) musi rozpocząć nasłuchiwanie przy użyciu klasy

Sockets. Tcplistener.

System.Net.

Kiedy zostanie ustanowione połączenie, oba komputery będą mogły się

komunikować przy użyciu tej klasy.

Omówienie TCP jest połączeniowym protokołem z weryfikacją dostarczenia, pozwalającym na komunikowa­ nie się dwóch komputerów poprzez sieć. Udostępnia wbudowaną kontrolę przepływu oraz sek­ wencjonowanie i zarządzanie błędami, dzięki czemu jest niezawodny i łatwy w programowaniu. Aby utworzyć połączenie TCP, jeden kompucer musi pracować j ako serwer i nasłuchiwać odbioru w określonym punkcie końcowym

(endpoint),

definiowanym przez adres I P identyfi­

kujący komputer oraz numer portu. Drugi komputer musi działać jako klient i wysyłać żądania połączenia do punktu końcowego, w którym nasłuchuje pierwszy komputer. Po ustanowieniu połączenia oba komputery kolejno wymieniają komunikaty. System .NET ułatwia ten proces dzięki abstrakcyjnemu strumieniowi. W rezultacie oba komputery po prostu dokonują zapisu i odczytu ze strumienia

System.Net.Sockets.NetworkStream,

aby przesyłać dane.

Uwaga Chociaż połączenie TCP zawsze wymaga serwera i klienta, obie role rów­ nie dobrze może odgrywać pojedyncza aplikacja. Na przykład w aplikacji peer-to-peer jeden wątek jest dedykowany odbiorowi napływających zgłoszeń (działanie w charakte­ rze serwera), podczas gdy inny służy do inicjowania połączeń wychodzących (działanie w charakterze klienta). W przykładach dla tego rozdziału klient i serwer są dostarczone jako oddzielne aplikacje, umieszczone w oddzielnych podkatalogach (więcej informacji zawiera plik readme.txt dołączony do przykładowego kodu).

Po ustanowieniu połącrenia TCP dwa kompucery mogą wysyłać dowolny typ danych poprrez zapisywanie ich w strumieniu

NetworkStream.

Jednakże dobrym pomysłem jest rozpoczęcie

projektowania aplikacji sieciowej od zdefiniowania protokołu na poziomie aplikacji, którego klienci i serwery będą używać do komunikowania się re sobą. Protokół taki zawiera stałe repre­ zentujące dostępne polecenia, co pozwała na wyeliminowanie z aplikacji zakodowanych na stałe łańcuchów komunikacyjnych.

namespace SharedComponent { public class Se rverMes sages {

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

331

public con st st ring AcknowledgeOK = " OK" ; public con st st ring AcknowledgeCancel = " Ca n cel " ; public con st st ring Disconnect = " Bye" ; } public class ClientMessages { public const st ring RequestConnect = " Hello" ; public const st ring Dis connect = " Bye" ; } }

Słownik zdefiniowany w tym przykładzie jest bardzo elementarny. Można dodać więcej sta­ łych w zależności od typu aplikacji. Na przykład w aplikacji transferu plików można zawrzeć komunikat klienta dotyczący żądania pliku, a wtedy serwer może odpowiedzieć potwierdze­ niem i zwrócić szczegółowe dane dotyczące pliku, takie jak jego rozmiar. Te stałe powinny być skompilowane w oddzielną asemblację biblioteki klas, do której muszą odwoływać się zarówno klient, jak i serwer. Niżej zamieszczony kod stanowi wzór podstawowego serwera TCP. Nasłuchuje on na okre­ ślonym porcie (prowadzi odbiór) , akceptuje pierwsze nadchodzące połączenie i czeka na zgło­ szenie przez klienta żądania rozłączenia. W tym momencie serwer mógłby ponownie wywołać metodę Tcplistener.AcceptTcpClient, żeby czekać na następnego klienta, ale nie robi tego: po prostu się rozłącza. using System ; using System . Net ; using System . Net . Socket s ; using System . IO ; using Sha redComponent ; public class TcpSe rverTest { p rivate static void Main ( ) {

li Two rzenie p rocesu nasł uchuj ącego na porcie 8000 . Tcplistene r l is tene r = new Tcplistene r ( IPAdd res s . Pa rse ( " l27 . 0 . 0 . l " ) , 8000 ) ; Consol e . Writeline ( "About to initialize po rt . " ) ; l is tene r . S t a rt ( ) ; Console . Writeline ( " Listening f o r a connection . . . " ) ; t ry {

li Oczekiwanie na żądanie połączenia i zwrócenie zainicj owanego li obiektu TcpClient . TcpClient c l ient = listene r . AcceptTcpClient ( ) ; Console . Writeline ( " Connect ion a ccepted . " ) ;

li Odczytanie st rumienia s ieciowego . Netwo rkSt ream st ream = client . GetSt ream ( ) ;

li Utwo rzenie Bina ryWrite r do zapisywania w st rumieniu . Bina ryWrite r w = new Bina ryWrite r ( st ream ) ;

li Two rzenie Bina ryReader do odczytywania st rumienia .

332

C#

-

Księga przykładów Bina ryReade r r = new Bina ryReade r ( s t ream ) ; if ( r . ReadSt ring ( ) == Cl ientMessages . RequestConnect ) { w . Writ e ( Se rverMe s sages . AcknowledgeOK ) ; Consol e . W riteline ( " Connect ion completed . " ) ; whi le ( r . ReadSt ring ( ) ! = Cl ientMe ssages . Disconnect ) {} Console . Writeline ( ) ; Console . W riteline ( " D i s con nect request received . " ) ; w . Writ e ( Se rverMes sages . Di s connect ) ; } else { Console . Writeline ( " Could not complete connect ion . " ) ; }

li Zamykanie gniazda połączenia . client . Close ( ) ; Consol e . Writeline ( " Connect ion closed . " ) ;

li Zamykanie pod rzędnego gniazda ( koniec nasłuchiwania żądań ) . l istene r . Stop ( ) ; Console . Writeline ( " Li stener s topped . " ) ; } catch ( Exception e r r ) { Console . Writeline ( e r r . ToSt ring ( ) ) ; } Console . Readline ( ) ; } } Zamieszczony niżej kod jest wzorem podstawowego klienta TCP. Kontaktuje się z serwerem pod określonym adresem IP i portem. W tym przykładzie został użyty adres pętli zwrotnej ( 1 27.0.0. 1 ) , kcóry zawsze wskazuje na bieżący komputer. Należy zauważyć, że połączenie TCP zawsze wymaga dwóch portów: jednego po stronie serwera, a drugiego po stronie klienta. Jed­ nakże tylko port serwera musi być wyspecyfikowany. Port klienta może być wybierany dyna­ micznie podczas wykonywania programu spośród dostępnych portów. Taki jest właśnie stan domyślny klasy

TcpClient.

us ing System ; u s ing System . Net ; u s ing System . Net . Socket s ; u s ing System . IO ; u s ing Sha redComponent ; public class TcpCl ientTest { p rivate static void Main ( ) { TcpClient client = new TcpClient ( ) ; t ry { Console . Writeline ( "Attempt ing to connect to the s e rve r "on port 8000 . " ) ; cl ient . Connect ( IPAdd res s . Pa rse ( " 127 . 0 . 0 . 1 " ) , 8000 ) ; Console . Writeline ( " Connection established . " ) ;

"

+

Rozdział 1 1 : Programowanie sieciowe i m iędzysieciowe

li Odczytanie st rumienia sieciowego . Netwo rkSt ream st ream = client . GetSt ream ( ) ;

li Two rzenie Bina ryWrit e r do zapisywania w st rumieniu . Bina ryWriter w = new Bina ryWrite r ( st ream ) ;

li Two rzenie Bina ryReade rdo odczytywania ze st rumienia . Bina ryReader r = new Bina ryReade r ( st ream ) ;

li Rozpoczęcie dialogu . w . Write ( ClientMe ssages . RequestConnect ) ; i f ( r . ReadSt ring ( ) == Se rverMessages . AcknowledgeOK) { Console . Writeline ( " Connected . " ) ; Console . Writeline ( " P ress Enter to d i s connect . " ) ; Console . Readline ( ) ; Console . Writeline ( " D i sconnecting . . . " ) ; w . W ri t e ( C lientMessage s . Disconnect ) ; else { } Console . W riteline ( " Connection not completed . " ) ; }

li Zamknięcie gniazda połączenia . client . Close ( ) ; Console . Writeline ( " Po rt closed . " ) ; } catch ( Exception e r r ) { Console . Writelin e ( e r r . ToSt ring ( ) ) ; } Console . Readlin e ( ) ; } } Oto zapis próby połączenia po stronie serwera:

About to initial ize po rt . Listening f o r a connect ion . . . Connect ion a ccepted . Connection completed . Disconnect request received . Connection closed . Listene r stopped . A co - po stronie klienta:

Attempt ing to connect to the s e rver on po rt 8000 . Connection established . Connected . P re s s Ente r to disconnect . Disconnecting . . . Port closed .

333

334

C#

-

Księga przykładów

1 1 .9 Uzyskanie adresu IP klienta z gniazda połączenia Problem Aplikacja serwerowa wymaga określenia adresu IP klienta po zaakceptowaniu połączenia.

Rozwiązanie Użyj metody AcceptSocket klasy Tcplistener, aby uzyskać klasę niższego poziomu System.Net. Sockets.Socket zamiast TcpC/ient. Użyj właściwości Socket.RemoteEndPoint, aby uzyskać adres IP klienta.

Omówienie Klasa TcpClient nie umożliwia wyszukiwania właściwości gniazda niższego poziomu ani jakiejkol­ wiek informacji o porcie i adresie IP klienta. Klasa TcpClient dostarcz.a właściwości Socket, ale jest to właściwość chroniona (protected}, a zatem nie jest dostępna dla klas, które nie są z niej wypro­ wadzone. Aby mieć dostęp do gniazda niższej warstwy, trzeba zastosować jedną z opcji: •

Utworzenie niestandardowej klasy, wyprowadzonej z TcpClient. Ta klasa może mieć dostęp do właściwości protected Socket i wydobywać ją poprzez nową właściwość. Należy wówczas używać tej klasy niestandardowej zamiast TcpClient.



Obejście klasy TcpClient przy użyciu metody Tcplistener.AcceptSocket. Można nadal używać klas BinaryWriter i BinaryReader wyższego poziomu do zapisu i odczytu danych, ale trzeba utworzyć najpierw strumień NetworkStream przy użyciu gniazda.

Niniejszy przepis wykorzystuje drugie podejście. Po nim następuje poprawiona wersja kodu serwera z przepisu 1 1 .8 (wyróżniona wytłuszczoną czcionką), który teraz został zmieniony. u s ing System ; u s ing System . Net ; u s ing System . Net . Socket s ; u s ing System . IO ; u s ing SharedComponent ; public c l a s s TcpServerTest { p rivate static void Main ( ) {

li Two rzenie nowego p rocesu nasł uchuj ącego na po rcie 8000 . TcpListener l istene r = new TcpListene r ( IPAdd res s . Parse ( " l27 . 0 . 0 . l " ) , 8000 ) ; Console . WriteLine ( "About to initialize po rt . " ) ; l istene r . St a rt ( ) ; Console . WriteLine ( " Li stening for a connection . . . " ) ; t ry {

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

li li

Oczekiwanie na żądanie połączenia i zwrócenie zainicj owanego gniazda Socket dla łącznośc i .

Socket socket = listener . AcceptSocket ( ) ;

Console . Writeline ( " Connection accepted . " ) ;

li

Two rzenie st rumienia sieciowego .

Netwo rkSt ream st ream = new NetworkSt rea• ( socket ) ;

li Two rzenie Bina ryWrite r do zapisywania w st rumieniu . Bina ryWrite r w = new Bina ryWrit e r ( s t ream ) ;

li Two rzenie Bina ryReade r do odczytywania ze st rumienia . Bina ryReade r r = new Bina ryReade r ( s t ream ) ; if ( r . ReadSt ring ( ) == C l ientMes sages . RequestConnec t ) { w . Write ( Se rve rMessages . AcknowledgeOK ) ; Console . Writeline ( " Connection completed . " ) ;

li

Odczytanie IP klienta .

Console . Writeline ( "The ctient is f ro• IP addres s : "

+

( ( IPEndPoint ) socket . Re110teEndPoint ) . Address . ToString ( ) ) ; Console . Writ e ( "The client uses local port :

"

+

( ( IPEndPoint ) socket . RemoteEndPoint ) . Port . ToString ( ) ) ;

while ( r . ReadSt ring ( ) ! = ClientMessages . Disconnect ) {} Consol e . W riteline ( ) ; Console . Writeline ( " D i s connect request received . " ) ; w . Write ( Se rv e rMes sages . Disconnect ) ; } else { Consol e . Writeline ( " Could not complete connection . " ) ; }

li Zamknięcie g n iazda połączenia . socket . Close ( ) ;

Console . Writeline ( " Connection closed . " ) ;

li Zamknięcie pod rzędnego gniazda ( koniec nasł u c h iwania żądań ) . l istene r . Stop ( ) ; Console . Writeline ( " Listene r stopped . " ) ; } catch ( Exception e r r ) { Console . WriteLine ( e rr . ToSt ring ( ) ) ; } Console . Readline ( ) ; } }

335

336

C#

Księga przykładów

-

1 1 .1 O Ustawienie opcji gniazda Problem Chcesz włączyć opcje gniazd niższego poziomu, takie jak te, które specyfikują limity czasowe na wysyłanie i otrzymywania informacji.

Rozwiązanie Użyj metody

Socket.SetSocketOption.

Możesz określić właściwości gniazda, które są używane

do nasłuchiwania zgłoszeń łub te, które są używane w konkretnej sesji klienta.

Omówienie Można wykorzystać metodę

Socket.SetSocketOption,

aby ustawić kilka właściwości gniazd niż­

szego poziomu. Przy wywołaniu tej metody należy przekazać następujące parametry: • Wartość typu wyliczeniowego

SocketOptionlevel,

określająca typ gniazda, którego dotyczy

ustawienie (na przykład TCP, UDP itd.). • Wartość typu wyliczeniowego

SocketOptionName, określająca aktualnie modyfikowane SocketOptionName można znaleić w dokumentacji

ustawienie socketu. Pełną listę wartości .NET Framework.

• Wartości reprezentującej nowe ustawienie. Zazwyczaj jest to liczba całkowita, ale może być

także tablica bajtowa albo typ obiektu. Oto przykład, w którym wstaje ustawiony wysyłany limit czasowy dla gniazda:

li Ope rac j e wysyłania zostaną p rzerwane z powodu p rzekroczenia czasu , j eżeli li potwierdzenie n ie zostanie odeb rane w ciągu 1000 milisekund . socket . SetSocketOpt ion ( SocketOpt ionLevel . Socket , SocketOpt ionName . SendTimeout , 1009 ) ; Należy zauważyć, że aby uzyskać dostęp do gniazda reprezentującego połączenie klient/serwer, należy użyć metody

Tcplistener.AcceptSocket zamiast

metody

Tcplistener.AcceptTcpClient,

jak

to omówiono w przepisie 1 1 .9. Można również ustawić opcje gniazda na to gniazdo, które jest używane przez

Tcplistener do

monitorowania żądań połączenia. Jednakże trzeba w tym celu przedsięwziąć kilka dodatkowych kroków.

Tcplistener dostarcza właściwości Socket, ale jest ona chroniona (protected}, co oznacza,

że nic można do niej uzyskać dostępu bezpośrednio. W tym wypadku należy wyprowadzić nową klasę z

Tcplistener, jak to pokazano w poniższym przykładzie:

public cla s s CustomTcplistene r : Tcplistener { public Socket Socket { get { retu rn base . Se rve r ; } } public CustomTcpListene r ( I PAd d re s s ip , int po rt ) }

base ( ip , port ) { }

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

337

Teraz można użyć tej klasy przy tworzeniu TcpListener. Oto przykład posługiwania się tą tech­ niką przy ustawianiu opcji gniazda: CustomTcplistene r l istene r

=

new CustomTcplistene r ( I PAdd res s . Pa rs e (

" 127 . 0 . 0 . 1 " ) , 8000 ) ; listene r . Socket . SetSocketOpt ion ( SocketOptionlevel . Socket , SocketOpt ionName . ReceiveTimeout , 1000 ) ; li ( Te raz można użyć CustomTcpListene r tak samo j ak Tcplistene r . )

1 1 .1 1 Utworzenie wielowątkowego serwera TCP Problem Chcesz utworzyć serwer TCP, który mógłby jednocześnie obsługiwać wielu klientów TCP.

Rozwiązanie Użyj metody AcceptTcpClimt dla klasy TcpListener. Za każdym razem, gdy zostaje dołączony nowy klient, rozpocznij nowy wątek zarządzania zgłoszeniem i wywołaj ponownie TcpListener.

AcceptTcpClimt.

Omówienie Pojedynczy punkt końcowy TCP (adres IP i pon) może służyć wielu połączeniom. W istocie większość pracy wykona system operacyjny. Wszystko, co należy zrobić, to utworzyć obiekt roboczy na serwerze, który będzie zarządzał każdym połączeniem w oddzielnym wątku. Rozważmy podstawowe klasy klienta i serwera TCP, przedstawione w przepisie 1 1 .8. Można z łatwością przekonwertować serwer w serwer wielowątkowy, obsługujący wiele równoczesnych połączeń. Najpierw jednak trzeba utworzyć klasę, która oddziałuje interakcyjnie z indywidual­ nym klientem, jak to pokazano w poniższym kodzie: using System ; using System . Net ; using System . Net . Socket s ; using System . IO ; using System . Th reading ; using Sha redComponent ; public class ClientHand l e r { p rivate TcpClient client ; p rivate st ring I D ; public C lientHand l e r ( TcpClient client , s t ring I D ) { thi s . client this . ID

=

=

client ;

ID ;

} public void Sta rt ( ) { li Odczytanie st rumienia s ieciowego .

338

C#

-

Księga przykładów Netwo rkSt ream st ream = c l ient . GetSt ream ( ) ;

li

Two rzenie Bina ryWrite r do zapi sywania w s t rumieniu .

Bina ryWriter w = new Bina ryWrite r ( st ream ) ;

li

Two rzenie Bina ryReade rdo odczytywania ze st rumienia .

Bina ryReade r r = new Bina ryReade r ( st ream ) ; if ( r . ReadSt ring ( ) == Cl ientMessages . RequestConnect ) { w . Write ( Se rve rMes sage s . AcknowledgeOK ) ; Console . WriteLine ( ID + " : Connection completed . " ) ; while ( r . ReadSt ring ( ) ! = C l ientMes sages . Di sconnect ) { } Console . WriteLine ( I D + " : Dis connect request received . " ) ; w . Write ( Serve rMessages . Discon nect ) ; } else { Console . WriteLine ( ID + " : Could not complete connect ion . " ) ; }

li

Zamknięcie gniazda połączenia .

cl ient . Close ( ) ; Console . WriteLine ( ID + " : Client connection closed . " ) ; Console . ReadLine ( ) ; } } Następnie należy zmodyfikować kod serwera tak, żeby w sposób ciągły wykonywał pętlę, wytwa­ rzając nowe instancje

ClientHandler i

umieszczał je w nowych wątkach. Oto zmodyfikowany

kod dla tego zagadnienia:

public class TcpServerTest { p rivate static void Main ( ) { TcpListener listene r = new TcpListene r ( I PAdd ress . Pa rse ( " l27 . 0 . 0 . 1 " ) , 8000 ) ; Console . W riteLine ( " Se rve r : About to initialize po rt . " ) ; listene r . St a rt ( ) ; Console . WriteLine ( " Se rve r : Listening f o r a connection . . . " ) ; int cl ientNum = 0 ; while ( t ru e ) { t ry {

li li

Oczekiwanie na żądanie połączenia i zwrócenie zainicj owanego TcpCl ient dla łączności .

TcpClient client = l istene r . AcceptTcpClien t ( ) ; Consol e . W riteLine ( " Se rve r : Connection accepted . " ) ;

li

Two rzenie nowego obiektu do obsłużenia połączenia .

cl ientNum++ ; ClientHandler handler = new ClientHand le r ( client , " C l ient " + clientNum . ToSt ring ( ) ) ;

li

U ruchomienie obiektu w innym wątku .

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

339

Th read handle rTh read = new Th read ( new Th readSta rt ( handle r . Sta rt ) ) ; handlerTh read . I sBackg round = t rue ; handle rTh read . St a rt ( ) ;

li ( Można również dodać obiekty Hand l e r o raz Handl e rTh read do li zbioru w celu ś ledzenia ses j i kl ienta ) . } catch ( Exception e r r ) { Console . Writeline ( e r r . ToSt ring ( ) ) ; } } } }

Poniższy listing pokazuje zapis (po stronie serwera) sesji z dwoma klientami: Serve r : About to initial ize po rt . Serve r : Listening fo r a connection . . . Serve r : Connect ion a ccepted . Client 1 : Connection completed . Se rve r : Connection a ccepted . Client 2 : Connection completed . Client 2 : Dis connect request received . Client 2 : Client connection closed . Client 1 : D i s connect request received . Client 1 : Cl ient connection closed .

Można dołączyć dodatkowy kod na serwerze sieciowym, by śledził zbiór bieżących obiektów roboczych. Rozwiązanie takie pozwoli serwerowi na porzucenie zadań, które powinny zostać zatrzymane, jeżeli wymagane jest ograniczenie maksymalnej liczby jednoczesnych klientów.

1 1 .1 2 Asynchroniczne używanie TCP Problem Chcesz zapisać dane do strumienia sieci z prędkością jednej „porcji" w danym momencie bez blokowania reszty swojego kodu. Ta technika może być używana wtedy, gdy chcesz przesłać poprzez sieć duży plik w postaci strumienia.

Rozwiązanie Utwórz oddzielną klasę, która będzie zarządzać asynchronicznym strumieniowaniem danych. Możesz zacząć co zadanie, wysyłając do strumienia blok danych przy użyciu metody Network­ Stream.Begin Write i dostarczając metodę wywołania zwrotnego. Po jej wyzwoleniu zostanie wysłany następny blok.

340

C# - Księga przykładów

Omówienie Strumień NetworkStream zawiera podstawowe wsparcie dla pracy asynchronicznej w postaci metod Begin&ad i Begin Wn'te. Przy użyciu tych metod można wysyłać i otrzymywać bloki danych w jednym z wątków udostępnianych przez pulę wątków modułu runtime, bez blokowa­ nia własnego kodu. Ten przepis demonstruje technikę asynchronicznego zapisu. W trybie asynchronicznym należy przesyłać surowe dane binarne (tablice bajtów). Decyzja co do wielkości bloków danych odbieranych lub wysyłanych w jednej porcji należy do pro­ jektanta. W poniższym przykładzie pokazano nową wersję wielowątkowego serwera z przepisu 1 1 . 1 1 , w którym każda klasa ClientHandler wysyła dużą ilość danych odczytywanych z pliku. Dane te są przesyłane asynchronicznie, co oznacza, że ClientHandler mógłby wykonywać w tym czasie inne zadania (w przykładzie ClientHandkr po prostu odpytuje strumień sieciowy w poszu­ kiwaniu komunikatów nadesłanych przez klienta) . Zaletą tego podejścia jest to, że zawartość pliku nigdy nie jest w całości umieszczana w pamięci; jest on odczytywany dopiero wtedy, gdy ma być wysłany kolejny blok. Inną zaletą jest możliwość przerwania operacji przez serwer w dowolnym momencie. W poniższym przy­ kładzie klient odczytuje tylko jedną trzecią danych przed rozłączeniem - w danym momencie serwer ustawia zmienna boolowską o nazwie fikStop, aby przekazać do wywołania zwrotnego, że inne dane nie będą wysyłane. Oto zmodyfi kowana klasa ClientHandler. Należy zauważyć, że NetworkStream i FikStream są teraz śledzone jako zmienne członkowskie, dzięki czemu metoda wywołania zwrotnego może mieć do nich dostęp. Klasa TcpServerTest nie wymaga żadnych zmian. us ing System ; us ing System . Net ; using System . Net . Socket s ; using System . IO ; using Sha redComponent ; public class ClientHandle r { p rivate TcpClient cl ient ; p rivate st ring I O ;

l i Ilość danych zapisywanych w j ednym bloku ( 2 KB ) . p rivate int bu ffe rSize = 2048 ;

li Buf o r p rzechowuj ący dane do zapisania . p rivate byte [ J buffe r ;

l i Odczytywanie danych z pliku . p rivate FileSt ream fileSt ream ;

li Komunikac j a z klientem . p rivate Netwo rkSt ream netwo rkSt ream ;

li Sygnał zat rzymu j ąc y wysyłanie danych . p rivate bool fileStop = false ; public ClientHandle r ( TcpClient client , st ring I O ) { this . b u f f e r = new byte [ bu f fe rSize ] ; this . client = client ;

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

341

this . ID = ID ; } public void Start ( ) {

li Odczytanie s t rumienia s ieciowego . networkSt ream = client . GetSt ream ( ) ;

li Two rzenie obiektów do wysyłania i odbie rania tekstu . Bina ryWrit e r w = new Bina ryWrite r ( netwo rkSt ream ) ; Bina ryReader r = new Bina ryReade r ( netwo rkSt ream ) ; if ( r . ReadSt ring ( ) == Cl ientMe ssages . RequestConnect ) { w . Writ e ( Se rve rMe s sages . AcknowledgeOK ) ; Console . Writeline ( ID st ring message = " " ;

+

"

:

Connection completed . " ) ;

while ( mes sage ! = Cl ientMe ssages . Disconnec t ) { message = r . ReadSt ring ( ) ; if ( mes sage == ClientMessage s . RequestData ) {

li Nazwa pliku może być dostarczona p rzez klient a , l i a l e w p rzykładzie nazwa pliku j est zakodowana na stałe . fileSt ream = new FileSt ream ( " test . bin " , FileMode . Open ) ;

li Wysłanie wie l ko ś c i pliku ( aby klient wiedz iał , ile li danych ma odczytać ) . w . Writ e ( fileSt ream . Lengt h . ToSt ring ( ) ) ;

li Ta metoda u ru chomi async h roniczne działanie . St reamData ( nu ll ) ; } } fileStop = t rue ; Console . Writeline ( I D } else { Console . Writeline ( ID

+ "·

Discon nect request received . " ) ;

+ "·

Could not complete connection . " ) ;

}

li Czys zczen ie . client . Close ( ) ; Console . W riteline ( ID + " · Cl ient connect ion closed . " ) ; Console . Read line ( ) ; } p rivate void St reamData ( IAsyncResult a syncResul t ) {

li P rze rwanie , j eżeli klient się rozłączył . if ( fileStop == t rue ) { fileStop = false ; retu rn ; } if ( asyncResult ! = null ) {

342

C#

-

Księga przykładów

li Jeden blok został zapisany asynch ronicznie . networkSt ream . EndWri t e ( asyncResult ) ; }

li Pob ranie następnego bloku z pliku . int bytesRead

=

fileSt ream . Read ( buffe r , 0 , buffe r . Lengt h ) ;

li Jeżeli nie odczytano żadnych baj tów , st rumień dota rł do końca pliku . if ( bytesRead > 0 ) { Consol e . W riteLine ( " St reaming new bloc k . " ) ;

li Zapisanie kolej nego bloku do st rumienia s ieciowego . netwo rkSt ream . BeginWrite ( bu f f e r , 0 , buffe r . Leng t h , new AsyncCallbac k ( S t reamData ) , null ) ; } else {

li Zakończenie operacj i . Console . WriteLine ( " File st reaming complete . " ) ; fileSt ream . Close ( ) ; } } } Podobnego schematu można użyć do asynchronicznego odczytywania danych po stronie klienta.

1 1 .1 3 Komunikacja przy użyciu UDP Problem Chcesz wysłać dane między dwoma komputerami w sieci przy użyciu strumienia UDP (User Datagram Protocoł).

Rozwiązanie Użyj klasy System.Net.Sockets. UdpClient i wykorzystaj dwa wątki: do wysyłania i otrzymywania danych.

Omówienie UDP jest bezpołączeniowym protokołem, który nie zawiera kontroli przepływu ani spraw­ dzania błędów. W przeciwieństwie do TCP, UDP nie może być używany tam, gdzie wyma­ gana jest wiarygodna komunikacja. Jednakże, ze względu na mniejszą nadmiarowość, UDP jest często używane do „gadatliwych" aplikacji, w których dopuszczalne jest zagubienie części komunikatów. Można sobie na przykład wyobrazić sieć, w której indywidualni klienci co kilka

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

343

minut przesyłają do serwera informacje o aktualnej temperaturze w ich miejscach zamieszkania. W tym wypadku można użyć UDP, gdyż natężenie komunikacji jest bardzo wysokie, a szkody

spowodowane przez utracenie pakietu są niewielkie (serwer może kontynuować pracę, używając ostatnio nadesłanego odczytu temperatury). Pokazana niżej aplikacja wykorzystuje dwa wątki: jeden służy do otrzymywania komuni­ katów, a drugi do ich wysyłania. W celu przetestowania tej aplikacji należy załadować dwie instancje w tym samym czasie. Na komputerze A należy wyspecyfikować adres IP komputera B, a na komputerze B adres IP komputera A. Teraz można przesyłać komunikaty tekstowe tam i z powrotem na życzenie (test można zasymulować na pojedynczym komputerze przy użyciu dwóch różnych portów i adresu pętli zwrotnej IP 1 27.0.0. 1 ). -

using System ; using System . Text ; using System . Net ; using System . Net . Socket s ; using System . Th reading ; public class UdpTest { p rivate static int local Po rt ; p rivate static void Main ( ) {

li Definiowan ie pun któw końcowych , do któ rych będą wysyłane komunikaty . Console . Write ( " Connect to I P : " ) ; st ring I P = Console . ReadLine ( ) ; Consol e . Write ( " Connect to po rt : " ) ; int po rt = I nt32 . Pa rse ( Consol e . ReadLine ( ) ) ; I PEndPoint remoteEndPoint = new I PEndPoint ( I PAdd re s s . Pa rs e ( I P ) , port ) ;

li Definiowanie lokalnego zakończenia ( odbie raj ącego komunikaty ) . Console . Write ( " Local po rt f o r l is tening : " ) ; localPo rt = I nt32 . Pa rs e ( Console . ReadLine ( ) ) ; Consol e . WriteLine ( ) ;

li Two rzenie nowego wątku do odbie rania komunikatów . Th read receiveThread = new Th read ( new Th readSta rt ( ReceiveDa t a ) ) ; receiveThread . I sBackg round = t rue ; receiveThread . St a rt ( ) ; UdpCl ient cl ient = new UdpClient ( ) ; t ry { s t ring text ; do { text = Console . Read Line ( ) ;

li Wysłanie tekstu do klienta zdalnego . if ( text ! = " ) { "

li Kodowanie danych bina rnie p rz y użyciu UTFS . byte [ l data = En coding . UTFS . GetBytes ( text ) ;

344

C#

-

Księga przykładów

li Wysłanie tekstu do klienta zdalnego . clien t . Send ( data , dat a . Lengt h , remoteEndPoi nt ) ; } } while ( text ! = " " ) ; } catch ( Exception e r r ) { Console . Writeline ( e r r . ToSt rin g ( ) ) ; } Consol e . Readline ( ) ; } p rivate static void ReceiveData ( ) { UdpClient cl ient = new UdpCl ient ( localPo rt ) ; while ( t ru e ) { t ry {

li Odbieranie baj tów . IPEndPoint anyIP = new I PEndPoint ( I PAdd res s . Any , 0 ) ; byte [ ] data = c lient . Receive ( ref anyIP ) ;

li Konwe rtowanie baj tów na tekst p rzy użyciu UTF8 . st ring text = Encoding . UTF8 . Get5t ring ( data ) ;

li Wyświetlenie odeb ranego tekst u . Console . W riteline ( ">> "

+

text ) ;

} catch ( Exception e r r ) { Console . W riteline ( e rr . ToSt ring ( ) ) ; } } } }

Należy zauważyć, że w przeciwieństwie do aplikacji TCP, aplikacje UDP nie mogą używać abs­ trakcji NetworkStream. Muszą one dokonać konwersji wszystkich danych na strumień bajtów, używając klasy dekodującej, jak to podano w przepisie 2.2. Można testować tę aplikację z klientami na lokalnym komputerze przy użyciu adresu 1 27.0.0. l , przy założeniu, że zostały użyte dwa różne porty odbioru. Można sobie na przykład wyobrazić sytuację z dwoma klientami UDP, o nazwie klient A i klient B. Oto przykładowy zapis sesji dla klienta A: Connect to I P : 127 . 0 . 0 . 1 Connect to po rt : 8001 Local port for listening : 8080 Hi the re ! A oto odpowiedni fragment dla klienta B (po otrzymaniu komunikatów):

Connect to IP : 127 . 0 . 0 . 1 Connect to port : 8080 Local port for l istening : 8001 »

Hi there !

Rozdział 1 1 : Programowanie sieciowe i międzysieciowe

345

1 1 .1 4 Wysyłanie wiadomości e-mail poprzez SMTP Problem Chcesz wysłać e-mail na adres internetowy przy użyciu serwera SMTP (Simple Mail Transfer Protocol).

Rozwiązanie Użyj klasy SmtpMail i

MailMessage z przestrzeni nazw System. Web.Mail.

Omówienie Klasy w przestrzeni nazw

-bones)

System. Web.Mail zapewniają opakowanie

(bare­

typu szkieletowego

dla obiektów danych współpracujących z komponentem Collaboration Data Objects

for Windows 2000 (CDOSYS). Pozwalają one na utworzenie i przesłanie sformatowanych wia­ domości e-mail przy użyciu SMTP. Używanie tych typów jest proste. Należy utworzyć obiekt

MailMessage,

wyspecyfikować

nadawcę i odbiorcę adresu e-mail i umieścić zawartość komunikatu we właściwości

Body.

MailMessage myMes sage = new MailMessage ( ) ; myMe ssage . To = " someone@somewhe re . com" ; myMes sage . F rom = " me@somewh e re . com" ; myMe s sage . Subj ect = "Hel lo " ; myMes sage . P riority = Mail P riorit y . High ; myMes sage . Body = "This is the mes sage ! " ; Można wysłać wiadomość w formacie HTML poprzez zmianę formatu komunikatu i użycie etykiet HTML. myMessage . BodyFo rmat = Mail F o rmat . Html ; myMe ssage . Body = @ " "

+

@ " This is the message ! " ; Można również dodać załączniki przy użyciu zbioru

MailMessageAttachments

i klasy

Mail­

Attachment. MailAttachment myAttachment = new MailAttachment ( " c : \\mypic . gi f " ) ; myMe ssage . Attachment s . Add ( myAttachmen t ) ; Aby przesłać wiadomość, należy podać nazwę serwera SMTP i wybrać metodę SmptMaiLSend. SmtpMail . Smt pSe rve r = " test . ma i l s e rve r . com " ; SmtpMail . Sen d ( myMes sage ) ; Istnieje pewien problem, jeżeli chodzi o użycie klasy SmtpMail do wysyłania wiadomości e-mail. Klasa ta wymaga lokalnego serwera SMTP lub serwera przekazującego (relay), pracującego w sieci. Poza tym klasa SmtpMail nie wspiera uwierzytelnienia, tak więc jeśli jakiś serwer SMTP wymaga

346

C#

-

Księga przykładów

podania nazwy użytkownika i hasła, nie h



Innym popularnym sposobem jest nadpisanie metody lnitializelifetimeService w taki sposób, by zdalny obiekt przejął kontrolę nad swoim czasem życia. Podany niżej przykład pokazuje kod, który trzeba dodatkowo dołączyć do zdalnej klasy, aby zapewnić jej 1 0-minutowy czas życia i 5-minutowy czas odnawiania. public ove r ride obj ect InitializeLifet imeSe rvice ( ) { ILease lease = MyBase . I nitializeLifetimeService ( ) ; // Dzie rżawa może być skonfigu rowana tylko wted y , gdy znaj duj e s ię w stanie

li początkowym . if ( lease . Cu r rentState == LeaseState . I n itial ) { lease . I nit ial LeaseTime = TimeSpan . F romMinutes ( 19 ) ; lease . RenewOnCallTime = TimeSpan . F romMinute s ( S ) ; } ret u rn lease ; }

Jeżeli obiekt ma mieć nielimitowany czas życia, należy zwrócić odniesienie null zamiast obiektu !Lea.se. Przypadek taki występuje najczęściej wtedy, gdy tworzony jest obiekt jednokrotnego wywołania, który musi działać niezależnie (i permanentnie), nawet gdy klienci go nie używają.

1 2.1 2 Zarządzanie wersjami zdalnych obiektów Problem Chcesz utworzyć komponent hosta, w którym można umieścić więcej niż jedną wersję danego obiektu.

Rozdział 1 2: Usługi Web XML i zdalny dostęp

375

Rozwiązanie Zainstaluj wszystkie wersje obiektu w pamięci GAC i zarejestruj jawnie każdą z nich pod róż­ nymi punktami końcowymi identyfikatora URI .

Omówienie System .NET Remoting nie zawiera wbudowanych możliwości sterowania wersjami obiektów. Kiedy klient rworzy zdalny obiekt, komponent hosta automatycznie użyje wersji dostępnej w lokalnym katalogu lub w przypadku asemblacji współdzielonej, najnowszej wersji zawartej w GAC. Wersjonowanie można zrealirować jednym z trzech sposobów: • Utworzyć oddzielne aplikacje komponentu hosta. Każdy komponent hosta będzie miał

odrębną wersję asemblacji zdalnego obiektu i wersję tę zarejestruje z różnymi identyfika­ torami URI. Takie podejście zmusza do jednoczesnego działania wielu aplikacji hosta i jest najbardziej praktyczne przy stosowaniu hostów IIS Qak to pokazano w przepisie 1 2.9). • Utworzyć zupdnie nową asemblację zdalnego obiektu (zamiast prostej zmiany wersji).

Można wtedy zarejestrować klasy z obu asemblacji pod różnymi identyfikatorami URI, przy użyciu tego samego komponentu hosta. • Zainstalować wszystkie wersje asemblacji zdalnego obiektu w GAC. Następnie można

urworzyć komponent hosta, który mapuje różne identyfikatory URI do określonych wersji asemblacji zdalnego obiektu. Ostatnia opcja jest najbardziej elastyczna w przypadku dużej liczby wersji. Rozważmy plik konfiguracyjny, rejestrujący dwie wersje asemblacji

RemoteObjects

w dwóch różnych punk­

tach końcowych. Warto zauważyć, że konieczne jest wykorzystanie dokładnego numeru wersji i tokenu klucza publicznego przy używaniu asemblacji z GAC. Informacje te można uzyskać przeglądając asemblacje za pomocą dodatku (plugin) Windows Explorer GAC (dostępny w C:\[WindowsDir] \Assembly).

< ! - - I n f o rmac j e o t ypie są podzielone na dwa wie rsze ze względu na konieczność dopasowania do f o rmatu s t rony książki . W pliku konfigu racyj nym info rma c j e te muszą być umieszczone w j ednym wie rsz u . - - >

376

C#

-

Księga przykładów

Plik konfiguracyjny klienta nie zmieni się (poza aktualizacją identyfikatora URI w razie potrzeby) . Klient „wybiera" żądaną wersję poprzez użycie odpowiedniego identyfikatora URI .

1 2.1 3 Utworzenie metody jednokierunkowej korzystającej z usług XML Web lub zdalnego dostępu Problem Chcesz, żeby metoda Web lub komponent zdalny wykonywały długie zadanie, ale nie chcesz zmuszać klienta na oczekiwanie podczas wykonywania tego kodu.

Rozwiązanie SoapDocumentMethod lub atrybutu One�y na true. Utwórz jednokierunkową zastosowanie atrybutu One �y z przestrzeni

Utwórz jednokierunkową metodę Web przez zastosowanie

SoapRpcMethod i

ustawienie właściwości atrybutu

metodę zdalnego dostępu (Remoting) poprzez nazw System.Runtime.Remoting.Messaging.

Omówienie Mając do dyspozycj i metody jednokierunkowe, klient przesyła komunikat żądania, a serwer odpowiada natychmiast, wskazując tym samym, że metoda została rozpoczęta. Takie zachowa­ nie ma następujące konsekwencje: • Klient nie musi czekać, gdy wykonuje się kod serwera. 8 Metoda nie może zwrócić żadnej informacji do klienta: ani poprzez wartość zwrotną, ani

przez parametr

ByRef

• Jeżeli metoda wywoła wyjątek nie dający się obsłużyć, nie zostanie on przesłany z powrotem

do klienta. Jednokierunkowe metody nie są więc odpowiednie, jeżeli klient chce dostać jakąś informację z serwera. Są natomiast doskonałym sposobem prostego uruchomienia niektórych typów zadań po stronie serwera (na przykład rozpoczęcia przetwarzania wsadowego) . Aby utworzyć jednokierunkową metodę Web, należy zastosować atrybut

Method (z

przestrzeni nazw

System. Web.Services.Protocols)

SoapDocument­

do odpowiedniej metody i ustawić

Rozdział 1 2: Usługi Web XML i zdalny dostęp właściwość

One�y na true.

377

Oto przykład usługi XML Web, która udostępnia dwie metody,

a każda z nich powoduje opóźnienie I O sekund. Jedna z tych metod korzysta z atrybutu

One �y,

dzięki czemu klient nie odczuje czasu oczekiwania.

using System ; using System . Web . Se rvices ; using System . Web . Se rvices . P rotocols ; public c l a s s OneWayTestWebSe rvice { [ WebMethod ( ) ] public void DoLongTa s kWit hWait ( ) {

li ( u ruchomienie długiego zadania i spowodowanie oczekiwania klient a ) . Delay ( le ) ; } [ WebMethod , SoapDocumentMethod ( OneWay=t rue ) ] public void DoLongTaskWithoutWait ( ) {

li ( u ruchomienie dł ugiego zadania bez oczekiwania klienta ) . Delay ( 10 ) ; } p rivate void Delay ( int second s ) { DateTime c u r rentTime = DateTime . Now ; while ( DateTime . Now . Subt ract ( cu r rentTime ) . TotalSecond s < second s ) { } } } W cym przykładzie przyjęto założenie, że zarówno usługa XML Web, jak i klient używają kodo­ wania SOAP (opcja domyślna). Jeżeli korzysta się z kodowania właściwego dla zdalnego wywo­ ływania procedur (Remote Procedure Call - RPC), należy zastosować odpowiedni atrybut

SoapRpcMethod przy zaznaczaniu metody jednokierunkowej . Aby utworzyć jednokierunkową metodę dla ujawnionego komponentu zdalnego, należy zastosować atrybut

One�y (z przestrzeni

nazw

System.Runtime.Remoting.Messaging)

do odpo­

wiedniej metody. Poniższy kod pokazuje ten sarn przykład z komponentem zdalnym:

using System ; using System . Runtime . Remoting . Messaging ; public class OneWayTes tRemot ing : Ma rshalByRefObj ect { public void DoLongTa s kWithWait ( ) {

li ( u ruchomienie długiego zadania i s powodowanie oczekiwania klient a ) . Delay ( 10 ) ; } [ OneWa y ( ) 1 public void DoLongTaskWit houtWait ( ) {

li ( u ruchomienie długiego zadania bez oczekiwania kl ienta ) . Del a y ( 10 ) ; } p rivate void Delay ( int second s ) { DateTime cu r rentTime = DateTime . Now;

378

C#

-

Księga przykładów wh ile ( DateTime . Now . Subt ract ( cu r rentTime ) . TotalSeconds < seconds ) { } }

}

Uwaga Metody jednokierunkowe nie są sposobem usuwania opóźnień po stronie klienta. Można również modyfikować klienta tak, aby wywoływał asynchronicznie dowolną metodę Web. W tym wypadku klient będzie oczekiwał na zakończenie usługi XML Web, ale w innym wątku, dzięki czemu aplikacja może realizować inne zadania. Asynchro­ niczne wywołania metod zostały omówione w przepisie 1 2 .6 .

13 Bezpieczeństwo w czasie wykonania Jednym z podscawowych zadań systemu Microsoft .NET Framework jest zapewnienie bezpie­ czeństwa przetwarzania - zwłaszcza jeśli chodzi o mobilny kod i systemy rozproszone. Większość nowoczesnych systemów operacyjnych (w tym Microsoft Windows) wspiera zabezpieczenia uza­ leżnione od użytkownika, umożliwiając zarządzanie działaniami i zasobami, do których użyt­ kownik ma dostęp. Jednakże we współczesnym świecie, w którym działa wiele połączonych sieci komputerowych - w szczególności Internet - oparcie bezpieczeństwa systemu wyłącznie na toż­ samości użytkownika to za mało. Ze względów bezpieczeństwa kod nie powinien automatycznie uzyskiwać tych samych uprawnień, które ma uruchamiająca go osoba. System .NET Framework udostępnia dwa komplementarne modele zabezpieczeń, rozwiązu­ jące większość zagadnień związanych z użytkownikiem, jak i samym kodem: •

Zabezpieczenia dostępu dla kodu (Code access security - CAS)



Zabezpieczenia oparte na rolach (Role-based security - RBS)

CAS i RBS nie zastępują (ani nie powielają) właściwości związanych z bezpieczeństwem, jakie zapewnia działający w niższej warstwie system operacyjny. Są one niezależne od platformy i niosą ze sobą dodatkowe możliwości zabezpieczeń, pozwalające dopasować i rozszerzyć ogólne bezpieczeństwo zarządzanego rozwiązania. CAS posługuje się informacją o źródle i pochodzeniu asemblacji (ewidencja - evidmce), uzy­ skaną podczas wykonywania programu, w celu określenia, które działania i które zasoby mogą być dla kodu asemblacji dostępne (uprawnienia - permissions). Zasady zabezpieczeń (security policy) systemu .NET Framework - hierarchiczny zestaw konfigurowalnych reguł - określają mapowanie między ewidencją i uprawnieniami. W bibliotece klas systemu .NET Framework do ochrony najważniejszych funkcjonalności przed nieuprawnionym dostępem stosowane są żądania (demands) uprawnień. Żądanie wymusza na module CLR (Common Language Run­ time) sprawdzenie, czy kod wywołujący chronioną metodę ma odpowiednie uprawnienie. CAS powoduje, że możliwości kodu w czasie wykonania są zależne od poziomu zaufania, jakim jest obdarzony twórca i źródło kodu, a nie jego użytkownik.

380

C#

-

Księga przykładów

Przepisy związane z CAS w niniejszym rozdziale omawiają następujące tematy: •

Dopuszczenie dostępu częściowo zaufanego kodu do asemblacji o silnych nazwach (przepis 1 3. 1 ).



Całkowite wykluczenie działania CAS (przepis 1 3.2) lub wykluczenie tylko sprawdzania uprawnień wykonawczych (przepis 1 3.3).



Żądanie określonych praw dostępu dla kodu i ustalanie, jakie uprawnienia zostały przyznane przez moduł runtime (przepisy 1 3.4, 1 3.5, 1 3.6 i 1 3.7).



Kontrola dziedziczenia i wyjątków przy użyciu CAS (przepis 1 3.8).



Przeglądanie i posługiwanie się ewidencją asemblacji (przepisy 1 3.9 i 1 3. 1 O) .



Manipulowanie zabezpieczeniami czasu wykonania przy użyciu domen aplikacji (przepisy 1 3. 1 1 i 1 3. 1 2).

Stosując się do bardziej tradycyjnego modelu bezpieczeństwa, RBS pozwala uzależnić decy­ zje o uprawnieniach w trakcie wykonywania programu od tożsamości i roli użytkownika uru­ chamiającego aplikację. W systemie operacyjnym Windows oznacza to podejmowanie decyzji w oparciu o nazwę użytkownika oraz grup, do których dany użytkownik należy. Jednakże RBS oferuje uniwersalny mechanizm zabezpieczeń, niezależny od wewnętrznego systemu operacyj­ nego, co umożliwia (przy pewnym przeprojektowaniu) integrację z dowol nym systemem kont użytkowników. Przepisy w tym rozdziale dotyczą następujących aspektów .NET RBS: •

Integracja RBS z kontami użytkowników Windows i sprawdzanie, czy użytkownik jest członkiem określonej grupy Windows (przepis 1 3. 1 3).



Kontrola dostępu do funkcjonalności aplikacji w oparciu o tożsamość bieżącego użytkow­ nika i role, które odgrywa (przepis 1 3. 1 4) .



Personifikacja użytkowników systemu Windows przez system operacyjny w celu wykonania zadań w ich imieniu (przepis 1 3. 1 5) .

Przepisy w tym rozdziale przedstawiają pewne ogólne działania, które s ą przydatne w apli­ kacjach, jednak jest to tylko niewielka część zagadnień bezpieczeństwa w .NET Framework. Bardziej szczegółowe omówienie zagadnień zabezpieczeń .NET Framework zawiera publikacja Programming .NET Security (O'Reilly and Associates, 2003). Jest to książka autorstwa Allena Jonesa i Adama Freemana, omawiająca wszelkie aspekty bezpieczeństwa systemu .NET Frame­ work i demonstrująca zwiększone możliwości zabezpieczeń podczas wykonywania.

1 3.1 Dopuszczenie częściowo zaufanego kodu do asemblacji o silnych nazwach Problem Chcesz napisać współużytkowaną asemblację, która będzie dostępna dla częściowo zaufa­ nego kodu (domyślnie, moduł runtime wyklucza dostęp częściowo zaufanego kodu do typów i zmiennych zawartych w asemblacjach o silnych nazwach).

Rozdział 1 3: Bezpieczeństwo modułu Runtime

381

Rozwiązanie Zastosuj

atrybut

poziomu

asemblacji

System.Security.All.owPartialo/TrustedCaUersAttribute

do asemblacji współużytkowanej.

Omówienie Aby zminimalizować ryzyko wywołane użyciem złośliwego kodu, moduł runtime nie umożli­ wia dostępu częściowo wiarygodnych asemblacji do asemblacji o silnych nazwach. Ograniczenie to skutecznie zmniejsza możliwość zaatakowania systemu przez podstępnie działający kod, ale uzasadnienie tak ostrego podejścia wymaga nieco więcej wyjaśnień. Asemblacje o silnych nazwach są z zasady zainstalowane w globalnym buforze asemblacji (glo­ bał assembly cache

-

GAC) i zawierają ważne funkcjonalności, które są wykorzystywane wspólnie

przez liczne aplikacje. Jest to szczególnie ważne dla tych asemblacji, które tworzą bibliotekę klas systemu .NET Framework. Również inne asemblacje o silnych nazwach, pochodzące z dobrze znanych i szeroko dystrybuowanych produktów, są zainstalowane w GAC i dostępne dla aplikacji zarządzanych. Duże prawdopodobieństwo, że w GAC znajdują się konkretne asemblacje, łatwy do nich dostęp oraz ich ważność sprawia, że są one naturalnym celem różnych prób ataku. Ogólnie

rzecz

biorąc, najbardziej prawdopodobnym źródłem złośliwego kodu są lokalizacje

zdalne - na przykład lokacja w Internecie, nad którą nie ma się żadnej kontroli. Przy domyślnych ustawieniach zasad zabezpieczeń cały kod uruchamiany z lokalnego komputera ma pełne zaufa­ nie, podczas gdy kod uruchamiany ze zdalnych lokalizacji powinien być obdarzony zaufaniem tylko częściowo. Uniemożliwienie dostępu częściowo wiarygodnego kodu do asemblacji o silnych nazwach oznacza, że nie będzie on również w stanie wykorzystać ich właściwości do podstępnych celów ani nie będzie mógł ingerować w ich działanie w celu wyszukiwania dających się wykorzy­ stać niedociągnięć. Ta teoria oczywiście opiera się na założeniu, że zasady zabezpieczeń są admini­

strowane prawidłowo. Jeżeli całemu kodowi przypisze się pełną wiarygodność, to nie tylko każda asemblacja będzie miała dostęp do asemblacji o silnej nazwie, ale każdy kod będzie miał również dostęp do całej funkcjonalności .NET Framework. A to już byłaby prawdziwa katastrom.

Uwaga Przy projektowaniu, wdrażaniu i testowaniu asembłacji współużytkowanej, jeżeli prawidłowo zostaną wykorzystane zabezpieczenia i ograniczony dostęp do ważnych pól i typów, nie ma potrzeby całkowitego wykluczania do niej dostępu częściowo wiarygod­ nego kodu. Z drugiej jednak strony udowodnienie, że w asembłacji nie ma luk, które działający podstępnie kod mógłby wykorzystać, jest bardzo trudne lub wręcz niemożliwe. Dlatego przed zastosowaniem Al/owPartial/yTrustedCal/ersAttribute należy starannie roz­ ważyć, czy dopuszczenie dostępu częściowo zaufanego kodu do uruchamianej asembła­ cji o silnej nazwie jest rzeczywiście potrzebne.

Moduł runtime wyklucza dostęp częściowo wiarygodnego kodu do asemblacji o silnych nazwach

LinkDemand dla zestawu uprawnień FulJTrust w każdym (public), jak i chronionym (protected}, każdego publicznie dostępnego typu,

poprzez włączenie domyślnego żądania polu, tak publicznym

zdefiniowanego w tej asemblacji. Oznacza to, że tylko asemblacje z uprawnieniem równoważnym zestawowi Fu/JTrust są w stanie realizować dostęp do typów i pól asemblacji o silnych nazwach.

382

C#

-

Księga przykładów

Zastosowanie acrybucu AtkJwPartiaily TrustedCailersAttribute do uruchamianej asemblacji o silnej nazwie powiadomi moduł runcime o braku konieczności żądania

LinkDemand dla

zawartych

w niej cypów i pól.

Uwaga Moduł runtime jest odpowiedzialny za prowadzenie dorozumianych działań zabezpieczających LinkDemand, koniecznych dla ochrony asemblacji o silnych nazwach; asembler języka C# nie generuje podczas kompilacji jawnych instrukcji LinkDemand.

Poniższy fragment kodu pokazuje zastosowanie atrybutu A/kJwPartiaily TrustedCaiiersAttribute. Należy zauważyć, że cen atrybut musi być poprzedwny prefiksem

a.ssembly,

który sygnalizuje

kompilatorowi, że celem atrybutu jest asemblacja (atrybut ten zwany jest również atrybutem globalnym global attribute). Ponadto nie trzeba włączać do nazwy jej części Attribute (chociaż -

jest to dopuszczalne) . Ze względu na to, że celem atrybutu jest asemblacja, należy go umieścić po instrukcjach using najwyższego poziomu, ale przed deklaracjami przestrzeni nazw i cypów. us ing System . Security; [ a s sembly : AllowPa rtiallyTrustedCallers ) public cla s s AllowPa rtial lyTrustedCallers Example { }

Wskazówka Powszechnie umieszcza się wszystkie globalne atrybuty w oddzielnym pliku niż reszta kodu aplikacji. Microsoft Visual Studio .NET stosuje tę zasadę, tworząc plik o nazwie Assembłylnfo.cs, przechowujący wszystkie atrybuty globalne.

Jeżeli

o zasto.w�w.i.•.• "�?.' .•

Rozdział 1 3: Bezpieczeństwo modułu Runtime

383

Rozwiązanie Z poziomu kodu wstaw właściwość SecurityEnabled klasy System.Security. SecurityManager na wartość false i utrwal zmianę dzięki wywołaniu SecurityManager. SavePo/icy. Alternatywnie możesz użyć narzędzia Code Access Security Policy (Caspol.exe), wykonując polecenie caspol -6

off z wiersza poleceń.

Omówienie CAS jest kluczowym elementem modelu bezpieczeństwa systemu .NET, który wyróżnia go na de innych platform przetwarzania danych. Jednak, pomimo że CAS był wdrażany z uwzględnie­ niem zagadnień wydajności i umiarkowanie wykorzystuje bibliotekę klas systemu .NET, nadal istnieje obciążenie związane z każdym żądaniem zabezpieczeń i koniecznością przetworzenia stosu przez moduł runtime. W rzadkich przypadkach bezpieczeństwo na poziomie kodu nie jest istotne albo potrzeba wydajności przewyższa potrzebę użycia CAS. W tych sytuacjach można kompletnie wyłączyć działania CAS i usunąć nadmiarowość zabezpieczeń na poziomie kodu. Wyłączenie CAS daje w efekcie uprawnienia całego kodu do wykonywania wszystkich działań, udostępnianych przez system .NET Framework (i jest równoważne z podaniem zestawu uprawnień FullTrust) . Obej­ muje to również możliwość załadowania innego kodu, wywoływania powszechnych bibliotek i użycia wskaźników do bezpośredniego dostępu do pamięci.

Ostrzeżenie Działanie CAS należy wyłączać tylko ze względu na wydajność, po wykorzy­ staniu wszystkich innych możliwości uzyskania wymaganego poziomu. Profilowanie kodu zwykle identyfikuje obszary, w których można znacznie poprawić wydajność bez potrzeby wyłączania CAS. Ponadto trzeba zapewnić dostateczną ochronę zasobów systemu przy użyciu mechanizmów zabezpieczeń systemu operacyjnego (takich jak ACL).

Caspol.exe jest narzędziem dołączonym do .NET Framework, umożliwiającym konfigurowanie wszystkich aspektów zasad zabezpieczeń dostępu do kodu w trybie wiersza poleceń. Po wpro­ wadzeniu polecenia

caspol

-6

off (lub

jego przeciwieństwa

caspol

-6

on)

narzędzie ustawia

właściwość SecurityEnabled klasy SecurityManager. Klasa SecurityManager zawiera zestaw metod statycznych, zapewniających dostęp do krytycznych funkcji zabezpieczeń i danych. Poniższy kod demonstruje użycie właściwości SecurityEnabledwyłączającej i włączającej działanie CAS.

li Wyłączanie sp rawdzania zabezpieczeń CAS . System . Secu rity . Secu rityManage r . Secu rityEnabled

=

false;

li U t rwalenie zmiany konfigu racj i . System . Secu rit y . Secu rityManage r . SavePolicy ( ) ; Aby włączyć działanie CAS, należy użyć następujących instrukcji.

li Włączanie sp rawdzania zabezpieczeń CAS . System . Secu rit y . Secu rityManage r . Secu rityEnabled

=

t rue ;

384

C#

-

Księga przykładów

li Ut rwalenie zmiany konfigu rac j i . System . Secu rity . Secu rityManage r . SavePolic y ( ) ; Aby wyłączyć działanie CAS, kod musi zawierać element

Security.Permissiom.SecurityPermission.

Contro/Policy

uprawnienia

System.

Specjalne uprawnienie, umożliwiające działanie CAS,

nie jest oczywiście wymagane. Zmiana

SecurityEnabled nie dotyczy egzekwowania CAS w działających procesach, jak rów­ SecurityEnabl.ed

nież nowych procesów, aż do wywołania metody SavePolicy, która zapisuje stan

w rejestrze Windows. Niestety .NET Framework nie gwarantuje, że te zmiany będą poprawnie wpływały na działanie CAS w aktywnym procesie, należy więc zmienić ustawienia, a następnie uruchomić nowy proces, aby zapewnić wiarygodne i spodziewane działanie.

Uwaga Bieżący stan CAS (on/off) jest zapamiętany w rejestrze Windows, w kluczu HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\Security\Policy, jako część ustawienia flag, zawartych w ustawieniach Global Settings. Jeżeli ten klucz nie istnieje, CAS jest domyślnie włączony .

1 3.3 Wyłączenie sprawdzania uprawnień do wykonania Problem W czasie działania aplikacji chcesz przestać sprawdzać, czy każda ładowana asemblacja ma uprawnienia do wykonania.

Rozwiązanie Wstaw wartośćfa/se dla właściwości

Check&ecutionRights klasy System.Security.SecurityManager SecurityManager.SavePolicy. Alternatywnie można użyć narzędzia Code Access Security Policy (Caspol.exe) i wykonać polecenie caspol -e off. i utrwal zmianę poprzez wywołanie

Omówienie Kiedy moduł runtime ładuje asemblację, sprawdza, czy zestaw przyznanych jej uprawnień zawiera element

&ecution

uprawnienia

SecurityPermission.

Moduł runtime wykorzystuje opóźnioną

metodę przetwarzania zasad, co oznacza, że zestaw uprawnjeń asemblacji nie jest analizowany

aż do momentu pierwszego żądania sprawdzenia zabezpieczeń wobec tej asemblacji. Procedura sprawdzania uprawnień obejmuje nie tylko konieczność sprawdzenia przez moduł runtime, czy każda ładowana asemblacja ma uprawnienie do wykonania, ale (nie wprost) wymusza przetwo­ rzenie zasad, co w efekcie neguje pozytywne rezultaty opóźnionego przetwarzania. Te czynnikj

Rozdział 1 3: Bezpieczeństwo modułu Runtime

385

mogą spowodować zauważalne opóźnienie ładowania asemblacj i, szczególnie wtedy, gdy łado­ wanych jest kilka asemblacji na raz, jak to się dzieje w momencie startu aplikacji. W wielu sytuacjach dopuszczenie ładowania i wykonania kodu nie niesie ze sobą znaczą­ cego ryzyka, o ile inne ważne działania i zasoby są właściwie zabezpieczone przy użyciu CAS i mechanizmów ochronnych systemu operacyjnego. Platforma .NET umożliwia wyłączenie automarycznego sprawdzania uprawnień na wykonanie z poziomu kodu lub poprzez użycie Caspol.exe. Polecenie caspol -e off lub jego przeciwieństwa caspo l -e on ustawia właściwość CheckE­ xecutionRights klasy SecurityManager. Jest to pokazane w poniższych fragmentach kodu, który można zastosować na własny użytek:

li Wyłączenie s p rawdzania p raw wykonania . System . Secu rity . Secu rityManage r . Chec kExecutionRig hts

false ;

li Ut rwalenie zmiany konfigu racj i . System . Secu rity . Secu rityManage r . SavePolic y ( ) ; Aby umożliwić wykonanie sprawdzenia uprawnień, należy użyć następujących instrukcji:

li Włączenie sp rawd zania p raw wykonania . System . Secu rity . Secu rityManage r . CheckExecut ionRig h t s = t rue ;

li Ut rwalenie zmiany konf igu racj i . System . Secu rity . Secu rityManage r . SavePolicy ( ) ;

CheckExecutionRights, kod musi zawierać element Contro/Policy SecurityPermission. Ta modyfikacja spowoduje narychmiastową zmianę bieżącego

Aby zmodyfikować wartość uprawnienia

procesu, umożliwiając załadowanie asemblacji podczas wykonywania programu bez spraw­ dzania uprawnień do wykonania. Zmiana nie będzie dotyczyła innych działających procesów. Konieczne jest wywołanie metody

SavePolicy w

celu zapisanie zmian w rejestrze Windows, by

ustawienie oddziaływało na nowe procesy.

1 3.4 Zagwarantowanie, że moduł runtime przyzna asemblacji specjalne uprawnienia Problem Chcesz, by moduł runtime przyznał asemblacji uprawnienia dostępu, które są nieodzowne do udanego wykonania aplikacji.

Rozwiązanie Użyj w swojej asemblacji żądania uprawnień, które określi prawa dostępu dla kodu tej asem­ blacj i . Zadeklaruj żądanie uprawnień przy użyciu atrybutów uprawnienia dostępu na poziomie asemblacji.

386

C#

-

Księga przykładów

Omówienie Nazwa permission request jest nieco myląca, ponieważ moduł runtime nigdy nie nada asem­ blacji uprawnień, za wyjątkiem tych, które są wymuszane przez zasady zabezpieczeń. Jednak pomijając nazwę, żądania uprawnień służą ważnemu celowi. Wprawdzie sposób, w jaki moduł runtime nimi zarządza, może się z początku wydawać dziwny, jednak natura mechanizmu CAS nie dopuszcza żadnej prostszej alternatywy. Żądania uprawnień identyfikują te prawa, które kod musi mieć. Rozważmy program odtwa­ rzający filmy z sieciowego serwera, który klienci mogą pobrać i uruchomić na swoich kom­ puterach: uniemożliwienie mu otwarcia połączenia z serwerem z powodu zasad zabezpieczeń użytkownika byłoby katastrofą! Program ten by się ładowywał i uruchamiał, ale gdy tylko użytkownik spróbuje się połączyć z serwerem, by wyświetlać film, aplikacja wstanie przerwana z wywołaniem wyjątku System.Security.SecurityException. Rozwiązaniem tego problemu jest włączenie do asemblacji żądania uprawnień, określającego uprawnienie wymagane do otwar­ cia połączenia sieciowego z serwerem {uprawnienia System.Net. WebPermission lub System.Net. SocketPermission, zależnie od typu połączenia, które należy otworzyć) . Moduł runtime honoruje żądania uprawnień zgodnie z regułą, że lepiej wcale nie ładować kodu, niż żeby miało co zaowocować zawieszeniem przy próbie wykonania zabronionej operacji. Jeżeli zatem po sprawdzeniu zasad zabezpieczeń moduł runtime stwierdzi, że zestaw uprawnień uruchamianej asemblacji nie jest wystarczający dla żądanych przez nią uprawnień, nie załaduje asemblacji i zamiast tego wywoła wyjątek System.Security.Policy.PolicyException. Aby zadeklarować żądanie uprawnień, należy użyć odpowiednika atrybutu dla żądanego uprawnienia na dostęp do kodu, który będzie użyty. Wszystkie uprawnienia dostępu kodu mają odpowiedniki atrybucowe, używane do konstrukcj i deklaratywnych instrukcji zabezpie­ czeń, w cym żądań uprawnień. Na przykład odpowiednikiem uprawnienia SocketPermission jest SocketPermissionAttribute, a WebPermission - WebPermissionAttribute. Wszystkie uprawnie­ nia i odpowiadające im atrybuty są konstruowane zgodnie z tą samą konwencją nazewnictwa i należą do tej samej przestrzeni nazw. Poniższy kod przedstawia aplikację konsolową o nazwie PermissionRequestExample, która zawiera dwa żądania uprawnień: jedno dla SocketPermission, a drugie dla SecurityPermission. Ważne tu są następujące reguły: • •

Należy zadeklarować żądanie uprawnie11 po instrukcjach przed przestrzenią nazw lub deklaracjami typów.

using najwyższego poziomu, ale

Atrybut musi wskazywać na asemblację, a jego nazwa musi być poprzedzona prefiksem

assembly:. •

Nie ma potrzeby włączania do nazwy atrybutu jej części Attribute, chociaż jest to możl iwe.



Należy wyspecyfikować SecurityAction.RequestMinimum jako pierwszy argument pozycyjny atrybutu - ta wartość identyfikuje instrukcję jako żądanie uprawnień.



Należy skonfigurować atrybut tak, aby reprezentował żądane uprawnień na dostęp kodu przy użyciu właściwości atrybutu. Więcej szczegółów dotyczących właściwości implemen­ towanych przez każdy atrybut zabezpieczeń dostępu kodu zawiera dokumentacja systemu .NET Framework SDK.



Instrukcji żądania uprawnień nie należy kończyć średnikiem (;) .



Aby dokonać więcej niż jednego żądania, należy użyć kilku i nstrukcji żądania uprawnień, jak w następującym przykładzie:

Rozdział 1 3: Bezpieczeństwo modułu Runtime

387

using System . Net ; using System . Secu rit y . Permis s ion s ;

l i Żądanie u p rawnień dotyczące SocketPe rmis s ion , któ re zezwala kodowi na otwa rcie połączenia TCP do wskazanego hosta i port u . [ assetnbly : SocketPer•ission ( SecurityAction . RequestMinimum, Access = " Connect " , Host = "www . fab rikam . co•" , Port = "3538" , Transport = "Tcp " ) J

li Ż ądanie up rawnień dotyczące elementu UnmanagedCode w Secu rityPe rmis sion , s t e ru j ą cego możliwoś cią wykonania nieza rządzanego kodu .

[assemb ly: Securi tyPermission (SecurityAction . RequestMinimum, UnmanagedCode true ) J =

public class Pe rmis s ionRequestExample { public static void Main ( ) {

li j akiś p rog ram . . . } } Próba wykonania aplikacji

PermissionRequestExample,

gdy zasady zabezpieczeń nie przyznają

Policy­ Exception. Użycie domyślnych zasad zabezpieczeń może spowodować ten sam efekt, jeżeli asem­ asemblacji żądanych uprawnień, spowoduje przerwanie aplikacji i wywołanie wyjąrku

blacja jest uruchamiana ze strefy intranetowej, ponieważ asemblacje ładowane z tej strefy nie mają przyznanego uprawnienia SocketPermission.

Unhandled Exception : System . Secu rity . Policy . PolicyException : Requ i red pe rmis s ion cannot be acqui red . Przy próbie załadowania asemblacji z kodu (ręcznie lub automatycznie), jeżeli załadowana asem­ blacja zawiera żądania uprawnień sprzeczne z zasadami zabezpieczeń, metoda ładująca asembla­ cję wywoła wyjątek PolicyException, który powinien wstać właściwie obsłużony.

1 3.5 Ogranicznie uprawnień przyznawanych asemblacji Problem Chcesz ograniczyć uprawnienia dostępu dla kodu przyznawane asemblacji, a jednocześnie zagwarantować, że inne osoby lub programy nie wykorzystają jej jako mechanizmu do realizacji niepożądanych lub złośliwych działań.

Rozwiązanie Użyj w swojej asemblacji deklaratywnych instrukcj i zabezpieczeń w celu określenia opcjonal­ nych żądań uprawnień oraz żądań odrzucenia uprawnień. Żądania uprawnień opcjonalnych

388

C#

-

Księga przykładów

definiują maksymalny zestaw uprawnień, które moduł runtime może przyznać asemblacji. Żądania odrzucenia uprawnień określają szczególne uprawnienia, których moduł runtime nie może przyznać asemblacji.

Omówienie Ze względów bezpieczeństwa idealna jest sytuacja, w której kod ma tylko takie uprawnienia, jakie są niezbędne dla jego działania. Minimalizuje to możliwość wykorzystania kodu tej apli­ kacji do szkodliwych lub niepożądanych działań przez inne osoby lub inne programy. Problem stanowi fakt, że moduł runtime nadzoruje uprawnienia dostępu asemblacji, stosując zasady zabezpieczeń, które konfiguruje użytkownik albo administrator. Zasady te mogą być inne w każdej lokalizacji, w której dana aplikacja wstanie uruchomiona, a jej autor nie ma kontroli nad cym, jakie uprawnienia wynikające z zasad zabezpieczeń zostaną przyznane aplikacji. Wprawdzie nie można samemu kontrolować zasad bezpieczeństwa we wszystkich lokaliza­ cjach, w których wstanie kiedyś uruchomiony dany kod, jednak system .NET Framework udo­ stępnia dwa mechanizmy, poprzez które można ogran iczyć uprawnienia przyznawane asemblacji: żądania odrzucenia i żądania opcjonalne. Pierwsze z nich umożliwiają identyfikację uprawnień, które mają być przez moduł runtime odmówione. Jeśli po określeniu zasad wynikowych osta­ teczny zestaw uprawnień asemblacji zawiera jakiekolwiek uprawnienia wymienione w żądaniu odmowy, moduł runcime je usunie. Żądanie uprawnień opcjonalnych definiuje natomiast mak­ symalny zestaw uprawnień, które moduł runtime przyzna asemblacji. Jeżeli wynikowy zestaw uprawnień dla asemblacji zawiera inne niż wyspecyfikowane w żądani u opcjonalnym, moduł runtime usunie je także. W przeciwieństwie do przypadku żądania minimalnego uprawnienia (omówionego w przepisie 1 3.4), moduł runtime nie uchyli się od załadowania asemblacji, jeśli nie będzie mógł przyznać wszystkich uprawnień wymienionych w żądaniu opcjonalnym. Należy traktować żądania odrzucenia i żądania opcjonalne j ako alternatywę osiągnięcia tych samych rezultatów; wybrane rozwiązanie zależy od tego, jak wiele uprawnień ma być odrzuco­ nych. Jeśli ma ich być niewiele, żądanie odrzucenia jest prostsze. Jeżeli jednak chodzi o wielką ilość uprawnień, łatwiej jest zakodować żądanie opcjonalne dla kilku uprawnień, co automa­ tycznie odrzuci całą resztę . Żądania opcjonalne i żądania odrzucenia umieszcza się w kodzie przy użyciu deklaratyw­ nych instrukcji bezpieczeństwa o tej samej składni, co dla żądania minimalnego uprawnienia, omówionego w przepisie 1 3.4. Jedyną różnicę stanowi tu wartość System.Security.Permissiom. SecurityAction, przekazywana do konstruktora atrybutów uprawnienia. Należy użyć opcji Secu­ rityAction.RequestOptional aby zadeklarować żądanie opcjonalnego uprawnienia oraz Security­ Action.RequestRefo.se, aby zadeklarować żądanie odrzucenia. Podobnie jak dla żądań uprawnień minimalnych, należy żądania obu rodzajów deklarować jako atrybuty globalne, poprzez zapo­

assembly:. Ponadto wszystkie żądania using najwyższego poziomu, ale przed jakąkolwiek deklaracją

czątkowanie nazwy atrybutu uprawnienia prefiksem muszą się pojawić po instrukcjach przestrzeni nazw lub typu.

Przykład kodu OptionalRequestExample demonstruje żądanie opcjonalnego uprawnie­ nia dla zestawu uprawnień internetowych. Zestaw ten jest nazwanym zestawem uprawnień, definiowanym przez domyślne zasady zabezpieczeń. Gdy moduł rw1time ładuje asemblację

OptionalRequestExamp�,

nie przyzna asemblacji żadnego uprawnienia, które nie jest zawarte

w zestawie uprawnień internetowych (więcej szczegółów na temat uprawnień należących do zestawu uprawnień internetowych zawiera dokumentacja .NET Framework SOK).

Rozdział 1 3: Bezpieczeństwo modułu Runtime

389

using System . Secu rity . Pe rmi s sions ; [ assembly : Pe nnissionSet ( Secu rityAction . RequestOpt ional , Name = " In t e rnet " ) I public class OptionalRequestExample { public s tatic void Main ( ) {

li j akiś p rog ram . . . } } W przeciwieństwie do OptionalRequestExample, przykładowy kod RefuseRequestExarnple

wykorzystuje żądanie odrzucenia w celu wyeli minowania pojedynczego uprawnienia System. Security.Permissions.FiklOPermission, reprezentującego dostęp do zapisu na dysku C:. using System . Securit y . Pe rmi s s ion s ; [ as s embly : FileIOPe rmi s s ion ( Secu rityAction . RequestRe fuse , Write

=

@"C : \ " ) ]

public class RefuseRequest Example { public s tatic void Main ( ) {

li j akiś p rog ram . . . } }

1 3.6 Przeglądanie żądań uprawnień wykonywanych przez asemblację Problem Chcesz przejrzeć żądania deklararywnego uprawnienia lub odmowy zawarte w asemblacji, w celu prawidłowego skonfigurowania zasad zabezpieczeń lub zrozumienia ograniczeń biblio­ teki, której funkcje zamierzasz wywołać w swoim kodzie.

Rozwiązanie Użyj narzędzia przeglądu uprawnień (Permissions View - Permview.exe), dostarczanego z .NET Frarnework SOK.

Omówienie Chcąc prawidłowo skonfigurować zasady zabezpieczeń, należy znać wymagania uprawnień dostępu kodu, który ma być uruchamiany. Dotyczy to zarówno wykonywalnych asemblacji, jak i bibliotek, które będą wykorzysrywane w aplikacji. W przypadku bibliotek należy zdawać sobie sprawę z tego, które uprawnienia asemblacja odrzuca, by nie próbować użyć biblioteki do wyko­ nywania zabronionych działań (co zresztą wywoła wyjątek System.Security.SecurityException). Narzędzie Permview.exe udostępnia proste przeglądanie zadeklarowanych żądań upraw­ nień zawartych w asemblacji . Obejmuje to żądania uprawnień minimalnych, opcjonalnalnych, a rakże żądania odrzucenia. Przykładowy kod pokazuje klasę deklarującą te trzy rodzaje żądań:

390

C#

-

Księga przykładów

u sing System . Net ; u sing System . Secu rity . Permiss ion s ;

li Żądanie u p rawnień minimalnych SocketPermi s s ion . [ a ssembly : SocketPe rmis sion ( Secu rityAction . RequestMinimum , Un rest ricted = t rue ) J

li Żądanie up rawnień opcj onalnych Secu rit yPermi s s ion . [ a s sembly : Secu rityPermis s ion ( Secu rityAct ion . Reques tOptional , Un rest ricted = t rue ) )

li Żądanie odrzucenia FileIOPe rmi s sion . [ a s sembly : Secu rityPermi s s ion ( Secu rityAction . RequestRefu s e , U n rest ricted = t rue ) ] public class Permiss ionViewExample { public static void Main ( ) { 11 j akiś p rog ram . . .

} } Wykonanie polecenia pennview PennissionViewExample.exe wygeneruje pokazane niżej dane wyjściowe. Nie są one specjalnie przyjazne dla użytkownika, jednak można je odczytać i określić żądania uprawnień generowane przez asemblację. Wszystkie trzy rodzaje żądań uprawnień minimalnych, opcjonalnych i odrzucenia - mają oddzidne nagłówki i są zapisane w strukturze

XML obiektu System.Security.PermissionSet. Mic rosoft ( R ) . NET F ramewo rk Pe rmi s s ion Request Viewe r . Ve rsion 1 . 1 . 4322 . 5 10 Copyright ( C ) Mic rosoft C o rpo ration 1998 - 2002 . All rig h t s rese rved . minimal pe rmis sion set : optional permis sion set : re fu sed pe rmis sion set :

Rozdział 1 3: Bezpieczeństwo modułu Runtime 391

Używając przełącznika Idec/ podczas wykonania Permview.exe można przeglądać wszystkie zadeklarowane instrukcje zabezpieczeń, zawarte w asem­ blacji, w tym jawne żądania i wywołania. Pozwala to na wgląd w działanie asemblacji i ułatwia prawidłowe skonfigurowanie zasad zabezpieczeń. Należy jednak pamiętać, że program Permview.exe nie pokazuje imperatywnych instrukcji zabezpieczeń, zawar­ tych w asemblacji i że - bez dostępu do kodu źródłowego - nie jest to w ogóle możliwe. Wskazówka

1 3. 7 Określenie posiadania uprawnień podczas wykonywania programu Problem Podczas wykonywania programu chcesz określić, c;z;y asemblacja ma konkretne uprawnienie.

Rozwiązanie Należy utworzyć i skonfigurować instancję uprawnienia, które chcesz sprawdzić, a następnie przesłać jako argument do metody statycznej lsGranted klasy System.Security.SecurityManager.

Omówienie Przy użyciu żądań minimalnego uprawnienia można zapewnić przyznanie asemblacji przez moduł runcime specjalnego ze.stawu uprawnień; j eżeli kod został uruchomiony, można z powo­ dzeniem przyjąć, że ma żądane minimum uprawnień. Jednakże można spróbować zaimplemen­ tować opcjonalną funkcjonalność, którą aplikacja będzie udostępniać tylko wtedy, gdy moduł runtime przyzna jej odpowiednie uprawnienia. To podejście jest częściowo sformalizowane dzięki użyciu żądań opcjonalnych uprawnień, co pozwala na zdefiniowane zestawu uprawnień, które kod mógłby wykorzystać, gdyby zostały przyznane przez zasady zabezpieczeń, chociaż dla pomyślnego działania kodu nie są istotne (przepis 1 3 . 5 dostarcza więcej szczegółów na temat użycia żądań opcjonalnego uprawnienia). Problem z żądaniami opcjonalnego uprawnienia jest następujący: moduł runtime nie ma możliwości powiadomienia asemblacji, które z żądanych uprawnień opcjonalnych zostały przy­ znane. Aplikacja może zatem próbować wykonać operację chronioną, co zakończy się porażką, jeżeli wywołanie zakończy się wyjątkiem System.Security.SecurityException. Lepszym rozwiąza­ niem jest zatem sprawdzenie zawczasu, c;z;y niezbędne uprawnienia są przyznane. Odpowiednią logikę można wbudować do kodu, aby uniknąć wywoływania chronionych pól, co prowadzi do zbędnych przebiegów przez stos i generowania wyjątków zabezpieczeń. Poniższy fragment kodu pokazuje użycie metody IsGranted dla określenia, c;z;y bieżąca asemblacja uzyskała uprawnienie zapisu w katalogu C:\Data. To wywołanie można wykonywać za każdym razem, gdy trzeba sprawdzić występowanie uprawnienia, ale wygodniejsze jest zapamiętanie zwróconej wartości boolowskiej we fladze konfiguracji, wskazującej, czy aplikacja ma prawo do zapisu plików.

392

C#

-

Księga przykładów

li Def iniowanie zmiennej sygnal izu j ącej , czy asemblac j a ma p rawo do zapisu li w folde rze C : \Data . bool canWrite

=

false ;

li Two rzenie i konfigu rowanie obiektu FileIOPermis sion , rep rezent u j ącego p rawo li do zapisu w folde rze C : \Data . System . Se c u rity . Pe rmis s ions . FileIOPermiss ion fil eIOPerm

=

new System . Secu rity . Pe rmis s ions . FileIOPermis s ion ( System . Security . Pe rmi s s ion s . FileIOPe rmi s s ionAccess . W rite , @ " C : \Data" ) ;

li Sp rawdzenie , czy bieżąca asemblac j a ma wskazane u p rawn ienie . canWrite = System . Secu rity . Secu rityManage r . I sG ranted ( fileIOPe rm ) ;

1 3.8 Ograniczenie prawa do rozszerzenia klasy i nadpisywania jej pól Problem Chcesz kontrolować, kto może rozszerzać twoje klasy poprzez dziedziczenie, a rakże to, które pola wyprowadwna klasa może nadpisywać.

Rozwiązanie Użyj deklararywnej instrukcji zabezpieczeń, aby zastosować pole SecurityAction.!nheritance­

Demand do deklaracji klas i pól, króre chcesz chronić.

Omówienie Takie modyfikarory języka, jak seakd, public, private i virtual, oferują pewien poziom kontroli nad dziedziczeniem po danej klasie i nadpisywaniem jej pól. Jednakże te modyfikatory są nie­ elastyczne i nie dają możliwości szczegółowego określenia, kto może rozszerzać klasę albo nadpi­ sywać jej pola. Na przykład można sobie zażyczyć, żeby tylko kod utworzony w danej insrycucji lub departamencie mógł rozszerzać klasy krytyczne dla biznesu, albo żeby tylko kod załadowany z lokalnego komputera rozszerzał pewne metody. Dzięki zastosowaniu lnheritanceDemand do deklaracji klasy lub pola, można określić uprawnienia czasu wykonania, które musi mieć klasa, by móc rozszerzyć daną klasę albo nadpisać wybrane pola. Należy pamiętać, że uprawnie­ nia dla klasy są uprawnieniami dla asemblacji, w której dana klasa jest zadeklarowana. Wprawdzie w !nheritanceDemand można żądać dowolnego uprawnienia lub grupy upraw­ nień, zazwyczaj stosuje się żądanie uprawnień tożsamości (identity permissions). Uprawnienia tożsamości reprezentują ewidencję, przekazywaną modułowi runtime przez asemblację. Jeżeli asemblacja zaprezentuje właściwe typy ewidencji podczas ładowania, moduł runtime automa­ tycznie przypisze asemblacji odpowiednie uprawnienia tożsamości. Umożliwiają one użycie regu­ larnych instrukcji rozkazujących i deklaratywnych instrukcji zabezpieczeń oraz oparcie decyzji

Rozdział

1 3:

Bezpieczeństwo modułu Runtime 393

dotyczących zabezpieczeń bezpośrednio na identyfikacji kodu, bez konieczności bezpośredniej analizy obiektów ewidencji. Tabda 1 3- 1 przedstawia typy uprawnień tożsamości, generowanych dla każdego typu ewidencji (typy ewidencji są polami przestrzeni nazw System.Security.Policy, a typy uprawnienia tożsamości są polami przestrzeni nazw System.Security.Permissions.) Tabela 1 3-1

Klasy Evidence, generujące uprawnienia tożsamości

Klasy Evidence

Zezwolenie tożsamości

ApplicationDirectory

Żadne

Hash Publisher Site

Żadne PublisherldentityPermission SiteldentityPermission

StrongName Uri

StrongNameldentityPermission UrlldentityPermission

Zone

ZoneldentityPermission

Uwaga Moduł runtime przypisuje asemblacji uprawnienia tożsamości na podstawie ewidencji prezentowanej przez tę asemblację. Nie można przypisać asemblacji dodatko­ wych uprawnień tożsamości poprzez skonfigurowanie zasad zabezpieczeń.

Aby zaimplementować lnheritanceDemand, należy używać składni deklaratywnych zabezpie­ czeń. W związku z tym należy użyć atrybutu odpowiednika dla klasy uprawnienia, na które wstało zgłoszone żądanie. Wszystkie klasy uprawnień mają atrybut odpowiednika, którego należy użyć do konstrukcji deklaratywnych instrukcji zabezpieczeń, w tym InheritanceDemand. Na przykład odpowiednikiem dla uprawnienia PublisherldentityPermission jest Publisher­ IdentityPermissionAttribute, a dla StrongNameldentityPermission - StrongNameldentityPermission­ Attribute. Wszystkie uprawnienia i odpowiednie atrybuty wykorzystują tę samą konwencję i są polami tej samej przestrzeni nazw. Aby określić, jaki kod może rozszerzyć daną klasę, należy zastosować InheritanceDemand do deklaracji tej klasy. Poniższy fragment kodu pokazuje klasę chronioną przez Inheritance­ Demand. Tylko klasy w asemblacjach podpisanych certyfikatem wydawcy, zawartym w pliku pubcert.cer, mogą być wyprowadzone z klasy lnheritanceDemandF.xampk. Zawartość pliku certyfikatu jest odczytywana podczas kompilacji, a niezbędna informacja o certyfikacie wstaje wbudowana w asemblację. [ Publishe ridentityPe rmis sion ( Secu rityAct ion . Inhe ritanceDemand , CertFile

=

@ " C : \CSha rpCookbook\ 13 - Runtime Secu rity\pubce rt . ce r " ) l

public class Inhe ritanceDema ndExample { }

Aby określeć, jaki kod może nadpisywać określone poła, należy użyć lnheritanceDemand w deklaracji pola. Zastosowanie lnheritanceDemandw poniższym przykładzie dopuszcza nadpi­ sanie metody SomeProtected.Method tylko przez klasy posiadające zestaw uprawnień Ful/Trust.

394

C# - Księga przykładów [ Pe rmissionSet ( Se c u rityAction . Inhe ritanceDemand , Name= " FullTru s t " ) ] public void SomeP rotectedMethod ( ) { }

1 3.9 Przeglądanie ewidencji asemblacji Problem Chcesz sprawdzać ewidencję, którą moduł runtime przypisał do asembłacji.

Rozwiązanie Posłuż się obiektem

System.&faction.Assembly

reprezentującym asemblację, o którą chodzi.

Z właściwości Evidence obiektu Assembly odczytaj zbiór System.Security.Policy.Evidence i uzy­ skaj dostęp do obiektów ewidencji przy użyciu metod GetEnumerator, GetHostEnumerator lub

GetAssemblyEnumerator klasy Evidence.

Omówienie Klasa klasy

Evidence reprezentuje zbiór obiektów ewidencji . Właściwość Evidence (tylko do odczytu) Assembly zwraca obiekt zbioru Evidence, zawierający wszystkie obiekty ewidencji, które

moduł runtime przypisał do asemblacji podczas jej ładowania. Klasa Evidence zawiera dwa zbiory, reprezencujące różne typy ewidencji: ewidencję hosta i ewidencję asemblacji. Ewidencja hosta zawiera te obiekty ewidencji, które zostały przypisane do asemblacji przez moduł runtime lub zaufany kod ładujący asemblację.

Ewidencja asemblacji

reprezentuje obiekty ewidencji użytkownika, wbudowane w asemblację podczas jej tworzenia. Klasa Evidence implementuje następujące metody wyliczania obiektów ewidencji: • • •

GetEnumerator GetHostEnumerator GetAssemblyEnumerator

GetEnumerator zwraca System. Colkctions.!Enumerator, który numeruje wszystkie obiekty GetHostEnumerator i GetAssemblyEnumerator zwra­ cają instancję !Enumerator, która numeruje tylko obiekty ewidencji należące do danego zbioru. Zademonstrowany tutaj przykład ViewEvidenceExamp/,e pokazuje, jak wyświeclić ewidencję

Metoda

ewidencji, zawarte w zbiorze Evidence. Metody

hosta i ewidencję asemblacji na konsoli. Przykład opracowano w oparciu o fakt, że wszystkie klasy standardowej ewidencji nadpisują metodę Object. ToString dla wyświetlenia użytecznej reprezentacji stanu obiektu ewidencji. Jakkolwiek dane ce są i nteresujące, dany przykład nie zawsze pokazuje cę ewidencję, którą asemblacja posiadałaby przy załadowaniu bezpośrednio z programu. Host modułu rumime (taki jak M icrosoft ASP.NET lub M icrosoft Internet &plo­ rer) może swobodnie przypisać dodatkową ewidencję hosta przy załadowaniu. u s ing System ; using System . Reflection ;

Rozdział

1 3:

Bezpieczeństwo modułu Runtime 395

using System . Collection s ; using System . Secu rit y . Policy ; public class ViewEvidenceExample { public static void Main ( st ring [ ] a rg s ) {

li Ładowanie wskazanej asemblac j i . Assembly a = As semb l y . LoadF rom ( a rg s [ 0 ] ) ;

li Pobranie z bio ru Evidence z załadowanej a semblac j i . Evidence e = a . Evidence;

li Wyświetlenie ewidencj i hosta . I Enumerato r x = e . GetHostEnume rato r ( ) ; Console . W riteline ( " HOST EVIDENCE COLLECTION : " ) ; whil e ( x . MoveNext ( ) ) { Console . W riteline ( x . C u r rent . ToSt ring ( ) ) ; }

li Wyświetlenie ewidencj i asemblacj i . x = e . GetAs semblyEnume rat o r ( ) ; Console . W riteline ( "ASSEMBLY EVIDENCE COLLECTION : " ) ; while ( x . MoveNext ( ) ) { Console . W riteline ( x . Cu r rent . ToSt ring ( ) ) ; } } }

Wszystkie standardowe klasy ewidencji dostarczone przez system .NET Framework są nie­ zmiennicze, co oznacza, że nie można zmienić ich wartości od momentu, gdy moduł runtime je utworzy i przypisze do asemblacji. Ponadto nie można dodać ani usunąć pozycji podczas numerowania zawartości kolekcji przy użyciu !Enumerator, w przeciwnym wypadku metoda MoveNext wywoła wyjątek System.lnvalidOperationException.

1 3.1 O Posługiwanie się ewidencją podczas ładowania asemblacji Problem Chcesz się posłużyć ewidencją podczas ładowania asemblacji, aby mieć wpływ na uprawnienia przyznawane asemblacji przez moduł runtime.

Rozwiązanie Utwórz obiekty ewidencji, które mają być przypisane do asemblacji, dodaj je do instancji klasy System.Security.Policy.Evidence, a następnie zbiór Evidence przekaż jako argument do metody użytej do załadowania asembłacji.

396

C#

-

Księga przykładów

Omówienie Ewidencja, którą posiada asemblacja, definiuje touamość asemblacji i określa uprawnienia przy­ znawane asemblacji przez moduł runtime. Za określenie, którą ewidencję przypisać do asembla­ cji, jest w głównej mierze odpowiedzialny program ładujący asernblację, ale zaufany host (taki jak ASP.NET lub I nternet Explorer) może również przypisać ewidencję do asemblacji. Kod projek­ tanta może przypisać ewidencję podczas załadowania asemblacji, jeżeli zawiera element Control­ Evidence uprawnienia SecurityPermission.

Ostrzeżenie Jeśli podejmie się próbę ponownego załadowania asemblacji do poje­ dynczej domeny aplikacji z przypisaniem innej ewidencji, moduł runtime wywoła wyjątek

System. IO. FileLoadException.

Wiele metod umożliwia przypisywanie ewidencji podczas ładowania asernblacji; niektóre z nich ładują asemblację bezpośrednio, podczas gdy inne jako typy o utworzonych instancjach - nie bezpośrednio, co powoduje, że asemblacja zawiera żądany typ. Wspólną cechą tych wszyst­ kich metod jest wykorzystywanie przez nie jako argument zbioru Evidence, a klasa Evidence jest zasobnikiem obiektów ewidencji. Należy więc umieścić poszczególne obiekty ewidencji, które mają być przypisane do asemblacji, w zbiorze Evidence i przekazać je do metody ładują­ cej asemblację. Jeżeli nowa ewidencja zawiera wpisy konfliktowe z ewidencją przypisaną przez program ładujący asemblację, nowe wartości zastąpią poprzednie. Tabela 1 3-2 przedstawia klasy i ich metody, które ładują asemblację bezpośrednio lub pośrednio. Każda z metod zapewnia jedną lub więcej przeciążeń, akceptujących zbiór Evidence.

Tabela 1 3-2

Klasy i ich metody, umożliwiające przypisanie ewidencji do asemblacji

Klasa/Metoda

Opis

System.Activator

Te metody dotyczą domeny bieżącej aplikacji.

Createlnstance CreatelnstanceFrom

Tworzy i nstancję typu domeny bieżącej aplikacji w okre­ ślonej asemblacji.

System.AppDomain

Te metody dotyczą domeny aplikacj i reprezentowanej przez obiekt AppDomain, dla którego metoda została wywołana.

Tworzy instancję typu z określonej asemblacji. Createlnstance CreatelnstanceAndUnwrap CreatelnstanceFrom CreatelnstanceFromAndUnwrap

DefineDynamicAssembly

Tworzy obiekt System. Reflection.Emit.AssemblyBuilder, który może być użyty do dynamicznego utworzenia asem­ blacji w pamięci.

ExecuteAssembly

Ładuje i wykonuje asemblację, która ma zdefiniowany punkt wejścia {metoda Main).

Rozdział 1 3: Bezpieczeństwo modułu Runtime 397 Tabela 1 3-2

Klasy i ich metody, umożliwiające przypisanie ewidencji do asemblacji

Klasa/Metoda

Opis

Load

Ładuje wyspecyfikowaną asemblację.

System. Reftection.Assembly

Te metody dotyczą domeny bieżącej aplikacji.

Load LoadFile LoadFrom LoadWithPartialName

Ładuje wyspecyfikowaną asemblację.

Poniższy fragment kodu demonstruje użycie metody Assembiy.Load, ładującej asemblację do bie­ żącej domeny aplikacji. Przed wywołaniem Load, kod utworzy zbiór Evidence i użyje metody AddHost, aby dołączyć obiekty Site i Zone (pola z przestrzeni nazw System.Security.Policy).

li

Two rzenie nowych obiektów ewidencj i Site o raz Zon e .

System . Secu rity . Policy . Site s iteEvidence = new System . Secu rity . Pol icy . Site ( "www . mi c rosoft . com" ) ; System . Secu rity . Policy . Zone zoneEvidence =

new System . Secu rity . Policy . Zone ( System . Secu rit y . Secu rityZone . T rusted ) ;

li

Two rzenie nowego zbioru Evidence .

System . Secu rity . Policy . Evidence evidence

=

new System . Secu rity . Policy . Evidence ( ) ;

li

Dodanie obiektów Site i Zone do z bioru Evidence collection p rzy użyciu

metody AddHost . evidence . AddHos t ( s iteEviden ce ) ; evidence . AddHost ( zoneEviden ce ) ;

li

Ładowanie asemblacj i " SomeAs sembl y " i p rz ypisanie j ej nowych obiektów

ewidencj i Site i Zon e . Powod uj e to zastąpienie obiektów Site i Zone , p rzypisanych p rzez p rog ram ł aduj ący . System . Reflection . As sembly a ssembly = System . Reflection . Assembly . Load ( " SomeAssembly " , evidence ) ;

1 3.1 1 Modyfikowanie zabezpieczeń czasu wykonania przy użyciu ewidencji domeny aplikacji Problem Chcesz wymusić górny limit uprawnień dostępu dla wszystkich asemblacji ładowanych do okre­ ślonej domeny aplikacji.

398

C#

-

Księga przykładów

Rozwiązanie Skonfiguruj zasady zabezpieczeń, aby zagwarantować odpowiednie uprawnienia w oparciu o zaplanowaną ewidencję, którą chcesz przypisać do domeny aplikacji. Podczas tworzenia domeny aplikacji przy użyciu metody statycznej

CreateDomain klasy System.AppDomain, należy

System.Security.Policy.Evidence,

zawierający obiekty ewidencji domeny aplika­

dostarczyć zbiór

cji. Asemblacje, których uprawnienia chcesz limitować, załaduj do tej domeny aplikacji.

Omówienie Tak samo jak moduł runtime przypisuje uprawnienia do asemblacji w oparciu o ewidencje, które asemblacje prezentują podczas ładowania, przypisuje on także uprawnienia domenom aplikacji w oparciu o ich ewidencję. Moduł runtime nie przypisuje ewidencji do domen aplikacji w taki sam sposób, jak do asemblacji, ponieważ nie ma niczego, na czym można byłoby ją oprzeć. Zamiast tego kod tworzący domenę aplikacji musi przypisać cę ewidencję w razie potrzeby.

Uwaga Do kalkulowania uprawnień dla domeny aplikacji moduł runtime stosuje tylko poziomy zabezpieczeń przedsiębiorstwa, komputera i użytkownika; zasady zabezpieczeń istniejących domen aplikacji nie odgrywają tu żadnej roli. Przepis 1 3. 1 2 omawia zasady zabezpieczeń domeny aplikacji.

Domeny aplikacji nie posiadające ewidencji są transparentne dla mechanizmu zabezpieczeń kodu. Domeny aplikacji z przypisaną ewidencją mają przyznany zestaw uprawnień, utworzony w oparciu o zasady zabezpieczeń i odgrywają ważną rolę w rozstrzygnięciu żądań bezpieczeń­ stwa CAS. Kiedy wykonanie aplikacji przekracza granice domeny aplikacji, moduł runtime rejestruje to przejście na stosie wywołania. Kiedy żądanie bezpieczeństwa wywołuje wyjście ze stosu, rekordy przejścia domeny aplikacji są przetwarzane, podobnie jak inne rekordy stosu: moduł runtime porówna przyznany zestaw uprawnień z rekordem stosu, aby upewnić się, czy zawiera on żądane uprawnienia. W efekcie uprawnienia przyznane domenie aplikacji określają górny limit możliwości całego załadowanego do niej kodu. Ważnym przykładem użycia ewidencji domeny aplikacji jest Microsoft Internet Explorer. I nternet Explorer utworzy domenę aplikacji dla każdej strony, z której pobiera zarządzane kon­ trolki. Wszystkie kontrolki załadowane z danej strony - podobnie jak asemblacje, które ładują działają w tej samej domenie aplikacji. Gdy Internet Explorer tworzy domenę aplikacji dla danej strony, przypisuje do niej ewidencję

System.Security.Policy.Site.

Oznacza co, że podczas

ładowania asemblacji przez pobraną (np. z dysku lokalnego) kontrolkę, działania tej asemblacji będą ograniczone przez uprawnienia przypisane domenie aplikacji w oparciu o ewidencję strony

(Site)

i zasady zabezpieczeń.

Uwaga Jeżeli ewidencja nie zostanie jawnie przypisana do domeny aplikacji podczas jej tworzenia, domena nie będzie miała żadnego wpływu na żądania zabezpieczeń.

Rozdział 1 3: Bezpieczeństwo modułu Runtime 399 Aby przypisać ewidencję do domeny aplikacji, należy ucworzyć zbiór Evitknce i dodać do niego żądane obiekty ewidencji przy użyciu metody Evitknce.AddHost. Przy tworzeniu nowej domeny aplikacji należy przekazać zbiór Evitknce do jednej z przeciążonych odmian metody statycznej CreateDomain. Zwykły proces rozwiązywania zasad modułu runtime określa zestaw uprawnień domeny aplikacji. Pokazana niżej aplikacja AppDomainEvidenceExample ilustruje przypisanie ewidencji do domeny aplikacji. Przykładowi temu odpowiada scenariusz, w którym aplikacja ładuje kod określonego wydawcy do odpowiadającej mu domeny aplikacji. Dzięki przypisaniu do domeny aplikacji ewidencji System.Security.Policy.Publisher, reprezentującej wydawcę oprogramowania, przykład ogranicza możliwość ładowania kodu do domeny aplikacji. Stosując zasady zabezpie­ czeń można przypisać kodowi wydawcy maksymalny zestaw uprawnień, stosownie do zaufania, którym jest obdarzony sam wydawca. using System ; using System . Secu rity . Policy ; using System . Secu rit y . C ryptog raph y . X509C e rt ificates ; public class AppDomainEvidenceExample { public s tatic void Main ( ) {

li Two rzenie nowej domeny aplikacj i dla każdego wydawcy , któ rego kod li będz ie ładowany p rzez apl ikac j ę . Do metody C reateAppDomain należy li p rzekazać nazwę fi rmy o raz nazwę pliku j ej certyfikatu X . 509v3 . AppDomain appDoml

=

C reateAppDomain ( " Litwa re " , " l i twa re . c e r " ) ;

AppDomain appDom2 = C reateAppDomain ( " Fa b rikam " , " fab rikam . cer" ) ;

li Ładowanie kodu różnych wydawców do odpowiedniej domeny aplikacj i . }

li li li li

Metoda two rzy nową domenę aplikacj i , do któ rej j est ładowany kod wskazanego wydawc y . A rgument name o k reśla nazwę domeny . Argument certFile wska z u j e nazwę pliku , zawie raj ącego c e rtyfikat X . 509v3 wydawcy , któ rego kod będz ie u ruchamiany w nowej domenie .

p rivate static AppDomain C reateAppDomain ( st ring name , st ring cert File ) {

li Two rzenie nowego obiektu X509C e rtificate na pod stawie zawa rtości li wskazanego pliku ce rtyfikatu X . 509v3 . X509Certificate c e rt

=

X509Ce rtificate . C reateF romCe rt F i l e ( certFile ) ;

li Two rzenie nowej ewidencj i typu Publisher z obiektu X509Ce rt ificate . Publ isher publisherEvidence = new Publishe r ( ce rt ) ;

li Two rzenie nowego zbioru Evidence . Evidence evidence = new Evidence ( ) ;

li Dodanie ewidenc j i Publisher do z bio ru Evidence . evidence . AddHost ( publisherEvidence ) ;

li Two rzenie nowej domeny aplikacj i i dołączenie z bi o ru Evidenc e , li zawie raj ącego ewiden c j ę Publishe r .

400

C#

Księga przykładów

-

ret u rn AppDomain . C reateDomain ( name , evidence ) ; } }

1 3.1 2 Posługiwanie się zabezpieczeniami czasu wykonania przy użyciu zasad zabezpieczeń domeny aplikacji Problem Chcesz z programu sterować uprawnieniami przyznawanymi asemblacji.

Rozwiązanie Skonfiguruj programowo zasady zabezpieczeń domeny aplikacji, do której ładujesz asemblację.

Omówienie Zasady zabezpieczeń obejmują cztery poziomy: przedsiębiorstwa, komputera, użytkownika i domeny aplikacji. Moduł runtime określa uprawnienia przydzielane dla asemblacji poprzez określenie zestawu uprawnień wynikających z każdego poziomu zasad zabezpieczeń, a następ­ nie wylicza część wspólną (logiczne AND) czterech zestawów uprawnień. Uprawnienia z tego obszaru stanowią ostateczny zestaw przyznany asemblacji.

Jeżeli nawet zasady zabezpieczeń poziomu przedsiębiorstwa, komputera lub użytkownika określają grupę kodu Leve/Final, która instruuje moduł runtime, aby nie sprawdzał zasad niższego poziomu, to i tak moduł runtime zawsze użyje zasad poziomu domeny aplikacji do określenia zestawu uprawnień przyznanego asemblacji. Ważne

Tylko zasady poziomu przedsiębiorstwa, komputera i użytkownika są konfigurowane statycznie przy użyciu narzędzi administracyjnych. Ponieważ domeny aplikacji nie egzystują poza czasem wykonania, nie jest możliwe statyczne skonfigurowanie zasad domeny aplikacji. Aby skonfi­ gurować zasady zabezpieczeń domeny aplikacji, należy utworzyć programowo poziom zasad, a następnie przypisać go do domeny aplikacji. Konstruowanie poziomu zasad za pomocą kodu może być pracochłonnym i żmudnym zada­ niem, zależnie od złożoności wymaganych zasad zabezpieczeń. Klasa System.Security.Policy.Policylevel reprezenruje poziom zasad zabezpieczeń. Wewnątrz obiektu Policylevel należy zbudować hierarchię grup kodu, definiujących warunki uczestnictwa, zestawy uprawnień oraz atrybuty dla każdej z grup. Istnieje wiele różnych typów używanych do budowania poziomu zasad; omówienie ich wykracza poza ramy niniejszej książki. Szczegółowe omówienie tych klas i przykłady programowej konstrukcji

poziomu zasad zawiera wymieniona wcześniej publikacja Programming .NET Security.

Rozdział 1 3: Bezpieczeństwo modułu Runtime 401

Wskazówka Zazwyczaj stosuje się narzędzia wspierające tworzenie poziomu zasad oraz zapisanie definicji do pliku; można ją potem załadować z dysku na żądanie. Klasa PolicyLevel zawiera dwie metody, ułatwiające uproszczenie tego procesu: ToXml prze­ kształca obiekt PolicyLevel do łatwego do zapisania formatu, a FromXml rekonstruuje obiekt PolicyLevel z wcześniej zapisanego formatu.

Po utworzeniu obiektu

PolicyLevel,

wyrażającego postulowane zasady zabezpieczeń, można

System.AppDomain do domeny PolicyLevel do metody AppDomain. SetAppDomainPolicy. Kod projektanta musi zawierać element ControlDomainPolicy uprawnie­ nia SecurityPermission, aby wywołać SetAppDomainPolicy. SetAppDomainPolicy można zawołać tylko raz w każdej domenie aplikacji; wywołanie SetAppDomainPolicy po raz drugi spowoduje wyjątek System.Security.Policy.PolicyException. Nie trzeba przypisywać obiektu PolicyLevel do domeny aplikacji przed załadowaniem do przypisać go do domeny aplikacji. Należy uzyskać odnośnik aplikacji, która ma być konfigurowana i przekazać obiekt

niej asemblacji. Asemblacje załadowane przed ustanowieniem zasad zabezpieczeń uzyskują zestaw uprawnień oparty o zasady zabezpieczeń poziomu przedsiębiorstwa, komputera lub użytkownika. Zasady domeny aplikacji zastosowane zostaną tylko do asemblacji załadowanych po ich skonfigurowaniu. Możliwość ta jest wykorzystywana wówczas, gdy chce się załadować zaufane współdzielone asemblacje do domeny aplikacji, która nie powinna być ograniczana zasadami domeny aplikacji. Pokazany niżej przykład aplikacji AppDomainPolicyExample demonstruje proces tworzenia poziomu zasad i przypisywania go do domeny aplikacji. Przykład ten tworzy poziom zasad przyznających uprawnienia w oparciu o wydawcę asemblacji - wyrażony w terminologii ewi­ dencji

System.Security.Policy.Publisher.

using System ; using System . Secu rity ; using System . Secu rity . Policy ; using System . Secu rity . C ryptog raphy . X599Certificates ; public class AppDoma inPolicyExample { public static void Main { ) {

li Two rzenie nowej domeny apl ikacj i , do któ rej będą ładowane pob rane li asemblacj e . AppDomain domain

=

AppDomain . C reateDomain ( " modules " ) ;

li Ładowanie asemblacj i do domeny aplikacj i , któ ra n ie ma być li ograniczana p rzez zasady zabezpieczeń domeny aplikacj i . l i Konfigu rowan ie zasad zabez pieczeń dla nowego obiektu AppDomain . SetDomainPol icy { domain ) ;

li Ładowanie pob ranyh asemblac j i do zabezpieczonej domeny aplikacj i . }

402

C#

-

Księga przykładów li li li li

Metoda konfig u ru j e zasady zabezpieczeń p rzekazywanego obiektu AppOomain . Zasady zabezpieczeń s powodu j ą p rzypisanie innych up rawnień dla każdej asemblacj i w zależności od wydawcy . Asembl acj e nieznanych wydawców nie ot rzymuj ą żadnych u p rawnień .

p rivate static void SetOomainPolicy ( AppDomain domain ) {

li Two rzenie nowego pustego obiektu PolicyLevel dla domeny aplikacj i . Pol icyLevel policy = PolicyLevel . C reateAppDomainLevel ( ) ;

li li li li li li

Two rzenie nowego obiektu Fi rstMatchCodeG rou p , pełniącego funkc j ę głównego węzła hie ra rchii g rup kodu . G rupę tę należy s konfigu rować , aby zapewnić zgodność z całym kodem , używaj ąc wa runku członkostwa AllMembe rshipCondition i p rzyznać członkom zbiór up rawnień o nazwie Nothing . Oznacza t o , że wszystkie asemblac j e zaczyna j ą od pustego zbio ru p rzyznanych up rawnień na poziomie zasad domeny apl ikacj i .

policy . RootCodeG roup

=

new Fi rstMatchCodeG rou p (

new AllMembe rshipCondition ( ) , new PolicyStatement ( policy . GetNamedPe rmi s s ionSet ( " Nothing " ) ) );

li li li li li li li li li li

Two rzenie zbio ru g rup kodu w celu określenia , j akie u p rawnienia maj ą być p rzyz nane asemblacj i utwo rzonej p rzez konk ret nego wydawcę . Ponieważ główna g rupa kodu to Fi rstMatchCodeG rou p , p rocedu ra rowiązywania zasad s p rawdza asemblac j e tylko wobec tych g rup pod rzędnych , dopóki nie zna j d z ie pie rws zego t rafienia . Każda g rupa kodu j est two rzona p rzy użyciu at rybutu Exclus ive , aby zagwa rantować , że asemblac j a n ie uzyska dodat kowych u p rawnień z innej g ru py kodu . Two rzenie g rupy kod u , któ ra p rzyznaj e zestaw up rawnień FullTrust asemblacj om opublikowanym p rzez Microsoft .

X509Certificate mic rosoftCert

=

X509Ce rtificate . C reateF romCe rtFile ( " mic rosoft . ce r" ) ; pol icy . RootCodeGroup . AddChild ( new UnionCodeGro u p ( new Publishe rMembe rsh ipCondit ion ( m i c rosoftCe rt ) , new PolicyStatement ( policy . GetNamedPe rmis sionSet ( " FullTrust " ) , PolicyStatementAt t ribute . Exclu sive ) ));

li Two rzenie g rupy kod u , p rzyznaj ącej up rawnienia inte rnetowe li asemblacj om opublikowanym p rzez Litwa re , I n c . X509Ce rtificate l itwa reC e rt

=

X509Ce rtificate . C reateF romCert File ( " l itwa re . ce r " ) ; policy . RootCodeG roup . AddChild ( new UnionCodeG ro u p ( new Publishe rMembersh ipCondit ion ( l itwareCert ) , new PolicyStatemen t ( policy . GetNamedPe rmiss ionSet ( " I n t e rnet " ) , PolicyStatementAtt ribute . Exc l u sive ) ));

Rozdział

1 3:

Bezpieczeństwo modułu Runtime 403

li Two rzenie g rupy kodu , p rzyznaj ącej up rawn ienie Execut ion asemblacj om li opublikowanym p rzez Fab rikam , I n c . X599Ce rtificate fabrikamCert = X599Ce rtificate . C reateF romC e rtFile ( " fab rikam . ce r " ) ; policy . RootCodeGroup . AddChild ( new Un ionCodeG rou p ( new Publ ishe rMembe rs hipCondition ( fabrikamCert ) , new PolicyStatement ( policy . GetNamedPe rmis s ionSet ( " Execution " ) , PolicyStatementAtt ribute . Exclusive ) ));

li li li li li li

Dodanie f inalnej g rupy kodu , p rzechwytuj ącej wszyst kie asemblacj e , któ re nie należą d o j ednej z g rup odpowiednich wydawców. G rupa ta ot rzymymu j e zestaw up rawnień Nothing . Ponieważ g rupa j est two rzona z at rybutem Exclu sive , asemblac j e n ie ot rzyma j ą żadnych u p rawnień z innych g rup ani nawet z wyż szych poziomów zasad ( p rzeds iębio rstwa , maszyny l u b użyt kownika ) .

policy . RootCodeGroup . AddChild ( new Un ionCodeG rou p ( new AllMembe rshipCondition ( ) , new PolicyStatement ( policy . GetNamedPe rmissionSet ( " Nothing " ) , PolicyStatementAt t ribute . Excl u sive ) ));

li P rzypisanie zasad do udostępnionej domeny aplikac j i . domain . SetAppDomainPolicy ( policy ) ; } }

1 3.1 3 Określenie, czy aktualny użytkownik jest członkiem danej grupy Windows Problem Chcesz określić, cz:y bieżący użytkownik aplikacji jest członkiem określonej grupy użytkowni­ ków systemu Windows.

Rozwiązanie Uzyskaj obiekt System.Security.PrincipaL Windowsldentity, reprezentujący bieżącego użytkow­ nika systemu Windows, wywołując statycz114 metodę Windowsldentity. GetCurrent. Następnie przekaż zwrócony obiekt Windowsldentity do konstruktora klasy System.Security.PrincipaL WindowsPrincipal, aby otrzymać obiekt WindowsPrincipal. Na zakończenie, wywołaj metodę IslnRole obiektu WindowsPrincipal, aby określić, czy użytkownik należy do określonej grupy systemu Windows.

404

C#

-

Księga przykładów

Omówienie Mechanizm

RBS

systemu . NET Framework udostępnia poziom abstrakcji dla mechanizmu

zabezpieczeń zależnych od użytkownika systemu operacyjnego za pośrednictwem poniższych interfejsów: • •

System. Security. Principal.Ildentity System. Security.Principal. !Principal

Interfejs

lldentity

reprezentuje obiekt, na konto którego kod działa - na przykład, kontro

użytkownika lub konto usługowe. Interfejs

!Principal reprezentuje Ildentity

(tożsamość) tego

obiektu i zestaw ról. do których ten obiekt należy. Rola stanowi po prostu kategoryzację, użytą do pogrupowania obiektów o podobnych właściwościach zabezpieczeń, takich jak grupy systemu Windows. W celu zintegrowania system

.

RBS z mechanizmem zabezpieczeń użytkowników systemu Windows,

NET Framework zapewnia dwie następujące klasy (specyficzne dla systemu Windows), I!dentity i !Principal:

które implementują interfejsy • •

System.Security.Principal. Windows!dentity System.Security.Principal. WindowsPrincipal

Windowsldentity implementuje interfejs Ildentity i reprezentuje użytkownika systemu WindowsPrincipal implementuje !Principal i reprezentuje zbiór grup systemu Windows, do których należy użytkownik. Ponieważ .NET RBS jest rozwiązaniem ogólnym, Klasa

Windows. Klasa

zaprojektowanym w celu zapewnienia niezależności od platformy, projektant kodu nie ma dostępu do właściwości i funkcji konta użytkownika systemu Windows poprzez interfejsy

Ildentity i !Principal. Do realizacji tych Windowsldentity i WindowsPrincipal.

zadań należy się bezpośrednio posługiwać obiektami

Aby określić, czy bieżący użytkownik jest członkiem określonej grupy systemu Windows, należy najpierw wywołać metodę statyczną zwraca obiekt

Windowsldentity,

Windowsldentity. GetCummt.

Metoda

GetCurrent

reprezentujący użytkownika systemu Windows, na którego

Windows­ Windows!dentity jako argument do konstruktora. Na zakończenie należy wywołać metodę lslnRole obiektu WindowsPrincipal, aby sprawdzić, czy użytkownik znajduje się w określonej grupie (roli). IslnRole zwraca true, jeżeli użytkownik jest członkiem określonej grupy, w przeciwnym wypadku zwraca fa/se. koncie działa bieżący wątek. Następnie należy utworzyć instancję dla nowego obiektu

Principal i

przekazać obiekt

Uwaga Można uzyskać odniesienie /Principa/ do obiektu WindowsPrincipal, który reprezentuje bieżącego użytkownika, poprzez zastosowanie właściwości statycznej Cur­ rentPrincipal, klasy System. Threading. Thread. Jednakże technika ta zależy od konfigura­ cji głównych zasad dla bieżącej domeny aplikacji; w przepisie 1 3. 1 4 podanych jest więcej szczegółów.

Metoda

lslnRole

ma trzy przec1ąwne odmiany. Pierwsza pobiera jako argument łańcuch

zawierający nazwę grupy, dla której ma nastąpić sprawdzenie. Nazwa grupy musi mieć postać

Rozdział

1 3:

Bezpieczeństwo modułu Runtime 405

[DomainName]\ [GroupName] dla grup domenowych i [MachineName]\[GroupName] dla grup lokalnych. Dla sprawdzenia przynależności członka grupy systemu Windows, należy posłużyć się składnią BUILTIN\[GroupName] . lslnRo/e wykonuje badanie dla określonej nazwy grupy bez rozróżniania wielkości liter. Następne przeciążenie lslnRo/e akceptuje wartość int, określającą identyfikator roli (Role Identifier - RID). RID umożliwiają identyfikację grup niezależnie od wersji językowej systemu operacyjnego. Trzecie przeciążenie lslnRo/e akceptuje element typu wyliczeniowego System.Secu­ rity.Principal WindowsBuiltlnRo/e. Typ wyliczeniowy WindowsBuiltlnRo/e definiuje zestaw ele­ mentów, reprezentujących każdą z wbudowanych grup systemu Windows. Tabela 1 3-3 podaje nazwy, identyfikatory RID i wartość WindowsBuiltlnRo/e dla standardowych grup systemu Windows. Tabela 1 3-3 Wbudowane nazwy grup systemu Windows i identyfikatory

Nazwa konta

RID

BUILTIN\Account Operators

Ox224

AccountOperator

BUILTIN\Administrators

Ox220

Administrator

BUILTIN\Backup Operators

Ox227

BackupOperator

BUILTIN\Guests

Ox222

Guest

BUILTIN\Power Users

Ox223

PowerUser

BUILTIN\Print Operators

Ox226

PrintOperator

BUILTIN\Replicators

Ox228

Replicator

BUILTIN\Server Operators

Ox225

System Operator

BUILTIN\Users

Ox221

User

(Hex)

Wartość WintlowsBuiltlnRok

Uwaga Przy korzystaniu z systemu Microsoft Windows Server 2003 i późniejszych, klasa Windows/dentity udostępnia przeciążone konstruktory, pozwalające otrzymać obiekt Windowsldentity reprezentujący nazwę użytkownika. Można wykorzystać ten obiekt i proces przedstawiony w niniejszym przepisie do określenia, czy użytkownik jest członkiem określonej grupy systemu Windows. Próba użycia któregoś z w/w konstruktorów przy posługiwaniu się wcześniejszą wer­ sją systemu Windows, konstruktor Windowsldentity wywoła wyjątek System.Argument­ Exception. W tym przypadku należy użyć lokalnego kodu, aby uzyskać żeton dostępu systemu Windows, reprezentujący żądanego użytkownika. Następnie można wykorzy­ stać ten żeton do utworzenia instancji obiektu Windowsldentity, przepis 1 3. 1 5 wyjaśnia, jak otrzymywać żetony dostępu systemu Windows dla specyficznych użytkowników.

Aplikacja WindowsGroupExample, pokazana niżej, demonstruje sprawdzenie, czy bieżący użyt­ kownik jest członkiem zestawu wskazanych grup Windows. Grupy, które mają być sprawdzone, należy wyspecyfikować jako argumenty z linii poleceń. Nazwę grupy należy poprzedzić prefik­ sem z nazwą komputera albo domeny, lub BUILTIN dla standardowych grup systemowych.

406

C#

-

Księga przykładów

u s ing System ; u s ing System . Security . Principal ; public class WindowsG roupExample { public static void Main ( st ring [ ) a rg s ) {

li Uzyskanie obiektu Windows ident it y , rep rezent u j ącego aktualnie li zalogowanego użytkownika Windows . Windowsidentity identity = Windowsidentit y . GetCu r rent ( ) ;

li Two rzenie obiektu Windows P rincipal , rep rezentuj ącego opcj e li zabezpieczeń wskazanego obiekt u Windows ident ity , w tym wypadku listę li g rup Windows , do któ rej należy bieżący użyt kownik . WindowsP rincipal p rincipal = new WindowsP rincipal ( identit y ) ;

li I te rac j a p rzez kolej ne g rupy podawane j ako a rg ument y wie rsza li polecenia i sp rawdzanie , czy b ieżący użytkownik j est członkiem li każdej z nich . fo reach ( s t ring role in a rg s ) { Console . Writeline ( " I s { 0 } a member of { l } ? = { 2 } " , identity . Name , role , p rincipal . Is inRole ( role ) ) ; } } }

Uruchamianie tego przykładu jako użytkownik o nazwie Darryl na komputerze MACHINE przy użyciu polecenia WmdowsGroupExample BUILTIN\Administrators BUILTIN\Users MACHINE\Accountants, dane wyjściowe na konsoli będą miały następującą postać. Is MACHINE\Da r ryl a membe r of BUI LTIN\Administ rat o r s ? = False I s MACHINE\Da r ryl a membe r of BUILTIN\Use r s ? = T rue I s MACHINE\Da r ryl a membe r of MACHINE\Accountants? = T rue

1 3.1 4 Ograniczanie użytkowników mających prawo do wykonania kodu Problem Chcesz ograniczyć wykonywanie części twojego kodu do określonych użytkowników, posługu­ jąc się nazwą użytkownika lub rolą, w której partycypuje.

Rozwiązanie Wykorzystaj klasę System.Security.Permissions.Principa/Permission i jej równoważny atrybut System.Security.Permissions.Principa/PermissionAttribute, aby zabezpieczyć elementy programu za pomocą żądań RBS.

Rozdział 1 3: Bezpieczeństwo modułu Runtime 407

Omówienie System .NET Framework dopuszcza zarówno imperatywne, jak i deklaratywne żądania RBS. Klasa Principa/Permission umożliwia użycie imperatywnych instrukcji zabezpieczeń, a odpowia­ dający jej atrybut PrincipalPermissionAttribute pozwala na użycie instrukcji deklaratywnych. Żądania RBS używają tej samej składni, co żądania CAS, ale RBS specyfikują nazwę aktu­ alnego użytkownika albo (częściej) role, w których użytkownik musi partycypować. Żądanie RBS instruuje moduł runtime, by sprawdził nazwę i role dla bieżącego użytkownika. Jeśli nie spełniają one wymagań zawartych w żądaniu, zostanie wywołany wyjątek System.Security.

SecurityException. Poniższy fragment kodu pokazuje składnię imperatywnego żądania zabezpieczeń.

li Impe ratywna s kładnia żądania zabezpieczeń opa rtego na rolach . public static void SomeMethod ( ) { P rincipalPe rmis s ion pe rm

=

new P rincipalPe rm i s s ion ( " U s e rName " , " RoleName " ) ; pe rm . Demand ( ) ; }

Należy najpierw utworzyć obiekt Principa/Permission, specyfikujący nazwę użytkownika i nazwę roli, która będzie zgłoszona, a następnie należy wywołać metodę Demami. Można wyspecyfiko­ wać tylko jedną nazwę użytkownika i roli dla jednego żądania. Jeżeli jednym z tych czynników jest null, satysfakcjonującą odpowiedzią na żądanie będzie każda wartość. Inaczej niż z upraw­ nieniami dostępu dla kodu, żądanie RBS nie powoduje przejścia przez stos; moduł runtime ustala tylko nazwę użytkownika i rolę aktualnego użytkownika. Poniższy fragment kodu pokazuje składnię deklaratywnego żądania zabezpieczeń.

li Dekla ratywne żądanie zabezpieczeń opa rte na rolach . [ P rincipalPe rmi s s ion ( Secu rityAction . Demand , Name = " Us e rName " , Role " RoleName " ) l public static void SomeMethod ( ) { I* . . . *I} =

Można umieścić deklaratywne żądania RBS na poziomie klasy lub członka. Poziom klasy ozna­ zastosowanie żądania do wszystkich członków danej klasy, chyba że żądanie odnoszące się do określonego członka nadpisze żądanie klasy. Ogólnie rzecz biorąc, programista ma swobodę wyboru pomiędzy zastosowaniem żądań RBS jednego i drugiego rodzaju. Jednakże imperatywne żądania bezpieczeństwa pozwalają na integrację żądań RBS z logiką kodu, co umożliwia osiągnięcie wyrafinowanego zachowania żądań RBS. Ponadto, jeżeli projektant nie zna roli lub nazwy użytkownika, aby tworzyć żądania podczas kompilacji, będzie musiał użyć żądań imperatywnych. Żądania deklaratywne RBS mają tę przewagę, że są oddzielone od logiki kodu i łatwiejsze do zidentyfikowania. Ponadto można przeglądać deklaratywne żądania RBS przy użyciu narzędzia Permview.exe (omówione w prze­ pisie 1 3.6). Niezależnie od tego, które z żądań zostaje zaimplementowane - deklaratywne, czy imperatywne - należy zapewnić dostęp modułu runtime do nazwy i ról aktualnego użytkow­ nika, żeby można było poprawnie oszacować żądanie. cza

408

C#

-

Księga przykładów

Klasa

System. Threading. Thread

reprezentuje wątek systemu operacyjnego, który wykonuje

zarządzany kod. Statyczna właściwość

CurrentPrincipalklasy Threadzawiera instancję lprincipa/,

reprezentującą użytkownika, dla którego został uruchomiony zarządzany wątek. Na poziomie systemu operacyjnego każdy wątek ma przypisany żeton dostępu systemu Windows, repre­ zentujący konto, dla którego dany wątek został uruchomiony. Należy zauważyć, że instancja

!Principal oraz żeton dostępu systemu Windows stanowią dwa niezależne byty. System Windows korzysta z żetonu dostępowego, by wymusić zabezpieczenia systemowe, podczas gdy moduł systemu .NET runtime wykorzystuje instancję

IPrincipal do

oceny żądań RBS na poziomie

aplikacji. I chociaż mogą one reprezentować tego samego użytkownika (i często tak się dzieje), nie musi tak być za każdym razem. Domyślnie właściwość

Thread. CurrentPrincipal

jest niezdefiniowana.

Ponieważ otrzy­

manie informacji dotyczącej użytkownika może być czasochłonne, a korzysta z niej tylko niewiele aplikacji, projektanci modułu systemu .NET zdecydowali się na opóźnioną inicja­

CurrentPrincipal. Za pierwszym razem, gdy kod odwoła się do właściwości Thread. CurrentPrincipal, moduł runtime przypisze i nstancję !Principal do tej właściwości przy

cje; właściwości

użyciu następującej logiki:

1. Jeżeli domena aplikacji, w której dany wątek jest wykonywany, ma domyślny podmiot zabezpieczeń, moduł runtime przypisze go do właściwości

Thread. CurrentPrincipal.

Standardowo domeny aplikacji nie mają domyślnych podmiotów zabezpieczeń. Można ustawić domyślny podmiot za pomocą metody SetThreadPrincipal dla obiektu System.App­ Domain, reprezentującego konfigurowaną domenę aplikacji. Kod musi zawierać element ControiPrincipal uprawnienia SecurityPermission, aby wywołać SetThreadPrincipal. Domyślny podmiot zabezpieczeń można ustawić tylko raz dla każdej domeny aplikacji; kolejne wywoła­ nie SetThreadPrincipal spowoduje wyjątek

2.

System.Security.Policy.PolicyException.

Jeżeli domena aplikacji nie ma domyślnych podmiotów zabezpieczeń, o tym, którą imple­ mentacje;

!Principal zastosować

i przypisać do

Thread. CurrentPrincipal,

decyduje zasada

domyślnego podmiotu zabezpieczeń domeny aplikacji. Aby skonfigurować zasady domyślnego podmiotu domeny aplikacji, należy wykorzystać

AppDomain dla domeny aplikacji i wywołać dla tego obiektu metodę SetPrincipal­ Policy. Metoda SetPrincipalPolicy akceptuje element typu wyliczeniowego System.Security. Principal.PrincipalPolicy, określajacej typ obiektu !Principal, aby go przypisać do Thread. CurrentPrincipal. Kod musi zawierać element ControiPrincipal uprawnienia SecurityPermis­ sion, aby mógł wywoływać SetPrincipalPolicy. Tabela 1 3-4 wyszczególnia dostępne wartości Principa/Policy; wartością domyślną jest UnauthenticatedPrincipal. obiekt

3. Jeżeli kod zawiera element ControlPrincipal uprawnienia SecurityPermission, można utwo­

!Principal i przypisać go bezpośrednio do właściwości Thread. CurrentPrincipal. To powstrzyma moduł runtime przed przypisywaniem domyśl­ nych obiektów !Principal lub tworzeniem nowych w oparciu o zasady. rzyć instancje; własnego obiektu

Tabela 1 3-4

Elementy typu wyliczeniowego Principa/Policy

Nazwa elementu

NoPrincipal

Opis Nie zostaje ustalony żaden obiekt

Principal zwraca odniesienie null.

!Principal, a Thread. Current­

Rozdział 1 3: Bezpieczeństwo modułu Runtime Tabela 1 3-4

409

Elementy typu wyliczeniowego Principa/Policy

Nazwa elementu

Opis

UnauthenticatedPrincipai

Zostaje utworzony pusty obiekt System.Secun'ty.Principai. Generic­ Principai i jest on przypisany do Ihread. CurrentPrincipaL

Wind()UJsPrincipai

Obiekt WindowsPrincipai reprezentujący zalogowanego aktual­ nie użytkownika systemu Windows zostaje utworzony i przypi­ sany do Ihread. CurrentPrincipai.

Niezależnie od użytej metody, która ma ustalić !Principai dla bieżącego wątku, czynność ta musi być dokonana przed użyciem żądania zabezpieczeń RBS. W innym wypadku właściwa informa­ cja użytkownika (!Principal) nie będzie dostępna i moduł runtime nie będzie mógł przetwarzać żądania. Zazwyczaj przy pracy na platformie systemu Windows, ustawia się zasady podmiotu zabezpieczeń dla domeny aplikacji na PrincipalPolicy. WindowsPrincipal (jak to pokazano niżej), aby otrzymać informacje użytkownika systemu Windows. li Uzyskanie odniesienia do bieżącej domeny aplikac j i . AppDomain appDomain = System . AppDomain . Cu r rentDomain ; li Konfigu rowanie bieżącej domeny aplikacj i do korzystania z podmiotów li zabezpieczeń Windows . appDomain . SetP rincipalPolicy ( System . Se c u rity . P rincipal . P rincipalPol icy . WindowsP rincipal } ;

Plik RoleBasedSecurityExample.cs w przykładowym kodzie tego rozdziału demonstruje użycie obu rodzajów żądań RBS - imperatywnego i deklaratywnego. W pierwszym fragmencie pokazane są trzy metody chronione przy użyciu żądań RBS. Jeżeli obiekt Ihread. CurrentPrincipal nie będzie zgodny z żądaną nazwą użytkownika lub przynależnością do roli, żądanie wywoła wyjątek Securi­ tyException, który metoda musi obsłużyć lub przekazać do metody wyżej ulokowanej na stosie. public static void P rotectedMethod l ( ) { li Impe ratywne żądanie zabezpieczeń dla bieżącego podmiotu zabezpieczeń , li rep rezentuj ącego tożsamość: "Anya " , role odg rywane p rzez podmiot nie są li istot ne . System . Secu rit y . Pe rmission s . P rincipalPe rmission pe rm = new System . Secu rity . Pe rmission s . P ri ncipalPe rmis s ion (@"MACHINE\Anya " , n u ll } ; pe rm . Dem and ( ) ; } public static void P rotectedMethod2 ( ) {

/I

Impe ratywne żądanie zabezpieczeń dla bieżącego podmiotu zabezpieczeń ,

li maj ącego być członkiem roli " Manage rs " a l bo " Develope r s " . Jeżeli li podmiot j est członkiem j ednej z tych ról , uzys ka dostę p . P rzy użyciu li P rincipalPerm i s s ion można określić t y l ko zależność typu alternatywy li ( O R ) . Wynika to s tąd , że metoda P rincipalPolicy . Inte rsect zawsze li zwraca puste u p rawnienia , j eżeli oba a rgumenty nie są identyc zne . Można li j ednak s konst ruować: ba rdziej złożone wa runki logiczne .

41 0

C# - Księga przykładów

li W t ym p rzypadku nazwa toż samości j est nieistotna . System . Se c u rity . Permissions . P rincipalPermission pe rml new System . Secu rity . Permissions . P rincipalPe rmis s ion ( null , @"MACHINE\Manage rs " ) ; System . Securit y . Permi s s ions . P rincipalPe rmis s ion pe rm2 = new System . Secu rity . Permission s . P rincipalPe rmiss ion ( null , @"MACHINE\Develope rs " ) ; pe rml . Union ( pe rm2 ) . Demand ( ) ;

} public static void P rotectedMethod3 ( ) {

li Impe ratywne żądanie zabezpieczeń opa rtych na rolach dla bieżącego li podmiotu , reprezentuj ące toż samość o imieniu "Anya " ORAZ członkostwo li rol i "Manage rs " . System . Secu rit y . Permission s . P rincipalPe rmission perm = new System . Secu rity . Permission s . P rincipal Pe rmis s ion ( @ " MACHINE\Anya " , @ " MACHINE\Manage rs " ) ; perm . Demand ( ) ;

} Drugi fragment kodu podaje trzy metody, chronione przy użyciu deklaratywnych żądań RBS, równoważnych pokazanym wyżej żądaniom imperatywnym.

li Deklaratywne żądanie zabezpieczeń opa rtych na rolach dla bieżącego podmiotu , li reprezentuj ące toż samość o imieniu "Anya" p rzy nieistotnej rol i . [ P rincipalPe rmis sion ( Secu rityAction . Demand , Name = @"MACHINE\Anya " ) ] public static void P rotectedMethod l ( ) { l * . . . *I }

li li li li li

Dekla ratywne żądanie zabezpieczeń opa rtych na rolach dla bieżącego podmiot u , maj ącego był członkiem ról "Manage r s " l u b " Develope rs " . Jeżeli podmiot należy do j ednej z tych ról , dostęp zostanie p rzyznany . Można o k reślić tylko zależność typu alternatywy ( OR ) , ale nie koniunkcj i ( AND ) . Nazwa tożsamości j est nieistotna .

[ P rincipalPe rmission ( Secu rityAction . Demand , Role = @"MACHINE\Manage r s " ) ] [ P rincipalPermission ( Secu rityAction . Demand , Role = @"MACHINE\Develope r s " ) J public static void P rotectedMethod2 ( ) { l * . . . *I}

li Dekla rat ywne żądanie zabezpieczeń opa rtych na rolach dla bieżącego podmiotu , 11 reprezentuj ące toż samość o imieniu " Anya" o raz członko stwo roli " Manage rs " .

[ P rincipalPe rmis s ion ( Secu rityAct ion . Demand , Name = @ " MACHINE\Anya " , Role = @"MACHINE\Manage r s " ) ] public static void P rotectedMethod3 ( ) { l * . . . *I }

--

Rozdział 1 3: Bezpieczeństwo modułu Runtime 41 1

1 3.1 5 Personifikacja użytkownika systemu Windows Problem Chcesz uruchomić swój kod w kontekście innego użytkownika systemu Windows, niż bieżące komo użytkownika.

Rozwiązanie System.Security.PrincipaL Windowsldentity reprezentujący użytkownika systemu lmpersonate dla obiektu Windows!dentity.

Uzyskaj obiekt

Windows, którego chcesz personifikować, a następnie wybierz metodę

Omówienie Każdy wątek systemu Windows ma związany z nim żeton dostępu

(access token),

reprezentu­

jący konto systemu Windows, dla którego dany wątek został uruchomiony. System operacyjny Windows korzysta z żetonu dostępu dla określenia, czy wątek ma odpowiednie uprawnienia dla wykonywania chronionych operacji, takich jak odczyt i zapis plików, restartowanie systemu lub zmiana czasu systemowego. Domyślnie zarządzana aplikacja działa w kontekście konta systemu Windows, które zostało użyte do jej uruchomienia. Jest to zazwyczaj pożądane zachowanie, ale niekiedy zachodzi potrzeba uruchomienia aplikacji w kontekście innego konta systemu Windows. Jest to szcz.e­ gólnie częste w przypadku aplikacji po stronie serwera, które przetwarzają transakcje w imieniu użytkowników połączonych zdalnie. Zazwyczaj aplikacje serwerowe pracują w kontekście konta utworzonego specjalnie na potrzeby aplikacji - konta usługowego. Takie konto ma minimalne uprawnienia dostępu do zasobów systemu. Spowodowanie działania aplikacji, jak gdyby była połączonym użytkownikiem, umożliwia jej dostęp do operacji i zasobów zgodnych ze schema­ tem zabezpieczeń właściwym dla tego użytkownika. Kiedy aplikacja przybiera tożsamość innego użytkownika, ten fakt nazywa się personifikacją

(impersonation). Ta funkcja. właściwie zaimple­

mentowana, upraszcza administrację bezpieczeństwa i projektowanie aplikacji, przy zachowa­ niu możliwości zarządzania kontami użytkowników.

Jak powiedziano w przepisie 1 3. 1 4, należący do wątku żeton dostępu i podmiot zabezpieczeń systemu . NET są oddzielnymi bytami, mogącymi reprezentować różnych użytkowników. Technika personalizacji, omówiona w niniejszym przepisie, zmienia tylko żeton dostępu bieżącego wątku; nie zmienia natomiast podmiotu zabezpieczeń wątku. Aby je zmienić, kod musi zawierać element Contro/Principa/ uprawnienia SecurityPermis­ sion i przypisać nowy obiekt System.Security.Principa/./Principal do właściwości Current­ Principal bieżącego wątku systemowego System. Threading. Thread. Ważne

412

C# - Księga przykładów

Klasa Systmz.Security.Principal. Windowsldentity dostarcza funkcjonalności, dzięki której można przywołać personifikację. Jednakże właściwy proces zależy od wersji systemu Windows, w której została uruchomiona aplikacja. Jeżeli jest to wersja System Windows Server 2003 lub póź­ niejsza, klasa Windowsldentity obsługuje przeciążanie konstruktorów tworzących obiekty Windows!dentity w oparciu o nazwę konta reprezentującą użytkownika. We wszystkich wcześ­ niejszych wersjach systemu Windows należy najpierw otrzymać wskaźnik System.lntPtr, zawiera­ jacy odsyłacz do żetonu dostępowego reprezentującego użytkownika. Aby otrzymać odniesienie do żetonu dostępu, należy użyć lokalnej metody, takiej jak funkcja LogonUser z Win32 API.

Uwaga Główny problem związany z personifikacją w systemach Windows 2000 i Windows NT polega na tym, że konto musi posiadać przywilej SE_TCB_NAME, aby wykonać LogonUser. Wymaga to skonfigurowania zasad zabezpieczeń systemu Windows i przyznania dla konta prawa ,,Act as part of operating system" (.Działania jako część systemu operacyjnego"). Sprawia to, że konto uzyskuje bardzo wysoki (w istocie najwyższy możliwy) poziom zaufania. Przywilej SE_TCB_NAME nigdy nie powinien być przyznawany kontom rzeczywistych użytkowników, a tylko starannie chronionym kontom usługowym.

Po uzyskaniu obiektu Windowsldentity reprezentującego użytkownika, który ma być per­ sonifikowany, należy wywołać metodę lmpersonate. Od tego momentu wszystkie działania wykonywane przez kod projektanta nastąpią w kontekście personifikowanego konta. Metoda Impersonate zwraca obiekt Systmz.Security.Principal. WindowsSecurityContext, wskazujący konto aktywne przed personifikacją. Aby powrócić do oryginalnego konta, należy wywołać metodę Undo tego obiektu WindowsSecurityContext. Przytoczona niżej aplikacja konsolowa ImpersonationExample demonstruje personifika­ cję użytkownika systemu Windows. W przykładzie należy użyć dwóch argumentów wiersza poleceń: nazwy konta użytkownika, który ma być personifikowany oraz hasła dla tego konta. W przykładzie użyto funkcji LogonUser dla Win32 API, aby otrzymać żeton dostępu systemu Windows dla określonego użytkownika. Następnie dokonano personifikacji użytkownika, po czym znowu przywrócono kontekst oryginalnego użytkownika. Polecenie lmpersonation­ Example Bob password wykona personifikację użytkownika o nazwisku Bob, o ile takie komo istnieje w lokalnej bazie danych kom. using System ; u s ing System . IO ; u s ing System . Secu rity . P rincipal ; u s ing System . Secu rit y . Pe rmis sion s ; using System . Runtime . I nte ropServices ;

li Sp rawd zenie , czy asemblac j a ma up rawn ienie do wykonania nieza rządzanego kodu li i kont roli podmiotu wąt ku . [ a s sembly : Sec u rityPermi s s ion ( Secu rityAction . RequestMinimum , UnmanagedCode=t rue , Cont rolPrincipal=t rue ) J public class Impe rsonationExample {

-

Rozdział 1 3: Bezpieczeństwo modułu Runtime 41 3 li Defin iowanie s tałych używanych w funkcj i LogonUse r . const int LOGON32_PROVIDER_DEFAULT

0;

=

const int LOGON32_ LOGON_ INTERACTIVE

=

2;

l i Impo rtowanie funkcj i Win32 LogonUse r z bibliotegki advapi32 . dll . li O k reślenie " Setla s t E r r o r = t rue" w celu pop rawnej obsługi dostępu do li kodów błędów Win32 . [ Dl l impo rt ( " advapi32 . dll " , Set la s t E r ro r=t rue ) J static exte rn int LogonUse r ( st ring u s e rName , st ring domain , st ring passwo rd , int logonType , int logonP rovide r , ref I n t P t r acces sToken ) ; public s tatic void Main ( st ring [ ] a rg s ) {

li Two rzenie nowego wskaźnika I n t Pt r , p rzechowuj ącego żeton dostępu li zwracany p rzez funkcj ę LogonUse r . IntPt r acces sToken = I ntPt r . Zero ;

li li li li

Wywołanie LogonUser w celu uzyskania żetonu dla wskazanego użytkown i ka . Zmienna acce s s żeton j est p rzekazana do LogonUser p rzez odnośnik i będ z ie zawierał odsyłacz do żetonu dostępu Windows , j eżeli funkc j a LogonUser zakończy s ię powodzen iem .

int result

=

LogonUse r (

a rgs [ 0 J , a rgs [ l ] , LOGON32_LOGON_ INTERACTIVE , LOGON32_ PROVIDER_ DEFAULT , ref acces sToken

li li li li li li

nazwa loguj ącego s ię użytkown ika . użycie lokalnej bazy kont . hasło użyt kownika . two rzenie inte raktywnego logowania . użycie domyślnego dostawcy . odeb ranie uchwytu - żeton dostępu .

);

li J eżeli kod powrotu LogonUser wynosi z e ro , wys tąpił błąd . li wyświetlenie komunikatu o błędzie i wyj ście . if ( result == 0 )

{

Console . W riteline ( " LogonUser ret u rned e r ro r {0} " , Ma rshal . GetlastWin32 E r ro r ( ) ) ; } else {

li Two rzenie nowego obiektu Windows identity z żetonu dostępu . Windows identity identity = new Windows iden t ity ( ac ces sToken ) ;

li Wyświet lenie aktywnej tożsamoś ci . Console . W riteline ( " Identity befo re impe rsonation

=

{0} " ,

Windowsiden t ity . GetCu rren t ( ) . Name ) ;

li li li li

Personifikacj a wskazanego użytkown ika , zapisanie odniesienia do zwróconego obiektu Windowsimpe rsonationContext , któ ry zawiera informacj e wymagane do powrotu do kontekstu o ryginalnego użyt kownika .

414

C# - Księga przykładów Windowsimpe rsonat ionContext impContext identit y . Impe rsonate ( ) ;

li Wyświetlenie aktywnej tożsamości . Console . Writeline ( " Identity du ring impe rsonation

{0 } " ,

Windows identity . GetC u r rent ( ) . Name ) ;

li * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * li Wykonanie działań j ako personifikowany użyt kownik . li * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * li Powrót d o o ryginalnego użytkownika za pomocą obiektu li Windowsimpe rsonationContext . impContext . Undo ( ) ;

li Wyświetlenie aktywnej toż samości . Console . Writeline ( " Identity a f t e r impe rsonation Windowsidentit y . Get C u r rent ( ) . Name ) ; } } }

{0 } " ,

14 Kryptografia Kryptografia stanowi jeden z najbardziej złożonych aspektów projektowania oprogramowania, z jakim może zetknąć się programista. Teoria nowoczesnych technik kryptograficmych jest skraj­ nie trudna i wymaga zaawansowanej wiedzy matematycznej, którą dysponują tylko nieliczni. Na szczęście biblioteka klas Microsoft .NET Framcwork udostępnia łatwe w użyciu implemen­ tacje większości powszechnych technik kryptograficznych oraz najpopularniejszych i dobrze zna­ nych algorytmów. Przepisy zawarte w tym rozdziale obejmują następujące zagadnienia: • Generowanie liczb kryptograficznie losowych (przepis

14. 1 ) .

• Generowanie i weryfikacja kryptograficznych kodów skróconych (hash) i kodów skróco­

nych z kluczem (przepisy 1 4.2, 1 4.3, 1 4.4 i 1 4.5). • Korzystanie z symetrycznych i niesymetrycznych algorytmów do szyfrowania i odszyfrowy­

wania danych (przepisy 1 4.6 i 1 4.8). • Wyprowadzanie, zapamięrywanie i wymiana kluczy kryptograficznych (przepisy

1 4.7, 1 4.9

i 1 4 . 1 0) . W trakcie lektury tych przepisów w celu zastosowania ich we własnym kodzie warto pamiętać, że technik kryptograficznych nigdy nie stosuje się w sposób iwlowany. Kryptografia nie jest jednoznaczna z bezpieczeństwem; użycie kryptografii jest zaledwie jednym z elementów bez­ piecznego rozwiązania. Szersze omówienie bezpiecznego programowania, w którym kryptogra­ fia odgrywa ogromną rolę, zawiera książka

Writing Secure Code autorstwa Michaela Howarda Bezpieczny kod, APN Promise, 2004).

i Davida LeBlanc, Microsoft Press, 2003 (wydanie polskie:

Innym wyczerpującym źródłem informacji na temat kryptografii, zawierającym pogłębione omó­ wienie klas kryptograficznych, jest inna książka mojego autorstwa (napisana wspólnie z Adamem Freemanem), zatytułowana

Progmmming .NET Security (O'Reilly and Associates, 2003).

Na użytek czytelników nie obeznanych z terminologią stosowaną w kryptografii podaję defi­ nicje kilku ważnych terminów używanych w tym rozdziale. Definicje te pochodzą z opracowa­ nia

Microsoft Computer Dictionary, Fifth Edition (Microsoft



Ciphertext - zaszyfrowany tekst



Encrypt

Press, 2002).

Zakodowana (nieczytelna) postać zaszyfrowanego

komunikatu. -

szyfrowanie

Czynność kodowania (mieszania) informacji w taki sposób, aby

nie nadawała się do odczytania dla nikogo oprócz tych osób, które mają odpowiedni klucz.

416

C# - Księga przykładów •

Key - klucz

Łańcuch bitów, używanych do szyfrowania i odszyfrowywania przekazywa­

nej informacj i . •

Plaintext - czysty tekst

Jawny (niezaszyfrowany lub już odszyfrowany) tekst.

1 4.1 Utworzenie kryptograficznie losowego numeru Problem Chcesz utworzyć numer losowy, który może zostać użyty w aplikacjach kryptograficznych i innych zastosowaniach dotyczących zabezpieczeń.

Rozwiązanie Użyj kryptograficznego generatora liczb losowych, takiego jak klasa

System. Security. Crypto­

graphy.RNGCryptoServiceProvider.

Omówienie Klasa

System.Random

jest generatorem liczb pseudolosowych, używającym matematycznego

algorytmu do symulacji tworzenia losowych wartości. W istocie algorytm ten jest determi­ nistyczny, co oznacza, że zawsze można obliczyć kolejną wygenerowaną liczbę w oparciu o poprzednio wybrany numer. Oznacza to również, że liczby wygenerowane przez klasę Random są nieużyteczne w sytuacjach, w których bezpieczeństwo jest priorytetowe, takich jak generowa­ nie kluczy szyfrowych lub haseł. Jeśli potrzebne są niedeterministyczne liczby losowe do użycia w aplikacjach kryptograficz­ nych lub w funkcjach zabezpieczeń, należy użyć generatora liczb losowych wyprowadzonego

System.Security. Cryptography.Random-NumberGenerator. Klasa ta jest klasą abstrakcyjną (abstract), z której powinny dziedziczyć wszystkie konkretne klasy generatorów l iczb losowych .NET. Aktualnie klasa RNGCryptoServiceProvider j est jedyną konkretną implementacją, dołą­ czoną do systemu .NET. Klasa RNGCryptoServiceProvider dostarcza zarządzanego opakowania dla funkcji CryptGenRandom należącej do Win32 CryptoAPI i można ją wykorzystać do wypeł­ niania tablic bajtowych (byte) wartościami kryptograficznie losowymi. z klasy

Ważne Liczby tworzone przez klasę RNGCryptoServiceProvider nie są naprawdę losowe. Są jednak wystarczająco losowe, aby sprostać wymaganiom aplikacji kryptogra­ ficznych i zabezpieczeń w większości środowisk komercyjnych lub rządowych.

Podobnie jak w przypadku wielu innych klas kryptograficznych .NET, podstawowa klasa Ran­ domNumberGenerator jest klasą fabrykującą dla konkretnych klas implementacyjnych. Skład­ nia RandomNumberGenerator. Create 0 ) ;

430

C#

-

Księga przykładów

li li

Zamknięcie zasobów St ream i wyczyszczenie taj nych danych z obiektów , do któ rych za chwilę zostanie ut racone odnies ienie .

destFile . Fl u s h ( ) ; A r ray . Clea r ( key , 0 , key . Length ) ; A r ray . Clea r ( iv , 0 , iv . Length ) ; c ryptoSt ream . Cl ea r ( ) ; c ryptoSt ream . Close ( ) ; s rcFil e . Close ( ) ; destFile . Close ( ) ; } }

li li

Metoda roz szyf rowania pliku zaszyf rowanego algo rytmem T riple DES p rzy użyciu dosta rczonego klucza i iv .

p rivate static void Dec ryptFile ( s t ring s rcFileName , st ring destFileName , byte [ ] key , byte [ ] i v ) {

li

Two rzenie st rumieni w celu dostępu do ź ródłowego i wynikowego pliku .

St ream s rc File = new FileSt ream ( s rcFileName , FileMode . Open , FileAcces s . Read ) ; St ream dest File = new FileSt ream ( dest FileName , FileMode . C reat e , FileAcces s . Write ) ;

li

Two rzenie nowego algo rytmu T riple DES do roz szyf rowania pliku .

u s ing ( Symmet ricAlgo rithm alg = Symmet ricAlgo rithm . C reate ( " 3DES " ) ) {

li

Kon figu rowanie właściwości Key i I V algo rytmu syme t ryczneg o .

alg . Key = key ; alg . IV = iv ;

li li li li li

Two rzenie C ryptoSt ream do roz szyf rowan ia z awa rtości ź ródłowego st rumienia w mia rę j ego zapisywania . Wywołanie metody C reateDec ryptor klasy Symmet ricAlgo rithm w celu zwrócenia roz szyf rowanej instancj i IC ryptoT rans f o rm i p rzekazanie j ej do C ryptoSt ream .

C ryptoSt ream c ryptoSt ream = new C ryptoSt ream ( dest File , alg . C reateDec rypto r ( ) , C ryptoSt reamMode . W rite ) ;

li li

Dekla rowanie bufo ra używanego do odczytu danych z pliku ź ródłowego i zapisywanie roz szyf rowanego pl iku .

int buffe rlengt h ; byte [ ] buffer = new byte [ 1024 ] ;

li li

Odczytanie pliku zaszyf rowanego w blokach po 1024 baj t y i zapisanie roz s z y f rowanej we rs j i w p l i k u docelowym .

do { buffe rlength = s rc File . Read ( buffe r , 0 , 1024 ) ; c ryptoSt ream . Write ( bu f fe r , 0 , buffe rlengt h ) ;

Rozdział

1 4:

Kryptografia

431

} while ( bu f fe rlength > 0 ) ;

li Zamknięcie zasobów St ream i wyczyszczen ie taj nych danych li z obiektów, do któ rych za chwilę zostanie ut racone odniesien i e . c ryptoSt ream . Fl u s h FinalBloc k ( ) ; A r ray . Clea r ( key , 0 , key . Length ) ; A r ray . Cl ea r ( iv , 0 , iv . Length ) ; c ryptoSt ream . Clea r ( ) ; c ryptoSt ream . Close ( ) ; s rcFile . Close ( ) ; destFile . Close ( ) ; } } }

1 4. 7 Wyprowadzenie klucza szyfrowania symetrycznego z hasła Problem Chcesz wygenerować klucz szyfrowania symetrycznego z hasła, aby użytkownicy mogli łatwo pamiętać swoje klucze i nie musieli ich przechowywać w pamięci komputera.

Rozwiązanie Użyj klasy System.Security. Cryptography.PasswordDeriveBytes, aby utworzyć klucz symetrycznego szyfrowania z łańcucha.

Omówienie Dla większości ludzi zapamiętanie treści klucza symetrycznego szyfrowania jest niewykonalne i nie należy się spodziewać, że będą oni w stanie ręcznie wprowadzać tak długie liczby. Z tego względu klucze te muszą być przechowywane w bezpiecznej formie, dostępne dla aplikacji. Zazwyczaj oznacza to zapamiętanie kluczy na karcie inteligentnej, dyskietce, w bazie danych lub jako plik. Problemy związane z przypisywaniem, dystrybucją, dostępem i przechowywaniem kluczy stanowią jeden z najtrudniejszych aspektów implementacji rozwiązań kryptograficznych ­ zagadnienie to określane jest zbiorczą nazwą zarzqdzaniem klttczami (key management). Gdy zachodzi potrzeba przechowania tajnych informacji, należy nie tylko rozważyć zabezpieczenie danych, ale również ochronę sekretów użytych do tego celu! Alternatywą dla zapamiętywania klucza jest przypisanie użytkownikowi możliwie łatwo zapa­ miętywanego hasła (lub frazy) i użycie protokołu wyprowadzenia klucza do utworzenia klucza symetrycznego z tego hasła. Wtedy, jeżeli tylko użytkownik chce zaszyfrować lub odszyfrować dane, po prostu podaje hasło, a komputer generuje prawidłowy klucz. Jeśli tylko użytkownik poda to samo hasło, protokoł wyprowadzenia klucza będzie generował ten sam klucz.

432

C#

-

Księga przykładów

Ważne Wyprowadzenie kluczy z zapamiętywanych słów i fraz w znaczącym stopniu redu­ kuje losowość kluczy, a to z kolei zmniejsza bezpieczeństwo zapewniane przez funkcje kryptograficzne używające tych kluczy. Jest to cena, którą trzeba zapłacić za uproszczenie. Można sobie wyobrazić sytuację, że hacker odgadnie źle wybrane hasło - wtedy to on oszczędzi czas i wysiłek, który musiałby poświęcić na kryptoanalizę i łamanie szyfrów.

Biblioreka klas .NET Framework zawiera implementację wyprowadzenia klucza symerrycz­ nego: PasswordDeriveBytes. Klasa ta używa wielokrotnie algorytmu haszowania wobec hasła, aby wygnerować klucz o wymaganej długości. Przy konfigurowaniu obiektu PasswordDerive­ Bytes można określić, które z algorytmów haszujących .NET mają być użyte, podobnie jak liczbę powtórzeń - wartości domyślne to algorytm SHA-1 i 1 00 powtórzeń. Ponadto należy dostarczyć tzw. wartość salt. Salt jest losową daną, której używa proces wyprowadzenia klucza do utworzenia wersji bardziej odpornej na pewne typy ataku kryptograficznego. Wartości salt nie trzeba utajniać; przeciwnie, należy ją zapamiętać i stosować w przyszłości przy wyprowadza­ niu klucza z hasła. Bez właściwej wartości salt nie będzie możliwe wyprowadzenie prawidłowego klucza, a co za tym idzie - odszyfrowanie zaszyfrowanych danych.

Uwaga Nie można utworzyć asymetrycznych kluczy szyfrowania przy użyciu protokołu wyprowadzenia. Asymetryczne algorytmy szyfrowania opierają się na specyficznych zależnościach matematycznych między prywatną i publiczną składową pary kluczy. Tak więc każdy algorytm asymetrycznego szyfrowania wymaga posługiwania się odpowied­ nią procedurą generowania nowych kluczy.

Poniższy przykład demonstruje użycie klasy PasswordDeriveBytes do wygenerowania 64-bitowego symetrycznego klucza z hasła. Przykład ten wymaga dwóch argumentów wiersza poleceń. Pierw­ szy jest nazwą algorytmu haszującego, używanego do generowaniu klucza, a drugi jest hasłem używanym jako dane wejściowe do wyprowadzenia klucza. Obsługiwane nazwy algorytmów haszowania zawiera tabela 1 4- 1 . us ing System ; us ing System . Secu rity . C ryptog raph y ; public class DerivedKeyExample { public static void Main ( st ring [ ] a rg s ) { li Wyko rzystanie k ryptog raficznego generatora l i c z b losowych do li utwo rzenia wa rtości salt , używanej w algorytmie . byte [ ) salt = new byte [ 8 ) ; RandomNumberGenerator . C reate { ) . GetBytes { salt ) ;

li Two rzenie obiektu Pas swordDeriveBytes do wygene rowania klucza li z hasła . Należy podać f ródłowe hasło, któ re j es t d rugim a rgumentem li wie rsza polecenia , o ra z wa rtość salt . Pas swo rdDeriveBytes pdb =

Rozdział

1 4:

Kryptografia

433

new Pas swordDeriveBytes ( a rg s [ l ] , salt ) ;

li Wybór algo rytmu haszuj ącego , używanego do wygene rowania klucz a , na li pod stawie nazwy podanej w pierwszym a rgumencie wie rsza polecenia . li Domyślnie używany j est algorytm SHA- 1 . pdb . HashName = a rg s ( 9 J ;

li O k reślenie liczby iteracj i na 209 . Wa rtość ta steruje liczbą li zastosowan ia algo rytmu haszuj ącego wobec hasła w celu wygene rowania li klucza . Domyślna wa rtość wynosi 199 . pdb . It e rationCount = 299 ;

li Wygene rowanie 8 - baj towego ( 64 b it y ) klucza z hasł a . Siła klucza j est li og ran iczona p rzez długość kodu hash - 169 bitów w p rzypadku SHA- 1 . byte [ J key = pdb . GetBytes ( S ) ;

li Wyświetlenie klucza i salt na kon sol i . Console . W riteline ( " Key

= { 9} " , BitConve rte r . ToSt ring ( key ) ) ;

Cons ole . Writeline ( " Salt = { 9} " , BitConve rte r . ToSt ring ( salt ) ) ; } }

Polecenie DerivedKeyExample SHAI SOmeVereeStr@ngeP@$$wOrd używa algorytmu haszowania SHAl do wyprowadzenia 8-bajtowego (64-bitowego) klucza na bazie łańcucha „SOmeVereeStr@ngeP@$$wOrd". Uzyskany wynik będzie podobny do poniższego: Key = 53 - 72 - 74 - 5 8 - A4 - 88 - A4 - 89 Salt = 79 - 82 - 79 - F4 - 3B - F9 - D F - D2

Należy zauważyć, że za każdym razem program DerivedKeyExampłe będzie tworzył inny klucz; jest to efekt oddziaływania wartości salt. Wykomentowanie kodu, tworzącego wartość losową dla salt (wytłus1.CZ0na czcionka w listingu kodu), a następnie zrekompilowanie i uruchomienie przykładu DerivedKeyExampłe spowoduje, że program zawsze wygeneruje ten sam klucz dla danego hasła.

1 4.8 Bezpieczne wysyłanie utajnionych danych przy użyciu szyfrowania asymetrycznego Problem Chcesz użyć asymetrycznego szyfrowania do przesłania utajnionej informacji.

Rozwiązanie Utwórz instancję klasy asymetrycznego algorytmu System.Security. Cryptography.RSACryptoSeroice­ Provitkr. Użyj metody RSACryptoSeroiceProvitkr.Encrypt oraz klucza publicznego spodziewanego odbiorcy, aby zaszyfrować komunikat. Później odbiorca będzie używał metody RSACryptoSeroice­ Provider.Decrypt i �wojego klucza prywatnego do rozszyfrowania zaszyfrowanej informacji.

434

C#

-

Księga przykładów

Omówienie System .NET Framework definiuje hierarchię klas algorytmów asymetrycznych, podobnie do zdefiniowanych wcześniej algorytmów symetrycznych (omówionych w przepisie 1 4 .6). Wszystkie algorytmy asymetryczne muszą rozszerzać podstawową klasę bazową abstracto nazwie System.Security. Cryptography.AsymmetricAlgorithm. A oto dwie konkretne implementacje algo­ rytmu asymetrycznego: • •

System.Security. Cryptography. RSACryptoServiceProvider System.Security. Cryptography.DSACryptoServiceProvider

Jak wynika z sufiksa nazwy klasy CryptoServiceProvider, obydwie te klasy opakowują funk­ cjonalność dostarczoną przez Win32 CryproAPI . Jednakże ryłko klasa RSACryptoServicePro­ vider umożliwia szyfrowanie danych. Klasa DSACryptoServiceProvider implementuje Digital Signature Algorirhm (OSA), który może być użyry tylko do utworzenia podpisów cyfrowych (informacje przybliżające algorytm OSA można znaleźć w dokumencie Federal Information Processing Standard [FIPS] 1 86-2, pod adresem http://www. itl. nist.gov/fipspubsf). Chociaż można utworzyć instancję dla obiektu algorytmu asymetrycznego przy użyciu starycz­ nej metody fabrykującej Create klasy AsymmetricAlgorithm, rozwiązanie to nie jest zbyt wartościowe. Klasa AsymmetricAlgorithm nie deklaruje metod używanych przez klasę RSACryptoServiceProvider do szyfrowania i odszyfrowywania danych. Zamiast tego należy utworzyć instancję dla klasy RSACryptoServiceProvider bezpośrednio, przy użyciu jednego z jej konstruktorów. Zanim będzie można zaszyfrować łub odszyfrować dane za pomocą obiektu RSACryptoSer­ viceProvider, należy uzyskać dostęp do odpowiednich kluczy. Klucze algorytmu asymetrycz­ nego bardzo się różnią od stosowanych dla algorytmów symetrycznych. Po pierwsze, składają się z dwóch komponentów: klucza publicznego i klucza prywatnego. Oba te klucze łącznie są nazywane parq kluczy (key pair). Po drugie, nie stanowią one szeregu losowo wygenerowa­ nych bajtów, ponieważ muszą być utworzone zgodnie ze specjalną formułą. Pomiędzy klu­ czem publicznym i kluczem prywatnym istnieje określony związek matemaryczny; zależność ca umożliwia algorytmowi asymetrycznemu szyfrowanie danych za pomocą jednego klucza i ich rozszyfrowanie rylko przy użyciu drugiego klucza. Każdy algorytm asymetryczny używa własnej formuły generowania kluczy, a konkretne klasy implementacji zawierają funkcjonalność niezbędną do generowania nowych par w odpowiedni sposób. Jak sugeruje nazwa, klucz publiczny nie jest tajny i właściciel może go jawnie przesyłać jako wiadomość e-mail albo umieścić na stronie Web łub w serwerze dystrybucji kluczy i poka­ zać całemu światu. Ludzie, którzy chcą przesłać tajne informacje, używają klucza publicznego docelowego odbiorcy do jej szyfrowania. Adresat używa następnie swojego klucza prywatnego do rozszyfrowania tej informacji. Klucz prywatny musi pozostać w tajemnicy; każdy kto posią­ dzie klucz prywatny, może odszyfrować dane, zaszyfrowane przy użyciu odpowiadającego mu klucza publicznego. Aby utworzyć zaszyfrowaną asymetrycznie tajną informację, należy mieć klucz publiczny zamierzonego odbiorcy i załadować go do obiektu RSACryptoServiceProvider. Oto dwa sposoby ładowania publicznego klucza: •

Użycie metody RSACryptoServiceProvider.ImportParameters do zaimportowania struk­ tury System. Security. Cryptography.RSAParameters, która zawiera klucz publiczny odbiorcy. Właściciel klucza będzie zazwyczaj generował strukturę RSAParameters przy użyciu metody

Rozdział

RSACryptoServiceProvider.ExportParameters

1 4:

Kryptografia

435

i prześle ją do nadawcy. Może także przesłać

bajtową reprezentację klucza publicznego, którą trzeba będzie załadować ręcznie do struk­

RSAParameters. Użycie metody RSACryptoServiceProvider.FromXmlString tury



do załadowania danych klucza

publicznego z ciągu XML, kodującego klucz publiczny. Właściciel zazwyczaj wygeneruje dane XML klucza przy użyciu metody

RSACryptoServiceProvider. ToXmlString

i prześle

je do nadawcy.

Ważne Obie metody, ExportParameters i ToXm/String klasy RSACryptoServiceProvi­ der, pobierają pojedynczy argument boolowski, który - jeżeli ma wartość true .spo­ woduje, że obiekt RSACryptoServiceProvider wyeksportuje obydwa klucze: publiczny i prywatny. Dlatego należy pamiętać, aby przy eksportowaniu kluczy w celu dystrybucji lub zapamiętania wyspecyfikować wartość tego argumentu jako fa/se. -

Po załadowaniu klucza publicznego odbiorcy do obiektu

RSACryptoServiceProvider można przy­

stąpić do szyfrowania danych. Algorytmy asymetryczne działają znacznie wolniej, niż algorytmy symetryczne przy szyfrowaniu i rozszyfrowaniu danych. Z tego powodu rzadko używa się algo­ rytmu asymetrycznego do szyfrowania dużej ilości danych. Zazwyczaj przy szyfrowaniu dużej ilości danych korzysta się z algorytmu symetrycznego, a następnie stosuje się szyfrowanie symetrycznych kluczy przy użyciu algorytmu asymetrycznego tak, aby można było bezpiecznie przesłać syme­ tryczne klucze razem z danymi. Przepis 1 4 . 1 O zawiera kompletne omówienie tego scenariusza.

RSACryptoServiceProvider nie udostępnia modelu szyfro­ System.10.Smam, podobnego do użytego w przepisie 1 4.6. Aby szyfrować dane za pomocą obiektu RSACryptoServiceProvider, należy wywołać metodę Encrypt, przekazując jej tablicę bajtową zawierającą czysry tekst, który ma być zaszyfrowany; metoda zwróci tablicę bajtową zawierającą zaszyfrowany tekst. Metoda Encrypt pobierze rów­ nież drugi argument boolowski, określający ryp dopełnienia (padding), używanego przez obiekt RSACryptoServiceProvider. Dopełnienie określa sposób przetworzenia czystego tekstu przez

Zgodnie z cą zasadą użytkowania, klasa wania/ rozszyfrowywania opartego o

obiekt algorytmu asymetrycznego przed zaszyfrowaniem. Zapewnia ono, że algorytm nie będzie musiał przetwarzać niepełnych bloków danych, a jednocześnie pełni funkcję dodatkowego zabezpieezenia przed pewnymi formami ataku kryptograficznego. Opis dostępnych schema­ tów dopełniania wykracza poza tematykę tej książki. Generalnie

rzecz

biorąc, przy korzysta­

niu z systemu operacyjnego M icrosoft Windows XP łub w wersji późniejszych, dla argumentu wypełnienia należy podać true; w innych przypadkach trzeba użyć false - w przeciwnym razie

Encrypt wywoła wyjątek System. Security. Cryptography. CryptographicException. Rozszyfrowanie komunikatu jest równie proste, jak szyfrowanie. Odbiorca tworzy obiekt

RSACryptoServiceProvider

i ładuje do niego klucz prywatny. Zazwyczaj klucz jest przecho­ wywany w kontenerze kluczy zarządzanym przez CryptoAPI (patrz przepis 1 4.9). Odbiorca wywołuje metodę

RSACryptoServiceProvider.Decrypt i

przekazuje do niej otrzymany zaszyfro­

wany tekst. Ponownie trzeba określić mechanizm wypełnienia - ten sam który został użyty przy szyfrowaniu danych. Metoda Decrypt zwraca tablicę bajtową, zawierającą rozszyfrowany czysty tekst. Jeżeli reprezentuje on łańcuch, odbiorca musi dokonać konwersji tablicy bajtowej do postaci łańcucha przy użyciu klasy System. Text. Encoding.

436

C#

-

Księga przykładów

Klasa RSACryptoServiceProvider dziedziczy metody EncryptValue i Decrypt­ Value po swojej klasie nadrzędnej System.Security. Cryptography. RSA. Klasa RSA­ CryptoServiceProvider nie implementuje tych metod i wygeneruje wyjątek System. NotSupportedException przy ich wywołaniu. Uwaga

Przedstawiony niżej przykład klasy AsymmetricEncryptionExample ilustruje użycie klasy RSA­ CryptoServiceProvider do zaszyfrowania łańcucha, a następnie jego odszyfrowania. Metoda główna (Main) przykładu (tutaj nie pokazana, ale dostępna w dołączonym do książki kodzie przykładowym) pobiera wiadomość do zaszyfrowania jako argument wiersza poleceń. Generuje ona parę kluczy i zapamiętuje je w zarządzanym przez CryptoAPI kontenerze kluczy MyKeys; klucz publiczny zostaje wyeksportowany do obiektu RSAParameters. Te czynności wstały dołą­ cwne tylko na potrzeby przykładu - w rzeczywistości nadawca miałby tylko klucz publiczny odbiorcy, podczas gdy odbiorca utrzymywałby klucz prywatny w tajemnicy. Metoda główna wywołuje następnie metodę EncryptMessage, przekazując do niej bajtową reprezentację łańcucha wiadomości oraz obiekt RSAParameters, zawierający klucz publiczny odbiorcy. Metoda EncryptMessage zwraca tablicę bajtową, zawierającą zaszyfrowaną wer­ sję tekstu komunikatu, która ma być wysłana do zamierzonego odbiorcy. Następnie metoda główna wywołuje metodę DecryptMessage, przekazując jej zaszyfrowany komunikat i obiekt CspParameters, który zawiera odsyłacz do kontenera kluczy MyKeys; zawiera on prywatny klucz odbiorcy. W trakcie wykonania metoda główna wyświetla oryginalny komunikat, zaszyfrowany tekst i na koniec rozszyfrowany komunikat. u s ing System ; u s ing System . Text ; us ing System . Security . C ryptog raphy ; public class Asymmet ricEnc ryptionExample {

li Metoda główna pomin ięta , pat rz kod p rzykładowy li Metoda szyf rowania RSA komunikatu p rzy użyciu klucza publicznego li zawartego w st rukt u rze RSAPa ramet e rs . p rivate static byte [ ] Enc ryptMessage ( byte [ ] plaintext , RSAParamete rs rsaPa rams ) { byte [ ] ciphertext = null ;

li Two rzenie instancj i algo rytmu RSA . using ( RSAC ryptoSe rviceProvider rsaAlg new RSAC ryptoSe rviceProvide r ( ) ) { rsaAlg . Impo rtPa ramete rs ( rsaPa rams ) ;

li Szyf rowanie czystego tekstu p rzy użyciu dopełnienia OAEP , któ re li j est wspie rane tylko w Windows XP i później szych we rs j ach . ciphe rtext = rsaAlg . En c rypt ( plaintext , t rue ) ; }

li Czyszczen ie wa rtośc i czystego tekstu , p rzechowywanego w tablicy

Rozdział 1 4: Kryptografia 437 li baj towej . Pozwala to zagwarantowat , że t a j n e dane nie będą nadal li p rzechowywane w pamięci po zwolnieniu odnośników do nich . A r ray . Clea r ( plaintext , 0 , plaintext . Lengt h ) ; ret u rn ciphe rtext ; } li Metoda roz s z y f rowywania wiadomości z a s z y f rowanej za pomocą RSA li p rzy użyciu klucza p rywatnego z kontenera klucz y , wskazanego p rzez li obiekt C spPa rameters . p rivate static byte [ J Dec ryptMes sage ( byte [ ] ciphertext , C spPa ramete rs cspPa rams ) { li Dekla racj a tablicy baj towej do p rzechowania roz szyf rowanego tekstu . byte [ J plaintext

=

null ;

li Two rzenie in stancj i algo rytmu RSA . using ( RSAC ryptoSe rviceProvide r rsaAlg new RSAC ryptoSe rviceP rovide r ( cs pPa rams ) ) { li Roz s z yf rowanie tekstu p rzy użyciu dopełnienia . plaintext = rsaAlg . Dec rypt ( c iphe rtext , t rue ) ; } retu rn plaintext ; } }

Wykonanie polecenia AsymmetricEncryptionExample "Meet me under the clock tower at noon." spowoduje powstanie danych wyjściowych pokazanych niżej. O riginal mes sage Meet me unde r the clock towe r at noon . Formatted C iphe rtext = 78 - 16 - 4C - 17 - 20 - 1C - F6 - 94 - 95 - 4A - FE - BE - 2A - C F - 6A - 8B - 2 C ­ =

D2 - 16 - E6 - BB - 55 - F0 - D E - E l - 93 - F6 - 3 1 - A4 - 05 - AA - 33 - 29 - 33 - D9 - 6D - 43 - D 2 - 1 E - D0 - 1 0 - 4 5 AF - 34 - 7 C - B6 - FB - 18 - ED - D l - C F - B2 - 3 0 - 4E - 43 - 85 - 3C - 65 - A5 - 57 - B3 - A2 - 2E - 1 9 - 95 - 2A - 0 F 1 1 - 98 - 7 1 - F7 - 1 B- 57 - B3 - BB - 5 E - E3 - 05 - A8 - 6 1 - A7 - FA - 99 - C6 - 4A - B5 - E2 - 90 - B l - B6 - 70 - 646 F - EA - 45 - 69 - 40 - 2B - 16 - 27 - DC - 6A - 2E - 26 - El - 90 - F7 - B8 - 93 - 2A - 87 - 3D - 3C - 7F - 7A - D F - C5 A0 - 7E - B9 - 9 F - 4 1 - 60 - 95 - A0 - 2 1 - 93 - 11 Oec rypted message

=

Meet me unde r the clock towe r at noon .

Uwaga Należy zauważyć, że przy wielokrotnym wykonywaniu przykładu z tym samym komunikatem oraz kluczami jako danymi wejściowymi, otrzymany zaszyfrowany tekst będzie za każdym razem inny. Jest to zachowanie oczekiwane, chociaż początkowo mylące. Mechanizm dopełnienia, używany przez klasę RSACryptoServiceProvider, wpro­ wadza losowe dane, aby pokrzyżować niektóre rodzaje ataków kryptograficznych.

438

C#

-

Księga przykładów

1 4.9 Bezpieczne zapamiętywanie klucza szyfrowania asymetrycznego Problem Chcesz zapamiętać asymetryczną parę kluczy w bezpiecznym miejscu, łatwo dostępnym z two­ jej aplikacj i.

Rozwiązanie Należy się oprzeć na funkcjonalności udostępnianej przez klasy algorytmów asymetrycznych

System.Security. Cryptogmphy.RSACryptoServiceProvider CryptoServiceProvider.

oraz

System.Security. Cryptogmphy.DSA­

Omówienie Obie konkretne klasy algorytmów asymetrycznych

ServiceProvider

-

-

RSACryptoServiceProvider i DSACrypto­

opakowują funkcjonalność implementowaną przez systemowego dostawcę

usług kryptograficznych (CSP), który jest składnikiem Win32 CryptoAPI. Oprócz podstawo­ wych usług kryptograficznych, takich jak szyfrowanie, rozszyfrowywanie i podpisy cyfrowe, każdy dostawca CSP udostępnia mechanizm kontenera kluczy (key container). Kontenery kluczy są obszarami pamięci, przeznaczonymi do przechowywania kluczy kryp­ tograficznych, którymi zarządzają dostawcy CSP; do ochrony zawartości kontenerów stosowane jest silne szyfrowanie i zabezpieczenia systemu operacyjnego. Kontenery zapewniają aplikacjom łatwy dostęp do kluczy bez naruszania bezpieczeństwa. Przy wywołaniu funkcji kryptogra­ ficznych CSP, aplikacja określa nazwę zasobnika kluczy i CSP odwołuje się do kluczy, jeśli to konieczne. Ponieważ klucze nie są przekazywane przez CSP do aplikacji, aplikacja nie może przez przypadek naruszyć bezpieczeństwa tych kluczy. Klasy

RSACryptoServiceProvider

i

DSACryptoServiceProvider

umożliwiają skonfigurowanie

implementacji CSP w niższej warstwie przy użyciu instancji klasy System.Security. Cryptography. CspParameters. Aby skonfigurować obiekt RSACryptoServiceProvider lub DSACryptoServicePro­

vider w celu wykorzystania zasobnika kluczy,

należy wykonać następujące kroki:

1. Utworzyć nowy obiekt CspParameters. 2. Określić pole

public dla KeyContainerName obiektu CspParameters

na wartość reprezentu­

jącą nazwę zasobnika kluczy, który ma być użyty; łańcuch ten może zawierać spacje.

3. Utworzyć nowy obiekt RSACryptoServiceProvider lub DSACryptoServiceProvider i przekazać obiekt

CspParameters jako

argument konstruktora.

Jeżeli kontener kluczy o tej nazwie istnieje w zakresie zarządzanym przez CSP i zawiera odpowied­ nie klucze, CSP użyje ich przy wykonywaniu operacji kryptograficznych. Jeżeli kontener kluczy lub klucze nie istnieją, CSP automatycznie utworzy nowe klucze. Aby zmusić CSP do zapamięta­ nia nowo utworzonych kluczy w nazwanym zasobniku kluczy, należy ustawić wartość właściwo­ ści PersistKeylnCsp obiektu RSACryptoServiceProvider lub DSACryptoServiceProvider na true.

Rozdział 1 4: Kryptografia 439 Metoda LoadKeys, pokazana tutaj, stanowi fragment przykładowego pliku StoreAsymmetric­ KeyExample.cs, dostępnego jako przykładowy kod dla niniejszego rozdziału. Metoda Lo�s tworzy nowy obiekt RSACryptoServiceProvider i konfiguruje go do wykorzystania zasobnika kluczy o nazwie My.Keys. Poprzez określenie PersistKeylnCsp tnll, algorytm automatycznie zapamięta nowo wygenerowane klucze w nazwanym zasobniku kluczy. =

li li li li

Metoda two rzenia obiektu RSAC ryptoSe rviceP rovid e r i ładowania kluczy z nazwanego kontenera C ryptoAPI , j eżeli i s t n iej e ; w innym p rzypadku RSAC ryptoSe rviceP rovider automatyc z n ie gene ruj e nowe klucze i umieszcza j e w nazwanym kontenerze d o później s zego wyko rzystania .

public static void LoadKeys ( st ring containe r ) {

li Two rzenie nowego obiektu C spPa ramete rs i wpisanie do j ego pola li KeyContaine rName nazwy wskazanego kontenera . System . Secu rity . C ryptography . Cs pPa ramete rs c spPa rams new System . Secu rity . C ryptog raphy . CspPa ramete r s ( ) ; c spPa rams . KeyCon taine rName

=

containe r ;

l i Two rzenie nowego asymet ryc z nego algo rytmu RSA i p rzekazanie obiektu li C spPa rameters , zawie raj ącego s z czegóły kontenera kluczy . using ( System . Secu rity . C ryptog raphy . RSAC ryptoSe rviceProvider rsaAlg = new System . Securit y . C ryptog raph y . RSAC ryptoSe rviceProvide r ( cs pPa rams ) ) {

li Konf igu rowanie obiektu RSAC ryptoSe rviceP rovide r , by zachował klu cze li w kontene rze . rsaAl g . Persi stKeyinCsp

=

t ru e ;

l i Wyświet lenie kl u c z y publ i c z nych w konsol i . System . Consol e . W riteline ( rsaAlg . ToXmlSt ring ( fa l s e ) ) ;

li Dzięki s konfigu rowaniu obiektu RSAC ryptoSe rviceP rovider do li zachowania kluczy , zostaj ą one umies zczone we wskazanym kontenerze . } }

Klasy RSACryptoServiceProvitkr i DSACryptoServiceProvider nie udostępniają żadnej bezpośred­ niej metody usuwania zasobników kluczy. Aby usunąć istniejące klucze, należy ustawić wartość PersistKeylnCsp na false i wywołać metodę Clear lub metodę Dispose obiektu RSACryptoService­ Provitkr lub DSACryptoServiceProvider. Pokazana niżej metoda Delete.Keys ilustruje tę technikę:

li Metoda two rzenia obiektu RSAC ryptoSe rviceP rovide r i usuwania istniej ących li kluczy z nazwanego kontene ra C ryptoAPI . public static void DeleteKeys ( st ring containe r ) {

li Two rzenie nowego obiektu C spPa rameters i wpisanie do j ego pola li KeyContaine rName nazwy wskazanego kontene ra . System . Secu rity . C ryptog raphy . CspPa rameters c spPa rams = new System . Secu rity . C ryptog raphy . C spPa ramete r s ( ) ; c spPa rams . KeyCon taine rName

=

containe r ;

440

C#

-

Księga przykładów li Two rzenie nowego asymet ryc z nego algo rytmu RSA i p rzekazanie obiektu li C spPa ramete rs , zawie raj ącego s z c zegóły kon tene ra klucz y . u sing ( System . Secu rity . C ryptog raph y . RSAC ryptoSe rviceP rovide r rsaAlg = new System . Secu rity . C ryptog raph y . RSAC ryptoSe rviceP rovide r ( c spPa rams ) ) {

li Kon figu rowanie obiektu RSAC ryptoSe rviceP rovide r , by nie zachowywał li kluczy w kontene rze . rsaAlg . Pe rsistKeyinCsp = fal se ;

li Wyświet lenie kluczy publicznych w konsol i . Pon ieważ nastęnie li zostanie wywołane Dispose ( ) , n ie będzie widoczna zmiana istniej ących li kluczy do momentu ponownego wywołania metody . System . Con sole . WriteLine ( rsaAlg . ToXmlSt ring ( fa l s e ) ) ;

li li li li li li

W momencie , gdy kod opuszcza b lok inst rukcj i " u sing " , zostaj e wywołana metoda Dispose wobec obiektu RSAC ryptoSe rviceP rovide r . Obiekt został skonfigu rowa n y , aby NIE p rzechowywał kl uczy , tak więc powiązany z nim kontener kluczy zostanie wyczyszczony . Zamiast Dispose ( ) można wywołać rsaAlg . Clea r ( ) , co s powodu j e ten sam efekt , gdyż niej awnie wywołuj e metodę Dispose ( ) .

} }

Win32 CryptoAPI umożliwia zarówno przechowywanie kluczy użytkownika, jak i kluczy maszynowych. System operacyj ny Windows gwarantuje, że dostęp do kontenera użytkow­ nika ma tylko ten użytkownik, który go utworzył, ale maszynowy kontener z kluczami jest dostępny dla każdego użytkownika komputera. Domyślnie klasy RSACryptoServiceProvider i DSACryptoServiceProvider używają kontenerów kluczy użytkownika. Można wyspecyfikować użycie maszynowego kontenera kluczy przez ustawienie statycznej właściwości UseMachine­ KeyStore obiektów RSACryptoServiceProvider lub DSACryptoServiceProvider na true. Będzie miało to wpływ na każdy kod, wykonywany w bieżącej domenie aplikacji. Można także usta­ wić właściwość CspParameters.Flags na wartość System.Security. Cryptography. CspProviderF!ags. UseMachineKeyStore przed utworzeniem obiektu asymetrycznego szyfrowania.

Wymagania dotyczące bezpieczeństwa powinny być przemyślane dokład­ nie przed użyciem maszynowego kontenera kluczy. Fakt, że każdy z użytkowników, który ma dostęp do komputera, ma jednocześnie dostęp do kluczy zawartych w kontenerze, przekreśla większość zalet asymetrycznego szyfrowania.

Ostrzeżenie

Rozdział 1 4: Kryptografia

441

1 4.1 O Bezpieczna wymiana symetrycznego klucza sesji Problem Chcesz wymienić symetrycznie szyfrowane dane z kimś innym metody dostarczenia symetrycznego klucza sesji wraz z danymi.

potrzebujesz bezpiecznej

Rozwiązanie Użyj mechanizmu wymiany kluczy, zaimplementowanego przez klasę System.Security. Crypto­ graphy.RSACryptoServiceProvider. Będzie on asymetrycznie szyfrował symetryczny klucz przy użyciu klucza publicznego zamierzonego odbiorcy. Możesz następnie przesłać zaszyfrowany symetryczny klucz razem z zaszyfrowanymi danymi do odbiorcy. Odbiorca musi rozszyfrować otrzymany klucz symetryczny przy użyciu swojego klucza prywatnego, a następnie kontynuo­ wać rozszyfrowywanie danych.

Omówienie Przy symetrycznym szyfrowaniu danych w celu transmisji, należy za każdym razem wygenerować nowy klucz, znany inaczej jako klucz sesji (session key). Użycie kluczy sesji ma dwie główne zalety: •

Jeżeli nieupoważniona osoba uzyska dostęp do wielu bloków tekstu zaszyfrowanego przy użyciu tego samego symetrycznego klucza, zwiększy to szansę odczytania danych przy uży­ ciu technik kryptoanalizy.



Jeżeli jakaś osoba postronna odkryje klucz sesji, da jej to dostęp tylko do pojedynczego zestawu zaszyfrowanych danych, a nic do wszelkich informacji przekazywanych wcześniej lub później.

Główny problem związany z korzystaniem z kluczy sesji stanowi ich dystrybucja i bezpieczeń­ stwo. Jeżeli jest używany zawsze ten sam klucz, spotkanie obu stron wymieniających informacje nie stanowi wielkiego problemu; można również skorzystać z zaufanego kuriera, który umoż­ liwi fizyczną wymianę kluczy. Jednakże przy generowaniu nowych kluczy dla każdej wymiany danych fizyczne spotkania nie wchodzą w grę. Jednym z rozwiązań może być uzgodnienie zawczasu wielkiej liczby kluczy sesji z odbiorcami, jednak podejście takie jest trudne do admi­ nistrowania, a sam fakt, że klucze są gdzieś przechowywane, zwiększa ryzyko ich ujawnienia. Lepszą metodą jest przesłanie klucza sesji w silnie zaszyfrowanej postaci razem z danymi, które zostały zaszyfrowane tym kluczem - ten proces jest znany jako wymiana klucza (key exchange). Wymiana klucza pociąga za sobą użycie szyfrowania asymetrycznego do zaszyfrowania syme­ trycznego klucza sesji. Chcąc przesłać do kogoś dane, należy wygenerować symetryczny klucz sesji, zaszyfrować dane, a następnie zaszyfrować klucz sesji przy użyciu klucza publicznego odbiorcy. Kiedy zamierzony odbiorca otrzyma dane, będzie mógł rozszyfrować klucz sesji przy użyciu swo­ jego klucza prywatnego, a następnie rozszyfrować dane. Co ważne, wymiana klucza umożliwia wymianę dużej ilości zaszyfrowanych danych z każdym, nawet z ludźmi, z którymi nigdy nie komunikowaliśmy się wcześniej, o ile mamy dostęp do ich asymetrycznych kluczy publicznych.

442

C#

-

Księga przykładów

Uwaga Idealne rozwiązanie stanowiłoby użycie algorytmy asymetrycznego do szy­ frowania wszystkich danych, co pozwoliłoby uniknąć wymiany kluczy symetrycznych. Jednakże mała prędkość działania algorytmów asymetrycznych przy szyfrowaniu i odszy­ frowaniu danych sprawia, że ich użycie do dużych ilości danych jest niepraktyczne. Uży­ cie algorytmów asymetrycznych do szyfrowania symetrycznych kluczy sesji jest lepszym rozwiązaniem i chociaż bardziej złożonym, oferuje znaczne korzyści: zarówno większą elastyczność, jak i prędkość.

Biblioteka klas .NET Framework zawiera wsparcie dla wymiany kluczy przy użyciu algorytmu RSA, ale w tym wypadku należy wybrać między dwoma schematami formatowania: Optima! Asymmetric Encryption Padding (OAEP) i PKCS # 1

v

1 .5. Dokładne omówienie tych sche­

matów wykracza poza zakres niniejszej książki. W ogólności zaleca się stosowanie formatowania OAEP, chyba że ismieje potrzeba komunikacji

ze

starszymi systemami, które posługują się for­

matowaniem PKCS. Niżej podane klasy implementują mechanizm wymiany kluczy - po jednej dla każdego schematu formatowania: • •

System.Security. Cryptography. RSAOAEPKeyExchangeFormatter System. Security. Cryptography. RSAPKCS I KeyExchangeFormatter

Aby przygotować symetryczny klucz do wymiany, należy utworzyć obiekt formatujący żąda­ nego typu, a następnie przypisać obiekt asymetrycznego algorytmu

(RSACryptoServiceProvider)

do formatera poprzez wywołanie metody SetKey . Należy skonfigurować algorytm asymetryczny, aby użyć publicznego klucza odbiorcy. Po skonfigurowaniu należy wywołać metodę CreateKey­ Exchange i przekazać tablicę bajcową, zawierającą symetryczny klucz sesji, który ma być forma­ cowany. Metoda CreateKeyExchange zwraca tablicę bajtową, zawierającą dane wymiany klucza, które mają być wysłane do spodziewanego odbiorcy. Deformatowanie wymienianego klucza jest odwróceniem procesu formatowania. Oto dwie klasy deformatowania - po jednej dla każdego schematu formatowania. • •

System. Security. Cryptography. RSAOAEPKeyExchangeDeformatter System.Security. Cryptography.RSAPKCSJ KeyExchangeDeformatter

Aby zdeformatować formatowany klucz sesji, należy utworzyć obiekt deformatera odpowied­ niego typu i wywołać jego mecodę

Set.Key,

aby przypisać obiekt asymetrycznego algorytmu.

Do asymetrycznego algorytmu należy załadować swój klucz prywatny. Na koniec należy wywo­ łać metodę DecryptKeyExchange, przekazując dane wymiany klucza. Metoda DecryptKeyExchange zwraca tablicę bajtową, zawierającą symetryczny klucz sesji. Plik KeyExchangeExample.c.� demonstruje wymianę klucza. Metoda główna

(Main) przykładu

(nie pokazana tutaj, ale dostępna w kodzie dołączonym do książki) symuluje tworn:nie, formato­ wanie, wymianę i deformatowanie symetrycznego klucza sesji. Generuje ona parę asymetrycznych kluczy do wykorzystania w tym przykładzie. W sytuacji wymiany rzeczywistego klucza, nadawca, który utworzył klucz symetryczny, powinien posiadać tylko klucz publiczny odbiorcy, natomiast odbiorca posiada utajniony klucz prywatny.

Rozdział 1 4: Kryptografia

443

Uwaga Długość klucza symetrycznego użytego do utajniania chronionych danych jest ważna, ale równie istotne jest, aby klucz asymetryczny użyty do zaszyfrowania klucza sesji miał długość równą co najmniej długości klucza symetrycznego. Jeżeli klucz asymetryczny jest słabszy od klucza symetrycznego, hacker z całą pewnością zaatakuje szyfr asyme­ tryczny i w ten sposób otrzyma klucz symetryczny, co umożliwi mu rozszyfrowanie danych. Więcej informacji na temat równoważnych długości odpowiednich kluczy można znaleźć na stronie http://ietf. orglintemet-draftsldraft-orman-public-key-lengths-05. txt.

Metoda główna wywołuje przykładową metodę Format.KeyExchange, przekazując tablicę bajtową zawierającą klucz symetryczny oraz obiekt RSAPammetm, zawierający klucz publiczny odbiorcy. Metoda Format.KeyExchange zwraca tablicę bajtową z zaszyfrowanym i sformatowanym kluczem symetrycznym, gotowym do wysłania do zamierzonego odbiorcy. Następnie metoda główna wywołuje metodę Deformat.KeyExchange, przekazując sformatowane dane klucza i obiekt CspPa­ mmetm z odniesieniem do zasobnika kluczy My.Keys, który zawiera prywatny klucz odbiorcy. Pod­ czas wykonania wyświetlany jest oryginalny klucz sesji, sformatowane dane wymiany, a na koniec zdeformatowany klucz sesji. using System ; using System . Text ; using System . Secu rit y . C ryptog raph y ; public c l a s s KeyExchangeExample {

li Metoda główna pominięta , pat rz kod p rzykładowy dla tego rozd ział u . li li li li li

Metoda szyf rowania symet rycznego klucza sesj i i fo rmatowania go w celu wymiany kluczy . W celu zaszyf rowania klucza sesj i konieczny j est dostęp do klucza publicznego wyb ranego odbio rcy , któ ry zostanie p rzekazany do metody zawartej w st ruktu rze RSAParamete rs . Klucz ten mógł zostać p rzesłany p rzez odbiorcę lub pob rany z usługi dyst rybucj i klu czy .

p rivate static byte [ J Fo rmatKeyExchange ( byte [ J ses sionKey , RSAPa ramete r s rsaPa rams ) {

li Two rzenie asymet rycznego algorytmu RSA . using ( RSAC ryptoS e rviceProvid e r asymAlg = new RSAC ryptoSe rviceP rovide r ( ) ) {

li Impo rtowanie klucza publicznego odbio rcy . asymAlg . Impo rtPa ramete rs ( rsaPa rams ) ;

li Two rzenie fo rmatera RSA OAEP w celu s fo rmatowania danych wymiany li kl u czy . RSAOAEPKeyExchangeFo rmat t e r f o rmatt e r =

new RSAOAEPKeyExchangeForrnatte r ( ) ;

li Określen ie RSA j ako algo rytmu użytego do szyf rowania klucza 1 1 ses j i .

444

C#

-

Księga przykładów fo rmatte r . SetKey ( a symAlg ) ;

li Szyf rowanie i fo rmatowanie klucza ses j i . ret u rn fo rmat ter . C reateKeyExchange ( se s s ionKey ) ; } }

li li li li li li

Metoda roz s zyf rowywania danych wymiany klucza i wydobycia symet rycznego klucza sesj i . Do roz szyf rowania konieczny j est dostęp do klucza p rywatnego RSA , któ ry j est zawa rty w kontene rze klucz y , wskazywanym przez a rgument cs pPa rams . Rozwiązanie to pozwala uniknąć koniecz ności p rzet rzymywania taj nych danych w pamięci i p rzekazywania ich pomiędzy metodami .

p rivate static byte [ ] Defo rmatKeyExchang e ( byte [ ] exchangeData , C s pPa rameters c spPa rams ) {

li Two rzenie asymet rycz nego algo rytmu RSA . using ( RSAC ryptoSe rviceProvider asymAlg = new RSAC ryptoServiceP rovide r ( c s pPa rams ) ) {

li Two rzenie defo rmat e ra RSA OAEP w celu wydobycia klucza ses j i . RSAOAEPKeyExchangeDeformatte r defo rmatter = new RSAOAEPKeyExchangeDefo rmatt e r ( ) ;

li Określenie RSA j ako algo rytmu do roz s zyf rowania danych wymiany li kluczy . defo rmatt e r . SetKey ( a s ymAlg ) ;

li Roz szyf rowanie danych wymiany kluczy i zwrócenie klucza sesj i . ret u rn defo rmatte r . Dec ryptKeyExchange ( exchangeData ) ; } } }

Uruchomienie przykładu KqExchangeExampk wygeneruje na konsoli następujące dane: Session Key at Source = E E - SB - 16 - SB - AC - 46 - 3D - 72 - C C - 73 - 19 - D9 - 0B - 8A - 19 - E2 - A6 02 - 13 - BE - F8 - C E - D F - 40 Exchange Data = 60 - FA - 3 B - 63 - 4 1 - 2 5 - F l - AD - 08 - F9 - FC - 67 - CD - C6 - FB - 3 E - 0F - C3 - 62 C6 - 3 F - SC - C0 - 7 E - D l - 60 - 2D - 1 9 - 5 8 - 07 - EE - BB - 7 C - 5 3 - AS - C2 - FB - CA - D7 - 64 - F F - BA - 3 3 - 7 7 AC - 52 - 87 - S F -75 - E7 - 5 7 - 99 - 0 1 - 90 - C D - 7 0 - 36 - 1E - 5 3 - 0C - 8 2 - C6 - CE - B8 - BC - 8B - C9 - 39 - 6 F 29 - 39 - SF - 6C - A6 - 43 - ES - B0 - A l - 4 2 - 46 - 1C - 9B- 1 C - 72 - EB - S E - 67 - 06 - 44 - C0 - C E - AB - 7 9 - B8 39 - 8E - 9 F - 0 1 - E8 - 49 - 5 1 - 36 - D6 - 27 - 09 - 94 - DA - 42 - C E - 79 - C 2 - 72 - 88 - 4D - C E - 63 - B4 - A0 - AC 07- AF - 26 - A7 - 76 - DE - 2 1 - BE - AS Session Key at Dest inat ion = E E - 5 B - 1 6 - S B - AC - 46 -3D - 7 2 - C C - 73 - 19 - D9 - 9B - 8A - 19 E 2 - A6 - 02 - 13 - BE - F8 - CE - D F - 40

15 Współdziałanie z niezarządzanym kodem Microsoft .NET Frarnework jest bardzo ambitnym projektem. Rozwiązanie łączy w sobie nowy jezyk C#, zarządzany moduł runtime (CLR), platformę mieszczącą aplikacje Web (Microsoft ASP.NET) oraz rozszerzoną bibliotekę klas do budowy wszystkich typów aplikacji. Jednakże pomimo swojej ekspansywności, .NET Frarnework nie powiela wszystkich funkcji dostęp­ nych w niezarządzanym kodzie. W chwili obecnej system .NET Framework nie zawiera jeszcze wszystkich funkcji dostępnych w Win32 API, w związku z czym często stosuje się złożone rozwiązania własne zbudowane za pomocą języków opartych o mechanizm COM, takich jak Microsoft Visual Basic 6 lub Microsoft Visual C++ 6. Na szczęście Microsoft nie wymaga od przedsiębiorstw, by porzucały podstawowy, zbudo­ wany wcześniej kod przy migracji na platformę .NET. Zamiast tego .NET Frarnework oferuje funkcje współdziałania, które umożliwiają użycie starszego kodu w aplikacjach .NET Frarne­ work, a nawet dostęp do asemblacji .NET, jakby były one komponentami COM. Przepisy w niniejszym rozdziale dotyczą następujących problemów: • Wywoływanie funkcji udostępnianych przez niezarządzane biblioteki DLL (przepisy 1 5. 1 do 1 5.5). • Użycie komponentów COM w aplikacji .NET Framework (przepisy 1 5.6 do 1 5 .8). • Użycie kontrolek ActiveX w aplikacji .NET Framework (przepis 1 5.9). • Udostępnianie funkcjonalności asemblacji .NET jako komponentu COM (przepis 1 5. 1 0) .

446

C#

-

Księga przykładów

1 5.1 Wywołanie funkcji z niezarządzanej biblioteki DLL Problem Chcesz wywołać funkcję języka C, zawartą w DLL. Ta funkcja może być częścią Win32 API lub własnego odziedziczonego kodu.

Rozwiązanie Zadeklaruj metodę w kodzie języka C#, użytym do dostępu do niezarządzanej funkcji, zarówno jako extem, jak i static, i zastosuj atrybut System.Runtime.lnteropServices.DlUmportAttribute, aby wyspecyfikować plik DLL oraz nazwę niezarządzanej funkcji.

Omówienie Aby wykorzystać funkcję C z biblioteki zewnętrznej, należy po prostu odpowiednio ją zadekla­ rować. Moduł CLR automatycznie wykona resztę czynności, w tym ładowanie DLL do pamięci, gdy funkcja jest wywoływana oraz przekształcenie parametrów z typów danych .NET na typy danych C. Usługi .NET, wspierające wykonanie międzyplatformowe, noszą nazwę Plnvoke (Plat­ form Invoke - przywołanie platformy). Ten proces jest zazwyczaj bezkolizyjny. Okazjonalnie jednak trzeba będzie wykonać trochę więcej pracy, na przykład wtedy, gdy chcemy posługiwać się strukturami zawartymi w pamięci, obsługą wywołań zwrotnych lub zmiennych łańcuchów. Funkcja Pi nvoke jest często wykorzystywana dla osiągnięcia dostępu do funkcjonalności zawartej w Win32 API , szczególnie wtedy, gdy chodzi o cechy niedostępne w zestawie zarzą­ dzanych klas .NET Framework. W tej książce zawarto wiele przykładów takiego wykorzystania funkcji Plnvoke. Win32 API tworzą trzy biblioteki rdzenia (core): •

Kernel32.dll udostępnia funkcjonalność operacyjną, taką jak ładowanie procesów, przełą­ czanie kontekstów oraz operacje I/O dotyczące plików i pamięci.



User32.dll zawiera funkcje niezbędne do posługiwania się oknami, menu, oknami dialogo­ wymi, ikonami itd.



G DI32.dlł udostępnia możliwości graficzne, umożliwiające rysowanie bezpośrednio w oknach, menu i powierzchniach kontrolnych oraz drukowanie.

Dla przykładu rozważmy funkcje Win32 API, używane do odczytu i zapisu plików INI, takich jak GetPrivateProfileString i WritePrivateProjileString, zawarte w Kernel32.dll. System .NET Framework nie zawiera żadnych klas, które opakowują tę funkcjonalność. Jednakże można zaimportować te funkcje przy użyciu atrybutu DlUmportAttribute-. [ Dlllmport ( " ke rnel32 . DLL" , Ent ryPoint= "WriteP rivateP rof ileSt ring " ) J p rivate static exte rn bool WriteP rivateP rofileSt ring ( s t ring lpAppName , st ring l pKeyName , st ring l pSt ring , st ring l pFileName ) ;

Rozdział 1 5: Współdziałanie niezarządzanego kodu 447 Argumenty wyspecyfikowane w podpisie metody

WritePrivateProfileString muszą

być zgodne

z metodą DLL, inaczej wystąpi błąd przy próbie ich przywołania. Należy zwrócić uwagę, że nie zostaje zdefiniowany żaden szkielet metody, ponieważ deklaracje odnoszą się do metody DLL.

Część EntryPoint

DlllmportAttribute jest w cym przykładzie punktu wejścia, gdy nazwa zadeklarowanej funkcji

atrybutu

potrzeby określania

opcjonalna. Nie ma pokrywa się z nazwą

funkcji w bibliotece zewnętrznej.

A oto przykład niestandardowej klasy lniFileWrapper, który deklaruje te metody prywatnie, a następnie dołącza metody publiczne

(public),

które je przywołują zależnie od bieżącego wska­

zanego pliku:

using System ; using System . Text ; using System . Runt ime . Inte ropServices ; using System . Windows . Fo rms ; public class IniFileWrappe r { p rivate s t ring f ilename ; public s t ring Filename { get { retu rn f ilename ; } } public IniFileWrappe r ( st ring filename ) { t h i s . filename = fil ename ; } [ Dl l!mpo rt ( " ke rnel32 . dl l " , Ent ryPoint= " GetP rivateP rofileSt ring " ) ) p rivate static extern int GetP rivateProfileSt ring ( st ring lpAppName , s t ring lpKeyName , st ring l pDefault , St ringBuilde r lpRetu rnedSt ring , int nSize , st ring l p FileName ) ; [ Dl l !mpo rt ( " ke rnel32 . dl l " , Ent ryPoint="W riteP rivateP rofileSt ring " ) J p rivate static exte rn bool W riteP rivateP rofileSt ring ( st ring lpAppName , st ring l pKeyName , st ring l pSt ring , st ring lpF ileName ) ; public st ring GetiniValue ( st ring section , st ring key ) { St ringBuilde r b u f f e r = new St ringBuilde r ( ) ; s t ring sDefault = " " ; if ( GetP rivateP rof ileSt ring ( section , key , sDefaul t , buffe r , buffe r . Capacity , f ilename ) ! = 0 ) { retu rn b u f fe r . ToSt ring ( ) ; } else { ret u rn n u l l ; } } public bool W riteiniValue ( st ring section , s t ring key , st ring value) { retu rn W riteP rivateP rofileSt ring ( sect ion , key , value , filename ) ; } } Istnieją również inne funkcje Win32 API, które umożliwiają uzyskiwanie informacji z pliku INI, w cym metody odczytujące poszczególne sekcje w pliku INI. Nie są one używane w cym prostym przykładzie.

448

C#

Księga przykładów

-

Wskazówka Metoda GetPrivateProfileString jest deklarowana łącznie z jednym para­ metrem StringBuilder (lpReturnedString). Jest to konieczne, gdyż łańcuch musi być modyfikowalny - kiedy wywołanie się zakończy, będzie on zawierał zwrotną informację pliku INI. Kiedy potrzebny jest taki mutujący ciąg, należy zastąpić klasę String klasą Strin­ gBuilder. Często konieczne będzie utworzenie StringBuilder z buforem określonego roz­ miaru, a następnie przekazanie do funkcji rozmiaru bufora jako innego parametru. Można określić liczbę znaków w konstruktorze StringBuilder (więcej informacji na temat klasy StringBuilder zawiera przepis 2.1 ) .

Program ten można łatwo przetestować. Najpierw należy utworzyć plik INI pokazany w poniż­ szym przykładzie: [ SampleSection ] Keyl=Valuel Key2=Value2 Key3=Val ue3

Następnie należy uruchomić poniższy kod. Przedstawia on aplikację konsolową, wczytującą i zapisującą wartości INI. p u b l i c class I n iTest { p rivate static void Main ( ) { IniFileWrappe r ini = new IniFileWrappe r ( Applicat ion . Sta rt upPath + " \\in itest . in i " ) ; st ring val = ini . Get iniValue ( " SampleSection " , " Key l " ) ; Console . W riteline ( "Value of Keyl in [ SampleSection ] is : " + val ) ; ini . WriteiniValue ( " SampleSection " , " Key l " , " New Val ue" ) ; val = ini . Geti niValue ( " SampleSection " , " Key l " ) ; Console . Writeline ( "Value of Keyl in [ SampleSection ] i s now : " + val ) ; in i . WriteiniVal ue ( " SampleSection " , " Key l " , "Value l " ) ; Console . Readline ( ) ; } }

1 5.2 Przejęcie uchwytu kontrolki, okna lub pliku Problem Chcesz wywołać niezarządzaną funkcję, która wymaga podania uchwytu kontrolki, okna lub pliku.

Rozdział 1 5: Współdziałanie niezarządzanego kodu

449

Rozwiązanie Wiele klas, w tym wszystkie klasy wyprowadzone od Control oraz klasa FikStream, zwraca uchwyty jako lntPtr, poprzez właściwość o nazwie Handk. Inne klasy również udostępniają podobne informacje; na przykład klasa System.Diagnostics.Process oprócz właściwości Handk udostępnia właściwość Process. Main WindowHandle.

Omówienie .NET Framework nie ukrywa danych niższej warsrwy, takich jak uchwyty kontrolek i okien, używane przez system operacyjny. Informacje takie są zazwyczaj nieużywane, jednak można je odczytać w razie konieczności wywołania niezarządzanej funkcji, która ich wymaga. Na przy­ kład wiele funkcji należących do Microsoft Windows API wymaga sterowania uchwytami do okien. Dla przykładu rozważmy aplikację Windows przedstawioną na rysunku 1 5- 1 . Składa się ona z pojedynczego okna, które zawsze jest usytuowane nad pozostałymi oknami, niezależnie od fokusu. Takie zachowanie jest wymuszone przez ustawienie właściwości Form. TopMost pro­ perty na wartość true. Formularz zawiera także timer, który okresowo wywołuje niezarządzane funkcje GetForegroundWintiJJw i GetWindowText z WinAPI, aby określić, które okno obecnie posiada fokus.

Actlve \lr\idow Captlon :

R�IS-02 - Mcrosolt � C# .IET [rui] - Ad!vewndowlrło.cs [Desąl]

AdlYe \lr\idow Hardo :

964022

Is R This wndow7

Falsa

Rysunek 1 5-1

Wyszukiwanie informacji o aktywnym oknie.

Przykład zawiera dodatkowo pewien interesujący szczegół. Kod używa również właściwo­ ści Form.Handle, aby uzyskać uchwyt formularza głównej aplikacji, po czym porównuje je z uchwytem aktywnego formularza w celu sprawdzenia, czy bieżąca aplikacja posiada fokus. A oto kompletny kod formularza: using System ; using System . Windows . Fo rms ; using System . Runtime . In t e ropServices ; using System . Text ; public class ActiveWindowlnfo : System . Windows . Fo rms . Form {

li ( Kod p roj ektanta pominięto ) . p rivate System . Windows . Fo rms . Time r tm rRef resh ; p rivate System . Windows . Fo rms . Label lblC u r rent ; p rivate System . Windows . Fo rms . Label l blHandle ; p rivate System . Windows . Fo rms . Label l b lCapt ion ;

450

C#

-

Księga przykładów [ Dlllmport ( " u ser32 . dl l " ) ] p rivate static exte rn int Get Foreg roundWindow ( ) ; [ Dlllmport ( " u s e r32 . dl l " l l p rivate static exte rn int GetWindowText ( int hWnd , St ringBuilde r text , int count ) ; p rivate void tmrRef resh_Ti c k ( object sende r , System . EventArgs e ) { int cha rs = 256 ; St ringBuilde r buff = new St ringBuilde r ( c h a rs ) ; int handle = Get Fo regroundWindow ( ) ; if ( GetWindowText ( hand l e , buff , c ha rs ) > 0 ) { l blCaption . Text = bu f f . ToString ( ) ; l blHandle . Text = handle . ToSt ring ( ) ; if ( new IntPt r ( handl e ) == t h i s . Handle ) { l blCu r rent . Text = "True " ; } else { lblCu r rent . Text = " False " ; } } }

}

Wskazówka Infrastruktura formularzy Windows zarządza uchwytami (hand/es) w spo­ sób przezroczysty. Zmieniając niektóre właściwości formularzy, można wymusić wyge­ nerowanie przez CLR nowego uchwytu. Dlatego należy odczytać uchwyt bezpośrednio przed jego użyciem, zamiast zapamiętywać je na dłuższy czas.

1 5.3 Wywołanie niezarządzanej funkcji wykorzystującej strukturę Problem Chcesz wywołać niezarządzaną funkcję, która akceptuje strukturę jako paramecr.

Rozwiązanie Zdefiniuj strukturę w kodzie języka C#. Użyj atrybutu System.Runtime.lnteropServices.Struct­ LayoutAttribute dla skonfigurowania alokacji struktury w pamięci. Użyj metody static SizeOf klasy System.Runtime.lnterop.Marshal, jeżeli trzeba określić rozmiar niezarządzanej struktury w bajtach.

--

Rozdział 1 5: Współdziałanie niezarządzanego kodu

451

Omówienie Czysty kod języka C# nie daje możliwości bezpośredniej kontroli przydziału pamięci. Zamiast tego CLR swobodnie przemieszcza dane wewnątrz pamięci w dowolnej chwili w celu opcyma­ liwwania wydajności. Może to powodować problemy przy interakcji ze starszymi funkcjami C, które oczekują sekwencyj nego układu struktur w pamięci. Na szczęście .NET umożliwia rozwiązan ie tego problemu przy użyciu atrybutu StructLayoutAttribute, który umożliwia wyspe­ cyfikowanie aranżacji pól danej klasy lub struktury w pamięci. Dla przykładu posłużmy się niezarządzaną funkcją GetVersionEx, dostępną w pliku Kernel32.dlł. Funkcja ta akceptuje wskaźnik do struktury OSVERSIONINFO i używa go, aby zwrócić informację o wersj i systemu operacyjnego. Aby użyć struktury OSVERSIONINFO w kodzie języka C#, należy zdefiniować ją za pomocą atrybutu StructLayoutAttribute, jak to pokazano niżej: [ St ruct layout ( LayoutKind . Sequentia l ) ) public class OSVe rsioninfo { public int dwOSVe rsionin foSize ; public int dwMa j o rVersion ; public int dwMino rVe rsion ; public int dwBuildNumbe r ; public int dwPlatformid ; [ Ma rshalAs ( UnmanagedType . ByValTSt r , SizeConst=128 ) ] public St ring szCSDVe rsion ; }

Należy zauważyć, że ta struktura korzysta również z atrybutu System.Runtime.lnteropServices. Marsha/AsAttribute, wymaganego dla łańcuchów o stałej długości. W cym przykładzie Marshal­ AsAttribute określa łańcuch, który będzie przekazany jako wartość i będzie zawierał bufor o długości dokładnie 1 28 znaków, zgodnie ze specyfikacją struktury OSVERSIONINFO. Przy­ kład wykorzystuje układ sekwencyjny, co oznacza, że typy danych w strukturze są poukładane w takim porządku, w jakim są listowane w ramach klasy lub struktury. Przy użyciu układu sekwencyjnego można także skonfigurować upakowanie dla struktury, określając pole o nazwie Pack w konstruktorze StructLayoutAttribute. Wartością domyślną jest 8, co oznacza, że struktura wstanie upakowana w 8-bajtowych granicach. Zamiast układu sekwencyjnego można użyć LayoutKind.Explicit, ale w cym wypadku należy zdefiniować przesunięcie w bajtach dla każdego pola przy użyciu atrybutu FieldOffietAttribute. Ten układ jest przydatny w sytuacji, gdy mamy do czynienia z nieregularnie upakowanymi strukturami albo wtedy, gdy trzeba pominąć pewne elementy, których nie zamierza się używać. Oto przykład, w którym zdefiniowano klasę OSVersionlnfo z jawnym układem tego typu: [ St ructlayout { LayoutKind . Explicit ) ) public c l a s s OSVe rsioninfo { [ Field0ffset ( 0 ) ) public int dwOSVe rsioninfoSize ; [ Field0ffset { 4 ) J public int dwMa j o rVe rsion ; [ FieldOf f set ( B ) ) public int dwMino rVe rsion ; [ Field0f f set { 12 ) ) public int dwBuildNumbe r ; [ Field0f f set { 16 ) ) public int dwPlatformid ; [ Ma rshalAs ( UnmanagedType . ByValTSt r , SizeConst=128 ) J

452

C#

-

Księga przykładów [ Field0ffset ( 29 ) ] public St ring s zCSDVe rs ion ;

} Po zdefiniowaniu struktury używanej przez funkcję

GetVersionEx,

należy zadeklarować funk­

cję, a następnie ją wykorzystać. Przytoczona niżej aplikacja konsolowa przedstawia cały kod potrzebny do tego celu. Należy zauważyć, że do parametru OSVersionlnfo zastosowano atrybuty lnAttribute i OutAttribute, aby wskazać, że przekształcenie należy wykonać, kiedy struktura jest przekazywana do funkcji, a także w chwili, gdy jest zwracana z funkcji. Ponadto kod używa metody Marshal.SizeOf do skalkulowania rozmiaru pamięci zajmowanej przez przekształcaną strukturę. u sing System ; u s ing System . Runt ime . Inte ropSe rvices ; public class CallWithSt ruct u re {

li ( klasę OSVe rsioninfo pominięto ) . [ Dll!mport ( " ke rnel32 . dl l " ) J public static extern bool GetVe rsionEx ( [ I n , Out ] OSVe rs ioninfo osvi ) ; p rivate static void Main ( ) { OSVe rsioninfo osvi = new OSVe rsioninfo ( ) ; osvi . dwOSVe rsioninfoSize = Ma rshal . SizeOf ( osvi ) ; GetVe rsionEx ( osvi ) ;

+ osvi . dwOSVe rsioninfoSize ) ; Console . Writeline ( "Maj o r Version : " + osvi . dwMa j o rVers ion ) ; Console . Writeline ( " Class s ize : "

Console . Writeline ( "Mino r Version : " + osvi . dwMino rVe rsion ) ; Console . Writeline ( " Build Numbe r : " Console . W riteline ( " Plat f o rm Id : "

+ osvi . dwBuildNumbe r ) ; + osvi . dwPlat f o rmid ) ;

Console . W riteline ( " CSD Ve rsion : " + o svi . s zCSDVe r s ion ) ;

Console . Writeline ( " Platfo rm : " + Envi ronment . OSVe rsion . Platform ) ; Console . W riteline ( "Version : " + Envi ronment . OSVe rsion . Ve rs ion ) ; Console . Readline ( ) ; } } Po uruchomieniu tej aplikacji w systemie Windows XP pojawi się następująca informacja: Class size : 148 Maj o r Ve rs ion : 5 Minor Ve rs ion : 1 Bu ild Numbe r : 2609 Plat form Id : 2 CSD Ve rsion : Plat form : Win32NT Version : 5 . l . 26ee . e

Rozdział 1 5: Współdziałanie niezarządzanego kodu 453

1 5.4 Wywołanie niezarządzanej funkcji posługującej się wywołaniem zwrotnym Problem Chcesz wywołać niezarządzaną funkcję i umożliwić jej wywołanie metody w twoim kodzie.

Rozwiązanie Utwórz delegata, który ma podpis konieczny dla wywołania zwrotnego. Użyj tego delegata przy definiowaniu i korzystaniu z niezarządzanej funkcji .

Omówienie Wiele funkcji Win32 API korzysta z wywołania zwrotnego. Na przykład do wyszukania nazw wszystkich aktualnie otwartych okien można wywołać niezarządzaną funkcję z pliku User32.dll. Przy wywołaniu funkcji

Enum Windows

Enum Windows

należy podać wskaźnik do funkcji

w kodzie projektanta. System operacyjny Windows będzie wywoływał teraz tę funkcję powta­ rzalnie, po jednym razie dla każdego znalezionego okna i przekaże uchwyt okna do kodu pro­ jektanta. System .NET Framework pozwala obsłużyć wywołania zwrotne bez uciekania się do wskaź­ ników i bloków niezabezpieczonego kodu. Zamiast tego należy zdefiniować i posłużyć się dele­ gatem wskazującym funkcję wywołania zwrotnego. Na przykład przesłanie delegata do funkcji

Enum Windows spowoduje, że CLR automatycznie przekieruje go do spodziewanego wskaźnika niezarządzanej fimkcji. Niżej jest pokazana aplikacja konsolowa, wykorzystująca

Enum Windows

z wywołaniem

zwrotnym w celu wyświetlenia nazwy każdego z otwartych okien.

using System ; using System . Text ; using System . Runtime . Inte ropService s ; public class GetWindows {

li Podpis dla metody zwrotnej . public delegate bool Cal l Back { int hwnd , int l Param ) ;

li Nieza rządzana funkcj a , któ ra wyzwoli metodę zwrot ną po wyliczeniu li otwa rtych okien . [ Dl l impo rt ( " u s e r32 . dll " ) ] public static exte rn int EnumWindows { Cal l Back callback, int pa ram ) ; [ Dl l!mpo rt { " u s e r32 . dll " ) ] public s tatic extern int GetWindowText ( int hWnd , St ringBuilde r l pSt ring , int nMaxCount ) ; p rivate static void Main ( ) { Call Back call Back = new CallBa c k { DisplayWindowinfo ) ;

454

C#

-

Księga przykładów li Żądanie , aby s ystem operacyj ny wyl iczył wszyst kie okna i wyzwolenie li metody zwrotnej z uchwytem każdego z nich . EnumWindows ( callBack , 0 ) ; Console . Readline ( ) ; }

li Metoda odbie raj ąca wywołanie zwrotne . D rugi pa ramet r nie j est używany , li ale j est konieczny ze względu na wymóg zgod ności z podpisem . public static bool DisplayWindowi n fo ( int hWnd , int l Pa ram ) { int chars = lfl0 ; St ringBuilder buf = new St ringBuilde r ( cha rs ) ; if ( GetWindowText ( hWnd , bu f , cha r s ) ! = 0 ) { Console . Writeline ( bu f ) ; } ret u rn t rue ; } }

1 5.5 Odczytywanie niezarządzanej informacji o błędach Problem Chcesz odczytać informacje o błędach (kod błędu albo komunikac o błędzie) , wyjaśniające przyczyny niepowodzenia wywołania Win32 API.

Rozwiązanie W deklaracji niezarządzanej metody ustaw pole SetlastError atrybutu DlllmportAttribute na wartość true. Jeżeli podczas wykonywania metody wystąpi błąd, wywołaj statyczną merodę MarshaL GetlastWin32Error w celu odczytania kodu błędu. Aby otrzymać tekst opisu dla okre­ ślonego kodu błędu, należy użyć niezarządzanej funkcji FormatMessage.

Omówienie Nie można odczycywać informacji o błędzie bezpośrednio przy użyciu niezarządzanej funkcji GetlastError. Problem polega na rym, że kod błędu zwrócony przez GetlastError może nie odzwierciedlać błędu spowodowanego przez użytą niezarządzaną funkcję, gdyż może on być uscawiony przez inne klasy systemu .NET Framework lub CLR. Zamiast tego należy bezpiecz­ nie odczytać informację o błędzie przy użyciu statycznej metody Marshal. GetlastWin32Error. Metoda ta powinna zostać wywołana natychmiast po niezarządzanym wywołaniu. Zwróci ona informację o błędzie rylko raz (kolejne wywołania GetlastWin32Error zwrócą po prostu kod błędu 1 27) . Należy ponadto ustawić pole SetlastError atrybutu DlllmportAttribute na wartość

Rozdział 1 5: Współdziałanie niezarządzanego kodu

455

true w celu wskazania, że kody błędów przekazywane przez tę funkcję mają być zapamiętane w buforze.

[ Dl l import ( " us e r32 . d l l " , SetLa s t E r ro r= t r u e ) I

Można też uzyskać dodatkową informację dla kodu błędu Win32, posługując się niezarządzaną funkcją FormatMessage, dostępną w pliku Kernel32.dll. Podana niżej aplikacja konsolowa próbuje pokazać okno komunikatu, ale przekazuje błędny uchwyt okna. Informacja o błędzie jest wyszukiwana za pomocą MarshaL Getl.AstWin32E"or, a korespondująca z nią informacja tekstowa jest wyszukiwana za pomocą FormatMessage. u s ing System ; using System . Runtime . Int e ropServices ; public class TestE r r o r { [ Dl l impo rt ( " ke rnel32 . d ll " ) J p rivate u nsafe static exte rn int Fo rmatMes sag e ( int dwFlags , int lpSou rce , int dwMessageid , int dwLanguageid , ref St ring lpBuffe r , int nSize , int A rgument s ) ; [ Dl l impo rt ( " u se r32 . dl l " , Set Last E r ro r=t rue ) J public static exte rn int Mes s ageBox ( int hWnd , st ring pText , st ring pCaption , int uType ) ; p rivate static void Main ( ) { int badWindowHandle = 453 ; Mes sageBo x ( badWindowHandle , " Mes sage " , " Capt ion " , 0 ) ; int e r ro rCode = Ma rshal . GetLastWin3 2 E r ro r ( ) ; Console . W riteLine ( e r ro rCode ) ; Console . W riteLine ( Ge t E r ro rMessage ( e r ro rCode ) ) ; Console . ReadLine ( ) ; }

li Get E r ro rMessage f o rmatuj e i zwraca komunikat o błędzie odpowiadający li wej śc iowemu e r ro rCode . public static st ring Get E r rorMes sage ( int e rrorCode ) { int FORMAT_MESSAGE_ALLOCATE_BUFFER = exeee00 10e ; int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 ; int FORMAT_MESSAGE_ FROM_SYSTEM

= exee0e 1e0e ;

int messageSize = 255 ; s t ring lpMsgBuf = " " ; int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER FORMAT_MESSAGE_ FROM_SYSTEM I FORMAT_MESSAGE_IGNORE_ INSERTS ; int retVal = FormatMe ssage ( dwFlag s , e , e r ro rCode , 0 , ref lpMsgBuf , mes s ageSize , 0 ) ; if ( 0 == retVal ) { ret u rn nul l ; } else { ret u rn l pMsgBu f ; } }

456

C#

-

Księga przykładów

} Oto informacja zwrotna generowana przez ten program:

1400 Invalid window handle .

1 5.6 Użycie komponentu COM w kliencie .NET Problem Chcesz wykorzystać komponent COM w kliencie .NET.

Rozwiązanie Użyj głównej asemblacji międzyoperacyjnej, jeśli jest dostępna. W przeciwnym wypadku wyge­ neruj opakowanie wywoływalne w czasie wykonania, używając narzędzia Type Library Importer (Tlbimp.exe) lub właściwości Add Reference w Visual Studio .NET.

Omówienie System .NET Framework zawiera rozbudowane wsparcie współpracy z COM. Aby udostęp­ nić klientom .NET interakcje z komponentami COM, .NET używa opakowania wywoływal­ nego w czasie wykonania

(runtime ca/labie wrapper

-

RCW) - specjalnej klasy proxy .NET,

która pośredniczy pomiędzy kodem .NET a komponentem COM. Funkcja opakowania RCW obsługuje wszystkie szcreg6ły, w tym przekształcanie typów danych przy użyciu tradycyjnych interfejsów COM i zarządzanie zdarzeniami COM. Do dyspozycj i są następujące opcje wykorzystania RCW: • Uzyskanie pożądanej funkcji

od autora oryginalnego komponentu COM. W tym wypadku

RCW jest wywoływane jako primary

interop assembly (PIA).

• Wygenerowanie opakowania przy użyciu narzędzia Tlbimp.exe lub Visual Studio . NET. • Utworzenie własnego opakowania przy użyciu typów z przestrzeni nazw

lnteropServices (co

System.Runtime.

jest bardzo skomplikowane i czasochłonne).

By wygenerować RCW przy użyciu Visual Studio .NET, należy w menu wybrać Project

I Add

Reference, a następnie wskazać odpowiedni komponent na zakładce COM. Po kliknięciu OK pojawi się monit informujący o tworzeniu RCW Międzyoperacyjna asemblacja główna zosta­ nie wygenerowana i dodana do odnośników projektu. Następnie można użyć Object Browser w celu przejrzenia udostępnianych przezeń przestrzeni nazw i klas. jeżeli nie korzysta się z Visual Studio .NET, należy utworzyć opakowanie asemblacji za pomocą narzędzia Tlbimp.exe, dołączonego do .NET Framework. Jedynym wymaganym argu­ mentem jest nazwa pliku zawierającego komponent COM. Na przykład poniższa instrukcja utworzy RCW z domyślną nazwą pliku i przestrzeni nazw, przy założeniu, że plik MyCOM­ Componenc.dlJ znajduje się w bieżącym katalogu:

-

Rozdział 1 5: Współdziałanie niezarządzanego kodu 457 t lbimp MyCOMComponent . dl l Przy założeniu, że MyCOMComponent zawiera bibliotekę typów o nazwie MyClasses, wyge­ nerowany plik RCW będzie miał nazwę MyClasses.dll i będzie ujawniał klasy poprzez prze­ strzeń nazw MyClasses. Można także skonfigurować te opcje za pomocą parametrów wiersza poleceń, zgodnie z opisem dostępnym w dokumentacji MSDN. Na przykład przełącznik /out:[Filename] pozwala określić inną nazwy pliku asemblacji, a /namespace: [Namespace] włączenie innej przestrzeni nazw dla generowanych klas. Można również określić plik klucza przy użyciu /keyfile[keyfilename] , aby komponent był podpisany i posiadał silną nazwę, co pozwoli na umieszczenie go w globalnym buforze asemblacji (GAC). Przełącznik /primary pozwala utworzyć PIA. Jeżeli jest to możliwe, zawsze należy używać PIA zamiast generowania własnego RCW. Główne asemblacje międzyoperacyjne zazwyczaj działają zgodnie z oczekiwaniami, ponieważ zostały utworzone przez oryginalnego wydawcę komponentu. Mogą one zawierać także dodat­ kowe rozszerzenia .NET. Jeżeli dla komponentu COM w systemie zarejestrowano PIA, Visual Studio .NET automatycznie użyje tego PIA podczas tworzenia odniesienia do komponentu COM . Na przykład system .NET Framework zawiera asemblację adodb.dll, umożliwiającą uży­ cie klasycznych obiektów ADO mechanizmu COM. Po dodaniu odniesienia do komponentu Microsoft ActiveX Data Objects, ca międzyoperacyjna asemblacja zostanie użyta automatycznie i żadne nowe RCW nie zostanie wygenerowane. Także Microsoft Office XP zawiera PIA, które usprawnia wsparcie .NET dla Office Automation. Asemblację tę można pobrać ze strony inter­ netowej MSDN, pod adresem

http:l/msdn. microso.ft.com/downlaads!listlojfice.asp.

Poniższy przykład pokazuje, jak należy użyć międzyopcracyjności COM, by uzyskać dostęp do klasycznych obiektów ADO z aplikacji systemu .NET Framework:

using System ; public class ADOC l a s s ic { p rivate static void Main ( ) { ADOD B . Connect ion con = new ADODB . Con nection ( ) ; st ring connectionSt ring = " P rovider=SQLOLEDB . l ; " " Data Sou rce=localhost ; " +

+

" In itial C atalog=Northwind ; Integ rated Secu rity=SSPI " ; con . Open ( connect ionSt ring , nul l , nul l , 9 ) ; object rec o rd sAffected ; ADOD B . Recordset rs = con . Exe c u t e ( "SELECT * F rom C u stome rs " , out record sAf fected , 9 ) ; while ( rs . EOF ! = t ru e ) { Console . W riteLine ( rs . Field s [ " C u s tome rID " J . Value ) ; rs . MoveNext ( ) ; } Console . Readline ( ) ; } }

458

C#

Księga przykładów

-

1 5. 7 Szybkie zwolnienie komponentu COM Problem Chcesz zapewnić natychmiastowe usunięcie komponentu COM z pamięci, bez oczekiwa­ nia na procedurę czyszczenia pamięci

(.garbage colkction);

alternatywnie chcesz się upewnić,

że obiekty COM zostały zwolnione w określonym porządku.

Rozwiązanie Zwolnij odniesienie do obiektu COM w niższej warstwie przy użyciu statycznej metody

shaLRekaseComObject,

Mar­

przekazując do niej RCW

Omówienie Do określenia, kiedy obiekty powinny zostać zwolnione, COM używa licznika odniesień. Gdy projektant używa RCW, odniesienie do obiektu COM z niższej warstwy będzie utrzymywane nawet wtedy, gdy zmienna obiektu wyjdzie poza zakres. Odniesienie zostanie zwolnione dopiero wtedy, gdy program czyszczący pamięć zwolni obiekt RCW W rezultacie projektant nie może kontrolować ani kiedy, ani w jaki sposób obiekty COM będą zwolnione z pamięci. Aby obejść te ograniczenia, należy skorzystać z metody

MarshaLRekaseComObject. Rozwi­ Recordset i Connection

jając przykład z przepisu 1 5.6 (dla ADO) można zwolnić obiekty ADO poprzez dołączenie następujących dwóch linii na końcu kodu:

Ma rshal . ReleaseComObj ect ( rs ) ; Ma rshal . ReleaseComObj ect ( con ) ;

Uwaga Technicznie rzecz biorąc, metoda Re/easeComObject nie zwalnia w rzeczywi­ stości obiektu COM, tylko dekrementuje jego licznik odniesienia. Gdy ten licznik osiągnie O (zero), obiekt COM zostanie zwolniony. Jednak jeżeli różne fragmenty kodu używają tej samej instancji obiektu COM, wszystkie będą musiały go zwolnić, aby został faktycznie usunięty z pamięci.

1 5.8 Użycie parametrów opcjonalnych Problem Chcesz wywołać metodę w komponencie COM bez dostarczania wszystkich wymaganych para­ metrów.

Rozdział 1 5: Współdziałanie niezarządzanego kodu 459

Rozwiązanie Użyj pola

Tjpe.Missing.

Omówienie System .NET Framework zaprojektowano, szeroko stosując przeciążanie metod. Wic;kszość z nich przeciążono wielokrotnie, dzic;ki czemu można wywoływać wersje; wymagającą wyłącznie takich parametrów, jakie chcemy podać. Z drugiej strony COM nie obsługuje przeciążania metod. Zazwyczaj komponenty COM używają metod z długą listą opcjonalnych parametrów. Niestety jerzyk C# nie wspiera opcjonalnych parametrów, w wyniku czego projektanci C# są czc;sto zmuszeni do dostarczenia licznych dodatkowych lub nieistotnych wartości, gdy chcą uzyskać dostc;p do komponentu COM. A ponieważ parametry COM są czc;sto przekazywane przez odniesienie, kod projektanta nie może przekazać po prostu odniesienia

nuli

-

zamiast tego

musi zadeklarować zmienną obiektu, którą nastc;pnie przekaże. Można złagodzić ten problem do pewnego stopnia przez wykorzystanie pola

Iype.Missing,

jeżeli opcjonalny parametr można pominąć. Kiedy ten parametr ma być przekazany jako odnie­ sienie, należy po prostu zadeklarować pojedynczą zmienną obiektu i ustawić ją jako równą

Missing, a nastc;pnie używać jej

IJpe.

we wszystkich wypadkach, jak to pokazano niżej:

p rivate static obj ect n = Type . Missing ; W poniższym przykładzie zostały użyte obiekty Word COM do programowego utworzenia i pokazania dokumentu. Wiele metod, z których ten przykład korzysta, wymaga opcjonalnych parametrów przekazywanych przez odniesienie. Warto zauważyć, że użycie pola

IJpe.Missing

bardzo ten kod upraszcza. Każde użycie tego pola w poniższym przykładzie zostało wyróżnione wytłuszczoną czcionką. using System ; public class OptionalPa rameters { p rivate static obj ect n = Type . Mi s sing ; p rivate static void Main ( ) {

li U ru chomienie p rog ramu Wo rd w tle . Word . ApplicationCla s s app = new Word . ApplicationClas s ( ) ; app . DisplayAle rt s = Word . WdAle rt Level . wdAlert sNone ;

li Two rzenie nowego dokumentu ( nie j est to widoczne dla użyt kownika ) . Word . Document doc = app . Documen t s . Ad d ( ref n , ref n , ref n , ref n ) ; Console . W riteLine ( ) ; Console . W riteLine ( " C reating new document . " ) ; Console . W riteLine ( ) ;

li Dodanie nagłówka i dwóch wie rszy tekst u . Word . Range range = doc . Parag raph s . Ad d ( ref n ) . Range ; range . I n s e rtBefo re ( " Test Document " ) ; st ring style = " Heading 1 " ; object obj Style = s t y l e ;

460

C#

Księga przykładów

-

range . set_ Style ( ref obj Style ) ; range = doc . Pa rag raphs . Add ( ref n ) . Range ; range . I nse rtBefo re ( " Line one . \ n line two . " ) ; range . Font . Bold = l ;

li Wyświetlenie podglądu wyd ruku i u j awnienie p rog ramu Word . doc . P rint P review ( ) ; app . Visible = t ru e ; Console . Readline ( ) ; } }

1 5.9 Użycie kontrolki ActiveX w kliencie .NET Problem Chcesz umieścić kontrolkę ActiveX w oknie aplikacji .NET Framework.

Rozwiązanie Użyj RCW w taki sam sposób, jak dla zwyczajnego komponentu COM . Aby podczas projekto­ wania pracować z kontrolką ActiveX, dodaj ją do paska narzędziowego Visual Studio .NET.

Omówienie .NET Framework zawiera takie samo wsparcie dla wszystkich komponentów COM, w tym dla kontrolek ActiveX. Zasadnicza różnica polega na rym, że klasa RCW dla kontrolki ActiveX wywodzi się ze specjalnego typu .NET

System. Wi7UWws.Forms.AxHost. Technicznie

rzecz biorąc,

można dodać kontrolkę AxHost do formularza, a wtedy skomunikuje się ona z kontrolką ActiveX „w cle". Ponieważ typ AxHost jest wyprowadzony z System. Wi7UWws.Forms. Control, dostarcza stan­ dardowych dla systemu .NET właściwości, metod i zdarzeń, takich jak Location,

Size, Anchor itd.

W przypadku automatycznego generowania RCW klasy AxHost zawsze mają nazwy rozpoczyna­ jące się od liter Ax. Można utworzyć RCW dla kontrolki ActiveX, podobnie jak dla innych komponentów COM, przy użyciu Tlbimp.exe lub właściwości Add Reference w Visual Studio .NET, a następnie utwo­ rzyć kontrolki programowo. Jednakże najłatwiejszym sposobem przy korzystaniu z Visual Studio .NET jest dodanie kontrolki ActiveX do paska narzędziowego Toolbox (patrz przepis 1 1 .4, gdzie podano więcej szczegółów) . Po dodaniu kontrolki ActiveX do paska narzędziowego Toolbox nic się nie zmieni w pro­ jekcie. Można jednak wówczas użyć ikony z paska, aby dodać instancję kontrolki do formula­ rza.

Przy pierwszym takim działaniu Visual Studio .NET utworzy asemblację międzyoperacyjną

i doda ją do projektu. Na przykład po dodaniu kontrolki Microsoft Masked Eciit, która nie posiada bezpośredniego ekwiwalentu w .NET, Visual Studio .NET utworzy asemblację RCW

Rozdział 1 5: Współdziałanie niezarządzanego kodu 461 o takiej nazwie, jak Axlnterop.MSMask.dll. Oto kod, który można znaleźć w ukrytym obszarze projektanta, gdy utworzył on instancję kontrolki i dodał do formularza:

thi s . axMa skEdBoxl = new AxMSMa s k . AxMaskEdBox ( ) ; ( ( Sy stem . ComponentModel . ISupportinitialize ) ( t his . axMaskEdBox l ) ) . Begininit ( ) ;

li li li

axMa s kEdBoxl

this . axMas kEdBox l . Location = new System . D rawing . Point ( l6 , 12 ) ; this . axMaskEdBoxl . Name = " axMaskEdBox l " ; this . axMaskEdBoxl . OcxState = ( ( System . Windows . Fo rms . AxHost . State ) ( resou rces . GetObj ect ( " axMaskEdBoxl . OcxState " ) ) ) ; this . axMas kEdBoxl . Size = new System . D rawing . Size ( ll2 , 2 0 ) ; this . axMas kEdBoxl . Tabindex = 8 ; thi s . Cont rol s . Add ( th i s . axMaskEdBox l ) ; Należy zauważyć, że niestandardowych właściwości kontrolki ActiveX nie stosuje się bezpo­ średnio poprzez instrukcje zestawu właściwości. Są one odtwarzane jako grupa, kiedy kontrolka ustawia swoją trwałą właściwość

OcxState. Jednakże kod

projektanta może używać właściwości

kontrolki bezpośrednio.

1 5.1 O Wykorzystanie komponentu .NET przez COM Problem Chcesz utworzyć komponent .NET, który mógłby być wywoływany przez klienta COM.

Rozwiązanie Utwórz asemblację spełniającą określone ograniczenia przedstawione w niniejszym przepisie. Wyeksportuj bibliotekę typu dla asemblacji przy użyciu Type Library Exporter (Tlbexp.exe).

Omówienie System .NET Framework zawiera wsparcie dla klientów COM, umożliwiając wykorzystanie przez nich komponentów . NET. Kiedy klient COM utworzy obiekt .NET, CLR utworzy zarządzany obiekt oraz opakowanie obiektu (CCW),

wiwoływane przez COM. Klient COM

dokonuje interakcji z zarządzanym obiektem za pośrednictwem CCW Moduł runtime utwo­ rzy tylko jedno CCW dla zarządzanego obiektu, niezależnie od tego, jak wielu klientów COM go używa. Typy dostępne dla klientów COM muszą spełniać następujące wymagania: • Zarządzany typ (klasa, i nterfejs, struktura lub wyliczanie) musi być typu

liczny).

public

(pub­

462

C#

-

Księga przykładów

• Jeżeli klient COM ma utworzyć nowy obiekt, musi on mieć domyślnego konstruktora typu

publicznego. COM nie wspiera parametryrowanych konstruktorów. • Pola rypu, które mają być dostępne, muszą być polami publicznymi. Pola

private, protected,

interna/ i static nie są dostępne dla klientów COM. Ponadto należy rozważyć następujące zalecenia: • Nie jest zalecane tworzenie dziedziczonych zależności między klasami, ponieważ te zależno­

ści nie będą widoczne dla klientów COM (jakkolwiek .NET będzie próbował to symulować poprzez zadeklarowanie podstawowego współdzielonego interfejsu klasy o dzielonej bazie). • Zaleca się, żeby udostępniane klasy implementowały interfejs. Dodatkową kontrolę wersji

można uzyskać za pomocą atrybutu

System.Runtime.lnteropServices. GuidAttribute do okre­

ślenia GUID, które ma być przypisane do interfejsu. • Oprymalne jest nadanie zarządzanej asemblacji silnej nazwy, tak by mogła być zainstalo­

wana w GAC i współdzielona przez wielu klientów. Do utworzenia obiektu .NET klient COM potrzebuje biblioteki rypów (.db file). Biblioteka typów może być wygenerowana z asemblacji przy użyciu narzędzia Tlbexp.exe i następującej składni:

t l bexp Managedlibra ry . dl l Po wygenerowaniu biblioteki rypów, można odwoływać się d o niej z niezarządzanego narzędzia projektowania. W przypadku Visual Basic 6 odniesienie do pliku .tlb jest definiowane w oknie dialogowym Project/References. W Visual C++ 6 można użyć instrukcji portowania definicji z biblioteki typów.

#import w celu zaim­

16 Powszechnie używane interfejsy i wzorce Przepisy zawarte w niniejszym rozdziale pokazują, jak zaimplementować wzorce, które są czę­ sto używane przy projeRtowaniu aplikacji .NET Framework. Niektóre z nich są sformalizo­ wane przy użyciu interfejsów zdefiniowanych z bibliotece klas .NET Framework. Inne wzorce są mniej sztywne, ale nadal wymagają specjalnego podejścia przy projektowaniu i implementacji typów. Przepisy w i:iniejszym rozdziale omawiają następujące zagadnienia: • Tworzenie serialiwwalnych typów, które można łatwo zapisać na dysku, przesłać przez sieć

lub przelsazać jako wartość przez granice domen aplikacji (przepis 1 6. 1 ) . • Dostarczanie mechanizmu rworzącego dokładne i kompletne kopie (klony) obiektów (prze-

pis 1 6.2) . • Implementacja typów łatwych do porównania i sortowania (przepis

1 6.3).

• Wsparcie dla wyliczania elementów niestandardowych kolekcji (przepis

1 6.4).

• Prawidłowe zwalnianie niezarządzanych zasobów, gdy nie są już potrzebne (przepis

1 6.5).

• Wyświetlanie reprezentacji zmiennych obiektów w oparciu o specyfikatory formatu (prze­

pis 1 6.6) .

• Prawidłowa implementacja niestandardowych wyjątków i typów argumentów zdarzeń,

które są często używane przy projektowaniu aplikacji (przepisy 1 6.7 i 1 6.8). • Implementacja powszechnie używanych wzorców projektowania - wzorca pojedynczej

instancji

(Singleton Pattern)

i wzorca obserwatora

(Observer Pattern)

-

przy użyciu wbudo­

wanych właściwości języka C# i biblioteki klas .NET Framework (przepisy 1 6.9 i 1 6. 1 O).

464

C#

Księga przykładów

-

1 6.1 Implementacja serializowalnego typu Problem Chcesz zaimplementować niestandardowy typ, który jest serializowalny,

co

pozwoli na:

• Przechowanie instancji typu w pamięci stałej (na przykład w pliku lub bazie danych) . • Przesłanie instancji typu poprzez sieć. • Przekazanie instancji typu „za pośrednictwem wartości" pomiędzy domenami aplikacji.

Rozwiązanie Do serializacji prostych typów należy wykorzystać atrybut

System.SerializableAttribute w dekla­

racji typu. W przypadku bardziej złożonych typów, a także wtedy, gdy zachodzi potrzeba nad­ zorowania zawartości i struktury serializowanych danych należy zaimplementować interfejs

System.Runtime.Serialization.!Serializable.

Omówienie Przepis 2. 1 2 pokazuje, jak można serializować i deserializować obiekty przy użyciu klas for­ materów zawartych w bibliotekach klas .NET Framework. Jednakże typy nie są serializo­ walne domyślnie. Aby zaimplementować niestandardowy typ, który da się serializować, należy w deklaracji typu zastosować atrybut

SerializableAttribute.

Zastosowanie tego atrybutu wystar­

czy do utworzenia typu serial izowalnego, o ile wszystkie pola danych są typami serializowal­ nymi. Jeżeli implementowana jest niestandardowa klasa wyprowadzona z klasy podstawowej, wyjściowa klasa musi być również serializowalna. Wszystkie klasy formatera zawierają logikę niezbędną do serializacji typów, niosących atry­

Serializa.bleAttribute, i będą prawidłowo serializować wszystkie pola publiczne (public), (protected) i prywatne (private). Poniższy fragment kodu pokazuje deklarację typu pól serializowalnej klasy o nazwie Empwyee.

but

chronione i

using System ; [ Se rializable) public class Employee p rivate st ring name ; p rivate int age ; p rivate st ring add res s ; }

Uwaga

Klasy wyprowadzone z typu serializowalnego nie dziedziczą atrybutu Seria­ lizableAttribute. Aby wyprowadzone typy były serializowalne, należy zadeklarować je wprost jako serializowalne przez zastosowanie atrybutu SerializableAttribute.

--

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 465 Można wykluczyć określone pola z serializacji przez zastosowanie do nich atrybutu System. NonSerialiudAttribute. Jako regułę należy przyjąć, że z serializacji wyklucza się pola, które: •

Zawierają nieserialiwwalne rypy danych.



Zawierają wartości, które mogą być nieprawidłowe po deserializacji; przykładem mogą być połączenia do baz danych, adresy pamięci, identyfikatory wątków oraz uchwyty niezarzą­ dzanych zasobów.



Zawierają informacje wrażliwe lub poufne, takie jak hasła, klucze szyfrowania lub dane osobowe.



Zawierają dane, które łatwo odtworzyć lub wydobyć z innych źródeł - zwłaszcza wtedy, gdy są to dane o dużej objętości.

Jeżeli pewne pola są wykluczane z serializacji, należy zaimplementować własny ryp, aby skom­ pensować fakt nieobecności części danych po deserializacji obiektu. Nie można niestety utworzyć ani odczytać pominiętych pól danych w konstruktorze instancji, gdyż formatery nie wywołują konstruktorów w trakcie deserializacji obiektów. Najczęstszym rozwiązaniem tego problemu jest implementacja wzorca „Lazy Initialization" (opóźnionej inicjacji), w którym ryp tworzy lub odczytuje dane za pierwszym razem, gdy są one potrzebne. Poniższy kod przedstawia zmodyfikowaną wersję klasy Emp/.oyee z atrybutem NonSerialiud­ Attribute zastosowanym do pola addres,s co oznacza, że formater nie będzie serializował wartości tego poufnego pola. Klasa Emp/,oyee implementuje właściwości public, aby umożliwić dostęp do każdego pola danych prywatnych, zapewniając wygodne miejsce dla implementacji opóź­ nionej inicjacji pola address. using System ; [ Se rializable ) public class Employee { p rivate st ring name ; p rivate int age ; [ NonSerialized ] p rivate st ring add ress ;

li P rosty konst rukt o r Employee public Employee ( st ring name , int age , st ring add res s ) { thi s . name = name ; t h i s . age = age ; this . add ress = add ress ; }

li Publiczna właściwość zapewniaj ąca dostęp do nazwiska p racownika public st ring Name { get { ret u rn name ; } set { name = value ; } }

li Publiczna właściwość zapewn iaj ąca dostęp do wieku p racownika public int Age { get { ret u rn age ; }

466

C#

-

Księga przykładów set { age = value ; } }

li Publiczna właściwo ść zapewn iaj ąca dostęp do a d resu p racown ika . li Wyko rzystuj e opóźn ioną inicj acj ę w celu określania ad resu , gdyż obiekt li zdeserializowany n ie zawiera tej wa rtości . public st ring Add ress { get { if ( add ress

==

nul l ) {

li Ładowanie a d resu z t rwałego magazynu ; } ret u rn add res s ; } set { address

val ue ;

} } }

SerializableAttribute i NonSe­ rializedAttribute zapewnia dostateczne wsparcie serializacj i . Jeżeli wymagany jest ściślejszy nad­ zór nad procesem serializacji, można zaimplementować interfejs !Serializable. Klasy formatera Dla większości niestandardowych typów zastosowanie atrybutów

używają odmiennej logiki przy serializacji i deserializacji instancji typów, które implementują

!Serializable. W celu prawidłowej

implementacji

• Zadeklarować, że dany typ implementuje • Zastosować atrybut

!Serializable należy:

!Serializable.

SerializableAttribute do danej deklaracji typu, jak to wyżej opisano; nie NonSerializedAttribute, gdyż nie da to żadnego efektu. metodę !Serializable. GetObjectData (używaną podczas seriałizacji),

należy stosować atrybutu • Zaimplementować

która korzysta z następujących typów argumentów: O O

System.Runtime.Serialization.Serializationlnfo System.Runtime. Serialization. StreamingContext

• Zaimplementować niepubliczny konstruktor (używany podczas deserializacji), akceptujący

te same argumenty, co metoda

GetObjectData.

Należy pamiętać, że jeśli planuje się wypro­

wadzenie klas z klasy serializowalnej, trzeba użyć konstruktora chronionego

(protected}.

• Przy tworzeniu klasy seriałizowalnej z klasy podstawowej, która również implementuje

!Serializable,

metoda

GetObjectData

typu użytkownika i konstruktor deseriałizacji muszą

wywoływać równoważną metodę i konstruktora w klasie nadrzędnej .

GetObjectData i przekazuje do niej odniesie­ Serializationlnfo i StreamingContext jako argumenty. Typ projektanta musi wypełnić obiekt Serializationlnfo danymi do serializowania. Klasa Serializationlnfo udostępnia metodę Ada'Va­ /ue, służącą do dodawania kolejnych elementów danych. Przy każdym wywołaniu Ada'Value Podczas seriałizacji formater wywołuje metodę

nia

należy podać nazwę porcji danych - nazwa ta jest używana podczas deserializacji do odczyty-

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 467

AddValue ma 1 6 przeciążonych odmian i umożliwia dodanie różnych Seria/imtionlnfo. Obiekt StreamingContext dostarcza. informacji o celu i przeznaczeniu serializowanych danych

wania danych. Metoda

typów danych do obiektu

oraz wybór, które z nich mają być serialiwwane. Na przykład można bez obaw serialiwwać dane poufne, jeżeli są one przeznacwne dla innej domeny aplikacji tego samego procesu, ale nie wtedy, gdy dane będą zapisywane do pliku. Kiedy formater deserializuje instancję danego typu, wywołuje konstruktora deserializacj i,

StreamingContext jako argumenty. Typ Seria/imtionlnfo, używając jednej z metod Seria/imtionlnfo. Get* na przykład GetString, Get!nt32 lub GetBoolean. Podczas dese­ rializacji obiekt StreamingContext zapewnia i nformacje o źródle serializowalnych danych, umoż­ ponownie przekazując odniesienia

Seria/izationlnfo

i

projektanta musi wydobyć zserializowane dane z obiektu -

liwiając odwrócenie logiki użytej podczas serializacji.

Uwaga Podczas standardowych operacji serializacji formatery używają właściwości obiektu StreamingContext do dostarczenia szczegółowych informacji dotyczących źródła, przeznaczenia i powodu serializowania danych. Jednakże w wypadku wykonywania dostosowanej serializacji kod projektanta może skonfigurować obiekt formatera Streaming­ Context przed rozpoczęciem serializacji i deserializacji. Więcej szczegółów na temat klasy StreamingContext zawiera dokumentacja .NET Framework SOK.

Poniższy przykład pokazuje zmodyfikowaną wersję klasy Emp/.oyee, implememujacą interfejs !Seria/imble. W rej wersji klasa Empl.oyee nie serializuje pola a.ddress, j eżeli dostarczony obiekt StreamingContext określa, że miejscem przeznaczenia serializowanych danych jest plik. Pełen kod dla tego przykładu jest zawarty w pliku SerializableExample.cs, łącznie z metodą główną

(Main),

która demonstruje serializację i deserializację obiektu

Employee.

using System ; using System . Runtime . Se rializat ion ; [ Se ria l i z ab le J public class Employee : ISe rializable { p rivate st ring name ; p rivate int age ; p rivate st ring add ress ;

li P rosty kon st rukt o r Employee public Employee ( st ring name , int age , st ring add res s ) { this . name

=

name ;

this . age = age ; this . address = add res s ; }

li Konst rukt o r wymaga wł ączenia fo rmat e ra w celu zdese rial izowania obiektu li Employee . Należy zadekla rować konst ru kto r j ako p rywatny lub p rzynaj mniej li c h ronion y , aby zagwa rantowa ć , że nie będz ie wywoływan y niepot rzebnie . p rivate Employee ( Se rial ization ! n fo info , St reamingContext contex t ) {

468

C#

-

Księga przykładów li Wydobył nazwi s ko i wiek p racownika , któ re to dane są z awsze obecne li w danych se rializowanych bez względu na wa rtoś ć St reamingContext . name = info . GetSt ring ( " Name " ) ; age = info . Getint3 2 ( " Age " ) ;

li P róba wydobycia ad resu p racownika i odebranie kodu błędu , j eś l i j est li niedostępny . t ry { add res s = info . GetSt ring ( " Add res s " ) ; } catch ( Se rializationException ) { add res s = null ; } }

li Właściwości Name , Age i Add ress nie są wyświetlane . li Zadekla rowana w interfej s ie ISerializable metoda GetObj ectData li udostępnia mechanizm, dzięki któ remu f o rmate r u z y s ku j e dane obiekt u , li któ re powinny został zse rial izowane public void GetObj ectData ( Se rializationi n fo i n f , St reamingContext con ) {

li Zawsze s e rializuj nazwisko i wiek p racownika . in f . AddValue ( " Name " , name ) ; inf . AddVal u e ( "Age" , age ) ;

li Nie se rializuj ad resu , j eżeli St reamingContext wskazuj e , że dane li se rializowane będą zapisywane w pliku . if ( ( con . State & St reamingContextStates . F i l e )

--

0) {

inf . AddValue ( "Add res s " , add res s ) ; } } }

1 6.2 Implementacja typu klonowalnego Problem Chcesz utworzyć typ niestandardowy, który udostępnia prosty mechanizm programowego two­ rzenia kopii instancji typu.

Rozwiązanie Zaimplementuj interfejs

System.!Cioneable.

Rozdział

1 6:

Powszechnie używane interfejsy i wzorce 469

Omówienie Gdy jeden typ wartości zostaje przypisany do innego, tworzona jest kopia tej wartości. Mię­ dzy tymi dwoma wartościami nie ma połączenia - zmiana jednej nie wpływa na drugą. Jeżeli jednak przypisze się jeden typ odniesienia do innego (wyłączając łańcuchy, które są trakto­ wane specjalnie przez moduł runtime), nie jest tworzona nowa kopia typu odniesienia. Obydwa typy odniesienia są związane z cym samym obiektem i zmiany jego wartości są odzwierciedlane

w obu odniesieniach. Aby utworzyć prawdziwą kopię typu odniesienia, należy sklonować (clone) wskazywany przezeń obiekt. Interfejs

!Cloneabfe

Clone System.

identyfikuje typ jako dający się sklonować i deklaruje metodę

jako mechanizm tworzenia klonu obiektu. Metoda

Clone nie

ma argumentów i zwraca

Object, niezależnie od implementowanego typu. Oznacza to, że po sklonowaniu obiektu należy jawnie przekształcić klon do właściwego typu. Sposób implementacji metody

Clone

dla typu niestandardowego zależy od pól danych

zadeklarowanych w cym typie. Jeżeli typ niestandardowy zawiera tylko wartości i pola danych

(int, byte itd.) System.String, można zaimplementować metodę Clone, tworząc instancję nowego

obiektu i ustawiając jego pola danych na te same wartości, które zawierał oryginalny obiekt.

Object (z której

Klasa

lone,

wywodzą się wszystkie typy) zawiera chronioną metodę

która automatyzuje ten proces. Oto przykład prostej klasy

Empfoyee,

MemberwiseC­

zawierającej tylko

pola string. Z tego względu metoda Clone, tworząca klony opiera się na odziedziczonej metodzie MemberwiseClone.

using System ; public class Employee : ICloneable { public st ring Name ; public st ring Title ;

li P rosty konst rukto r Employee public Employee ( st ring name , st ring t i t l e ) { Name = name ; Title

=

title ;

}

li Two rzenie klonu metodą Obj ect . Membe rwiseClone , gdyż klasa Employee li zawiera tylko odnośniki łańcuchowe public obj ect Clone ( ) { retu rn Membe rwiseClone ( ) ; } } Jeżeli typ niestandardowy zawiera pola danych typu odniesienia, projektant musi zdecydować, czy metoda

Clone ma wykonywać shallow copy (płytką kopię) czy też deep copy (głęboką kopię).

Pierwsze oznacza, że wszystkie poła odniesień w klonie będą się odwoływać do tych samych obiektów, co ich odpowiedniki w oryginalnym obiekcie. Głęboka kopia oznacza, że trzeba utworzyć kopie (klony) pełnego grafu obiektu, tak by pola danych typu odniesienia w klonie odwoływały się do fizycznie niezależnych kopii (klonów) obiektów, do których odwoływał się oryginalny obiekt.

470

C# - Księga przykładów Płytką kopię można łatwo zaimplementować używając opisanej wcześniej metody

berwiseClone.

Mem­

Jednakże to głęboka kopia jest tym, czego programiści zazwyczaj oczekują przy

klonowaniu obiektu, chociaż rzadko to uzyskują. Dotyczy to zwłaszcza klas kolekcji z prze­

System. Colkctions, z których wszystkie implementują płytkie kopie w swoich Clone. Wprawdzie CLęsto bardziej użyteCLna byłaby implementacja głębokich kopii,

strzeni nazw metodach

iscnieją co najmniej dwa ważne powody, dla których typy (zwłaszcza klasy kolekcji ogólnych) nie implementują głębokich kopii: • Utworzenie klonu grafu dużego obiektu oznacza zarówno intensywne wykorzystanie proce­

sora, jak i pamięci. • Kolekcje ogólne mogą zawierać szerokie i głębokie grafy obiektów, składające się z dowol­

nego typu obiektów. Nie jest możliwa implementacja głębokiej kopii, zapewniająca utrzy­ manie tej różnorodności, gdyż niektóre obiekty w kolekcji mogą nie być klonowałne, a inne mogą zawierać wzajemne odniesienia, które mogą wprowadzić proces klonowania w nie­ skońCLOną pętlę.

Wskazówka Aby sklonować obiekt, który nie implementuje /Cloneable, ale jest seria­ lizowalny, można go serializować, a następnie deserializować, aby osiągnąć ten sam rezultat. Jednakże należy zwrócić uwagę na fakt, ze proces serializacji może pomijać niektóre pola danych Oak to przedstawia przepis 1 6. 1 ). Analogicznie, jeśli tworzony jest serializowalny typ niestandardowy, można potencjalnie użyć dopiero co opisanego pro­ cesu serializacji, aby wykonać głęboką kopię w obrębie implementacji metody /C/one­ able. Clone projektanta. Aby sklonować obiekt serializowalny, należy użyć klasy System. Runtime.Seria/ization. Formatters.Binary. BinaryFormatter i zserializować go do System. 10.MemoryStream, a następnie zdeserializować otrzymany obiekt.

Dla dobrze wytypowanych kolekcji, dla których natura zawartych elementów jest zrozumiała i kontrolowana, głęboka kopia może stanowić użyteCLną właściwość; na przykład

XmlNode implementuje głęboką kopię w metodzie Clone.

System.XmL

Pozwala to na wykreowanie prawdzi­

wych kopii całych hierarchii obiektów XML za pomocą pojedynczej instrukcji.

Clone, która wykonuje Employee, reprezentujących zespół pra­ cowników. Przy wywołaniu metody Clone obiektu Team utworzony zostanie klon każdego zawartego w nim obiektu Employee i dodany do sklonownego obiektu Team. Klasa Team dostar­ cza konstruktora private, aby uprościć kod w metodzie Clone - użycie konstruktorów jest czę­ Pokazana w przykładzie klasa

głęboką kopię. Klasa

Team

Team

zawiera implementację metody

zawiera kolekcję obiektów

sto spotykanym rozwiązaniem, uprasZCLającym proces klonowania. Plik CloneableExample.cs, należący do przykładowego kodu dla tego rozdziału, zawiera klasy

Team i Employee.

Plik ten

również dostarCLa metodę główną Main, która demonstruje skutek wykonania głębokiej kopii.

u sing System ; u sing System . Collections ; public class Team : ICloneable { public A r raylist TeamMembe rs public Team ( ) {

new A r raylist ( ) ;

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 471 }

li P rywatny kons t rukto r wywołany p rzez metodę Clone w celu utwo rzenia li nowego obiektu Team i wypełnienie j ego właściwości A r rayList klonami li obiektów Employee p rivate Team ( A r rayList membe r s ) { foreach ( Employee e in membe rs ) { TeamMembers . Add ( e . Clone ( ) ) ; } }

li Dodanie obiektu Employee do Team public void AddMembe r ( Employee membe r ) { TeamMembe rs . Add ( membe r ) ; } public obj ect Clone ( ) {

li Two rzenie głębokiej kopii zespołu poprzez wywołanie p rywatnego li kon st rukt o ra Team i p rzekazanie tablicy zawie raj ącej członków li zespołu retu rn new Team ( t h i s . TeamMembe rs ) ;

li Poniższe polecenie utwo rzy płyt ką kopię zespołu retu rn Membe rwiseClone ( ) ; } }

1 &.3 Implementacja typów porównywalnych Problem Chcesz zastosować mechanizm, który umożliwia porównanie typów niestandardowych, pozwa­ lając na łarwe porządkowanie kolekcji instancji tych typów.

Rozwiązanie Aby typ udostępniał standardowy mechanizm porównywania, należy zaimplementować inter­

System.!Comparab/e. Jeżeli konieczne jest porównywanie typu w oparciu o więcej niż jedną System. Coilections. !Comparer. fejs

charakterystykę, należy urworzyć oddzielne typy i mplementujące interfejs

472

C#

-

Księga przykładów

Omówienie Jeżeli zachodzi potrzeba posortowania typu według prostego porządku, jak na przykład rosnący numer ID albo alfaberycznie w oparciu o nazwisko, należy zaimplementować interfejs !Compa­ rabl.e. Interfejs !Comparabl.e definiuje pojedynczą metodę o nazwie CompareTo: int CompareTo ( obj ect obj ) ;

Obiekt (obj) przekazany do metody musi być obiektem tego samego rypu, co obiekt wywo­ ływany; w innym przypadku Compare To wywoła wyjątek System.ArgumentException. Wartość zwrócona przez CompareTo jest wyliczana w następujący sposób: •

Jeżeli bieżący obiekt jest mniejszy niż obj, zwraca wartość mniejszą niż zero (na przykład - 1 ).



Jeżeli bieżący obiekt ma tę samą wartość co obj, zwraca wartość zero.



Jeżeli bieżący obiekt jest większy niż obj, zwraca wartość większą niż zero (na przykład 1 ).

Znaczenie rych porównań zależy od implementacji interfejsu !Comparabl.e. Jeżeli na przykład sortowanie doryczyło nazwisk łudzi, można wykonać porównanie String. Jednakże gdy porzą­ dek ma wstać ustalony według dat urodzenia, należy wykonać porównanie obiektów System.

DateTime. Aby umożliwić różnorodność sposobów sortowania dla poszczególnych typów, należy zasto­ sować oddzielne rypy pomocnicze, które implementują interfejs !Comparer, definiujący metodę Compare przedstawioną niżej . int Compa re ( obj ect x , obj ect y ) ;

Te pomocnicze rypy muszą zawierać odpowiednią logikę do porównania dwóch obiektów i zwracać wartość w oparciu o następujące reguły: •

Jeżeli

x

jest mniejsze niż y, należy zwrócić wartość mniejszą niż zero (na przykład - I ).



Jeżeli

x

ma taką samą wartość jak y, należy zwrócić wartość zero.



Jeżeli x jest większe niż y, należy zwrócić wartość większą niż zero (na przykład 1 ).

Klasa Newspaper przedstawiona niżej demonstruje implementację interfejsów !Comparabl.e i !Comparers. Metoda Newspaper. CompareTo wykonuje porównanie obiektów Newspaper w oparciu o pola name, bez rozróżniania wielkości liter. Prywatna zagnieżdżona klasa o nazwie AscendingCirculationComparer implementuje !Comparer i porównuje dwa obiekry Newspaper w oparciu o ich poła circulation (nakład). Obiekt AscendingCirculationComparer jest otrzymy­ wany przy użyciu starycznej właściwości Newspaper. CirculationSorter. u s ing System ; u s ing System . Collection s ; public class Newspape r : ICompa rable { p rivate st ring name ; p rivate int ci rculation ; p rivate class AscendingCi rculationCompa re r : ICompa re r { int ICompa re r . Compa re ( obj ect x , obj ect y ) {

li Logika obsługi odsyłacza nul l , zgodnie z zaleceniami li interfej su ICompa re r . Null j est u z nawany za mniej niż

Rozdział 1 6: Powszechnie używane interfejsy i wzorce

473

li j akakolwiek inna wa rtość . if ( x == null && y == nul l ) retu rn 0 ; else if ( x == nul l ) ret u rn - 1 ; else i f ( y == n u l l ) ret u rn 1 ;

li S k rócony wa runek , gdy x i y odwołuj ą s ię do tego samego obiektu if (x == y) ret u rn 0 ;

li Sp rawdzenie , że za równo x , j ak i y s ą instan c j ami Newspaper Newspape r newspape rX = x a s Newspape r ; i f ( newspape rX = = null ) { th row new A rgumentException ( " Invalid object type " , " x " ) ; } News pape r newspaperY = y as Newspape r ; if ( newspape rY = = n u l l ) { t h row new A rg umen t Exception ( " I nvalid obj ect type " , " y " ) ; }

li li li li li li

Porównanie wielkości nakładów . ICompa re r określa , że : zwraca mniej niż z e ro , j eżeli x

<

y

zwraca z e ro , j eżeli x = y zwraca więcej n1z z e ro , j eżeli x > y Logikę taką można łatwo zaimplementować p rzy użyciu a rytmetyki l i c z b całkowitych .

retu rn newspape rX . ci rculation - newspape rY . ci rculat ion ; } } public Newspape r ( st ring name , int c i rculation ) { thi s . name = name ; this . c i rculation = c i rculation ; }

li Zadekla rowanie właściwości t y l ko do odczyt u , zwracaj ącej instancj ę li AscendingCi rculationCompa re r . public static I Compa rer C i rculationSo rte r{ get { ret u rn new AscendingCi rc u lationCompa re r ( ) ; } } public ove r ride st ring ToSt ring ( ) { retu rn st ring . Format ( " { 0 } : C i rc ulation = { 1 } " , name , ci rculat ion ) ; }

li Metoda Compa reTo po równ u j e dwa obiekty Newspape r ze względu na ich nazwy li ( bez uwzględn iania wielkości l ite r ) . public int Compa reTo ( obj ect obj ) {

li ICompa rable określa , że obiekt j es t zawsz e uważany za większy niż li nul l . if ( obj == n u l l ) ret u rn 1 ;

474

C#

-

Księga przykładów li Obej ście p rzypadku , gdy d rugi obiekt j est odsyłaczem do tego li obiekt u . i f { obj == this ) ret u rn 0 ;

l i P róba rzutowania innego obiektu do instancj i Newspape r . Newspape r othe r = obj a s Newspape r ;

li Jeżeli "other" j est pusty , nie może byt instan c j ą Newspape r . l i ICompa rable o k reśla , że Compa reTo musi w tej s ytuacj i wywołat li wyj ątek System . A rg umentExcept ion . if { ot h e r == null ) { th row new A rg umentException { " Invalid obj ect type " , " o bj " ) ; } else {

li li li li

Wyl iczenie wa rtośc i zwrot nej pop rzez wykonanie po równania nazw Newspape r . Ponieważ nazwa j es t łańcuc hem , naj łatwiej s zym rozwiązaniem j est posłużenie się możliwościami po równania klasy St ring , które wykonuj ą po równywania zależne od kultu ry .

retu rn s t ring . Compare { t his . name , othe r . name , t rue ) ; } } } Metoda główna

(Main)

przedstawiona niżej demonstruje możliwości porównywania i sorto­

/Comparable i /Comparer. Ta metoda two­ System. CoUections.ArrayList, zawierającą pięć obiektów Newspaper. Metoda główna następnie dwukrotnie sortuje ArrayList przy użyciu metody ArrayList.Sort. Pierwsza operacja Sort używa domyślnego mechanizmu porównania Newspaper, dostarczonego przez metodę /Compamble. Compare To. Druga operacja Sort do wykonania porównań wykorzystuje obiekt AscendingCirculationComparer poprzez jego implementację metody /Comparer. Compare. wania, uzyskane dzięki implementacji interfejsów rzy kolekcję

public static void Main { ) { A rraylist newspape rs = new A r raylist ( ) ; newspape rs . Add { new Newspape r ( "The Echo " , 125780 ) ) ; news pa pe rs . Add ( new News pape r ( " The Times " , 55230 ) ) ; newspape rs . Add ( new Newspape r ( "The Gazette " , 235950 ) ) ; newspape rs . Add ( new Newspape r ( " The Sun " , 88760 ) ) ; newspapers . Add ( new Newspape r ( "The He ral d " , 5670 ) ) ; Consol e . W riteline ( " U n so rted newspaper l i st : " ) ; foreach ( Newspape r n in newspape rs ) { Console . W riteline ( n ) ; } Consol e . Writeline ( Envi ronment . Newline ) ; Consol e . Writeline ( " Newspape r l i s t so rted by name ( default o rd e r ) : " ) ; newspapers . So rt ( ) ; foreach ( Newspaper n in newspape rs ) { Console . Writeline ( n ) ; } Consol e . Writeline { En v i ronment . Newline ) ;

Rozdział 1 6: Powszechnie używane interfejsy i wzorce

475

Consol e . W riteline ( " Newspape r l i s t so rted by ci rculation : " ) ; newspape rs . So rt ( Newspape r . Ci rculat ionSo rte r ) ; fo reach ( Newspaper n in newspape rs ) { Console . W riteline ( n ) ; } } Wykonanie metody głównej

Main spowoduje uzyskanie pokazanych niżej wyników:

Unso rted newspaper l i s t : The Echo : C i rculation = 125780 The Times : C i rculat ion = 55230 The Gazette : C i rculation = 235950 The Sun : C i rculation = 88760 The Herald : C i rculation = 5679 News pape r l i s t so rted by name ( default o rde r ) : The Echo : Ci rculation = 125780 The Gazette : Ci rculation = 235959 The Herald : C i rculation = 5670 The Sun : C i rculation

88760

=

The Times : C i rculation = 55230 Newspaper list so rted by c i rculation : The He rald : C i rculation = 5670 The Times : C i rculat ion = 55230 The Sun : C i rculation = 88760 The Echo : Ci rculation = 125780 The Gazette : Ci rculation = 235950

1 6.4 Implementacja typu wyliczalnego Problem Chcesz utworzyć typ kolekcji, którego zawartość będzie można wyliczyć przy użyciu instrukcji

fareach.

Rozwiązanie System.!Enumerable we własnym typie kolekcji. Metoda GetEnume­ rator interfejsu !Enumerable zwraca enumerator obiekt, który implementuje interfejs System. !Enumerator. Interfejs !Enumerator definiuje metody używane do wyliczania elementów kolek­ cji przez i n str ukcję fareach

Zaimplementuj interfejs

-

.

476

C#

-

Księga przykładów

Omówienie lndeksator numeryczny umożliwia iteracje; elementów kolekcji przy użyciu pętli for. Jednakże ca technika nie zapewnia odpowiedniego poziomu abstrakcji dla nieliniowych struktur danych, takich jak drzewa lub kolekcje wielowymiarowe. lnstrukcja foreach dostarcza łatwego w użyciu i eleganckiego składniowo mechanizmu iteracji kolekcji obiektów, niezależnie od jej wewnętrz­ nej scrukcury. Aby zapewnić wsparcie dla składni foreach, obiekt zawierający kolekcje; obiektów musi imple­

System.!Enumerable. Interfejs !Enumerable deklaruje pojedynczą metodę GetEnumerator, która nie wymaga argumentów i zwraca obiekt, implementujący System.!Enumerator, jak przedstawiono niżej. mentować interfejs

o nazwie

IEnume rato r GetEnume rato r ( ) ; I nstancja

!Enumerator,

GetEnumerator, jest obiektem obsługującym wyliczanie !Enumerator udostępnia kursor o właściwościach „tylko

zwrócona przez

elementów zbioru danych. Interfejs

do odczytu" i „tylko do przodu", zapewniający dostęp do pól kolekcji niższej warstwy. Tabela

1 6- 1 zawiera omówienie elementów interfejsu !Enumerator. Tabela 1 6-1

Elementy interfejsu /Enumerator

Element

Opis

Current

Właściwość zwracająca bieżący element danych. Po utworzeniu numeratora

Current

wskazuje pozycje; poprzedzającą pierwszy element danych. Oznacza

Current należy wywołać MoveNext. Jeżeli w momencie Current numerator jest ustawiony przed pierwszym albo po ostatnim elemencie w zbiorze danych, Current wywoła wyjątek System.lnvalidOperationE­ xception. to, że przed użyciem

wywołania

MoveNext

Metoda przesuwająca numerator do następnego elementu danych w kolekcji. Zwraca wartość zwraca wartość

true, false.

jeżeli elementów jest więcej; w przeciwnym przypadku Jeżeli znajdujące sic; w niższej warstwie źródło danych

ulegnie zmianie podczas pracy numeratora,

MoveNext wywoła wyjątek lnvalid­

OperationException. Reset

Metoda przesuwająca numerator do pozycji poprzedzającej pierwszy element danych w kolekcji. Jeżeli znajdujące sic; w niższej warstwie źródło danych zmieni sic; podczas pracy numeratora,

Reset wywoła wyjątek lnvalidOperationException.

TeamMember, Team i TeamMemberEnumerator demonstrują implementacje i nterfej­ !Enumerable i !Enumerator. Klasa TeamMember (wyliscowana niżej) reprezentuje członka

Klasy sów

zespołu.

li Klasa TeamMembe r rep rezent u j e indywidualnego członka zespoł u . public class TeamMember { public st ring Name ; public st ring Title ;

li P ro sty kon s t ruktor TeamMembe r . public TeamMembe r ( st ring name , s t ring title) {

Rozdział 1 6: Powszechnie używane interfejsy i wzorce Name

=

477

name ;

Titl e = title ; }

li Zwrócenie rep rezenacj i TeamMembe r w postaci łańcucha . public ove r ride st ring ToSt ring ( ) { ret u rn st ring . Fo rmat ( " { 0 } ( { 1} ) " , Name , Title ) ; } } Klasa

Team, która

mentuje interfejs

reprezentuje zespół ludzi, jest kolekcją obiektów

!Enumerable

i deklaruje oddzielną klasę o

TeamMember. Team imple­ TeamMemberEnumerator,

nazw ie

umożliwiającą wyliczanie. Często klasy kolekcji implementują bezpośrednio oba te interfejsy, zarówno

!Enumerable,

jak i

!Enumerator.

Jednakże użycie oddzielnych klas numeratora jest

znacznie prostszym podejściem, umożliwiającym użycie wielu numeratorów (i wielu wątków) do współbieżnego wyliczania elementów

Team

Team.

implementuje wzorzec obserwatora

(Observer Pattern) przy użyciu delegata oraz TeamMemberEnumerator o zmianach

pól zdarzeń do powiadamiania wszystkich obiektów

Team (szczegółowy opis wzorca obserwatora zawiera przepis 1 6. 1 O) . TeamMemberEnumerator jest prywatną zagnieżdżoną klasą, nie można zatem utworzyć jej instancji inaczej, niż poprzez metodę Team. GetEnumerator. Oto kod klas Team i TeamMem­ berEnumerator. zachodzących w kolekcji Klasa

li Klasa Team rep rezentu j e kolekc j ę obiektów TeamMembe r . Implementu j e inte rfej s li IEnumerable w celu umożliwienia wyl iczania obiektów TeamMembe r . public class Team : I Enume ra ble {

li li li li

TeamMembe rEnume rat o r j est p rywatną klasą zagn ieżdżoną , udostępniaj ącą funkcj onalność wyl ic zania obiektów TeamMembe rs zawa rtych w kolekcj i Team . J a ko klasa zagnieżd żona , TeamMemberEnume rator ma dostęp do p rywatnych pól klasy Team .

p rivate class TeamMemberEnume rato r : I E n ume rat o r {

li Team wyl iczany p rzez ten obiekt . p rivate Team sou rceTeam ;

li Wa rtość boolowska wskaz u j ąca , czy badana kolekcj a Team u legła li zmianie i w związku z tym j est niep rawidłowym ź ródłem dla dal szego li wyl iczania . p rivate bool teaml nvalid = fals e ;

l i Wa rtość cał kowita identyfiku j ąca bieżący obiekt TeamMembe r . Stanowi li indeks obiektów TeamMembe r w tablicy używanej p rzez zbiór Team . li Wstępna wa rtość to - 1 , o j eden mniej niż dla pierwszego elementu . p rivate int c u r rentMember

=

-1;

l i Kon s t ruktor p rzej muj e odsyłacz do Team , któ ry j es t ź ródłem dla li wyliczanych danych . inte rnal TeamMemberEnume rat o r ( Team team) { thi s . sou rceTeam = team ;

478

C#

-

Księga przykładów

li

Rej est rowanie sou rceTeam w celu wyk rywania zmian

sou rceleam .leamChange += new TeamChangedEventH_and l e r ( this . TeamChange ) ; }

li

Implementa c j a właściwości I Enume rato r . C u r rent .

public obj ect C u r rent { get {

li li

Jeżeli TeamMembe rEnume rator j est umies z czony p rzed pie r.1szym lub po ostatnim elemencie , wywoływany j est wyj ąte k .

if ( cu r rentMember = = - 1 I I cu rrentMember > ( sou rceTeam . teamMembe r s . Count - 1 ) ) { th row new I nval idOpe rationException ( ) ; }

li

W p rzeciwnym p rzypadku zwracany j es t aktualny obiekt

11 TeamMembe r

ret u rn sou rceTeam . teamMembe rs [ cu r rentMembe r ] ; } }

li

Implementacj a metody IEnumerat o r . MoveNext .

public bool MoveNext ( ) {

li

Jeżeli Team j est niep rawidłowy , wywołanie wyj ątku

if ( teami nval id ) { th row new InvalidOpe rat ionExcept ion ( " Team modified " ) ; }

li

P rzej ście do kolej nego TeamMembe r

cu rrentMembe r++ ;

li li

Zwrócenie fal se , j eżeli p rzej ście nastąpiło za ostatni obiekt TeamMembe r

if ( cu r rentMembe r

>

( sou rceTeam . teamMembe rs . Count - 1 ) ) {

ret u rn false ; else { } retu rn t rue ; } }

li li

Implementac j a metody I Enume rat o r . Reset . Powodu j e ona z resetowanie pozycj i TeamMembe rEnume rato r n a początek z bi o ru Team .

public void Reset ( ) {

li

Jeżeli Team j est n iep rawidłowy , wywołanie wyj ątku

if ( teaminvalid ) { t h row new I nvalidOperationException ( "Team modified " ) ; }

Rozdział 1 6: Powszechnie używane interfejsy i wzorce

479

l i P rzes unięcie wskaźnika c u r rentMembe r wstecz do pozycj i li pop rzedzaj ącej pierwszy element . c u r rentMembe r = - 1 ; }

l i Obsł uga zda rzeń d o p rzechwytywania powiadomień , ż e zbió r li Team uległ zmianie . inte rnal void TeamChange ( Team t , EventArgs e ) {

li Zasygnalizowanie , że zbiór Team j est nieważny teamlnval id = t ru e ; } }

li Delegat określaj ący sygnat u rę , któ rą muszą implementować wszystkie li metody obsługi zdarzeń zmian zbio ru . public delegate void TeamChangedEventHandle r ( Team t , EventArgs e ) ;

li Tablica zawie raj ąca obiekty TeamMembe r p rivate A r rayList teamMembers ;

li Zda rzenie służące do powiadomienia TeamMemberEnume rat o r s o zmianie li z bioru Team . public event TeamChangedEventHandler TeamChange ;

li Kon s t rukto r Team public Team ( ) { teamMembe rs = new A r rayList ( ) ; }

li Implementacj a metody IEnume rabl e . GetE numerat o r . public IEnume rat o r GetE nume rato r ( ) { retu rn new TeamMembe rEnume rato r ( t h is ) ; }

li Dodanie obiektu TeamMembe r do Team public void AddMembe r ( TeamMembe r membe r ) { teamMembe rs . Add ( membe r ) ;

li Powiadomienie o zmianie l i st y if ( TeamChange ! = nul l ) { TeamChange ( th i s , null ) ; } } }

Jeżeli klasa kolekcji zawiera różne typy danych, które zamierzamy wyliczać oddzielnie, nie wystarczy implementacja i nterfejsu !Enumerable w samej klasie kolekcji. W takim wypadku należy zaimplementować pewną ilość właściwości zwracających różne instancje IEnumerator. Na przykład, jeżeli klasa Team reprezentuje zarówno członków zespołu, jak i sprzęt przypisany

480

C#

Księga przykładów

-

do tego zespołu, można zaimplementować właściwości podobne do przedstawionych w przy­ kładzie poniżej.

li Właściwość pozwalaj ąca na wyli czanie c złonków zespołu public IEnumerato r Membe rs { get { ret u rn new TeamMembe rEnume rato r ( t h is ) ; } }

li Właściwość pozwalaj ąca na wyl iczanie kompute rów zespołu public IEnumerato r Computers { get { ret u rn new TeamComputerEnumerato r ( t hi s ) ; } } Aby wykorzystać różne numeratory, można użyć następującego kodu:

Team team = new Team ( ) ; fo reach ( TeamMember in team . Membe r s ) {

II

j a kiś kod . .

} foreach ( TeamComput e r in team . Compute r s ) {

II

j a kiś kod . .

}

Uwaga Instrukcja foreach również dostarcza typów, które implementują odpowiednik wzorca dla interfejsów zdefiniowanych przez /Enumerable i /Enumerator, nawet jeśli ten typ nie implementuje interfejsów. Jednakże kod projektanta będzie bardziej przejrzysty i zrozumiały po implementacji interfejsu /Enumerable. Więcej szczegółów na temat uwa­ runkowań instrukcji foreach zawiera specyfikacja języka C# (http:/lmsdn.microsoft.coml netlecma;).

1 6.5 Implementacja klasy jednorazowego użycia Problem Chcesz urworzyć klasę, która odnosi się do niezarządzanych zasobów i udostępnia użytkowni­ kom mechanizm deterministycznego zwalniania tych zasobów.

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 481

Rozwiązanie System.!Disposable !Disposable.Dispose.

Zaimplementuj interfejs wywoła metodę

i zwolnij niezarządzane zasoby, gdy kod klienta

Omówienie Obiekt, do którego nie ma już odniesienia, w dalszym ciągu istnieje na stercie i zużywa zasoby, dopóki mechanizm sprzątający

(garbage coUector)

go nie zwolni i nie zwróci zasobów. Mecha­

nizm sprzątający automatycznie zwalnia zasoby zarządzane (takie jak pamięć), ale nie uwalnia niezarządzanych zasobów (takich jak uchwyty plików czy połączenia baz danych), do których mają odniesienia liczne obiekty. Jeżeli obiekt zawiera pola danych, które mają odniesienia do niezarządzanych zasobów, musi jawnie uwolnić te zasoby. Jednym z rozwiązań jest zadeklarowanie destruktora - lub finalizatora - dla danej klasy.

Destruktor jest występującym

w języku C# ekwiwalentem bardziej uniwersalnego finalizatora,

stosowanego w .NET. W celu zwrócenia pamięci użytkowanej przez instancję klasy mechanizm sprzątający wywoła finalizator obiektu. Finalizator może przedsięwziąć nie'lbędne kroki w celu zwolnienia nie'lafządzanych zasobów. Ze względu na fakt, że mechanizm sprzątający używa pojedynczego wątku do wykonywania wszystkich finalizatorów, stosowanie ich może mieć negatywny wpływ na wydajność procesu sprzątania pamięci,

co

z kolei obniży ogólną wydaj­

ność aplikacji. Ponadto nie można kontrolować momentu, w którym moduł runcime uwolni niezarządzane zasoby, ponieważ nie można be'lpośrednio wywołać finalizatora obiektu, a wpływ na aktywność mechanizmu sprzątającego, zapewniany pCZe'l klasę

System. GC, jest ograniczony.

Alternatywą dla użycia finalizatorów jest zdefiniowanie w .NET Framework wzorca zwal­ niania

(Dispose pattern), jako narzędzia zapewniającego deterministyczną kontrolę nad momen­

tem uwalniania niezarządzanych zasobów pCZe'l moduł runtime. Aby zaimplementować wzór

!Disposable, deklarujący pojedynczą metodę Dispose. W metodzie Dispose należy umieścić kod nie'lbędny do zwalniania niezarzą­

zwalniania, klasa musi implementować interfejs o nazwie

dzanych zasobów. Instancje klas, które implementują wzór zwalniania, noszą nazwę obiektów jednorazowego użycia

(dżsposable objects). Jeśli kod kończy się obiektem tego typu, wywołuje on metodę Dispose

obiektu, aby uwolnić nie'larządzane zasoby, nadal powstawiając mechanizmowi sprzątającemu zadanie zwalniania zasobów zarządzanych. Ważne jest zrozumienie faktu, że moduł runtime nie wymusza zwalniania obiektów; odpowiedzialność za wywołanie metody

Dispose spada na

klienta. Jednakże, dzięki wykorzystaniu wzorca zwalniania prze'l bibliotekę klas .NET Frame­ work, język C# udostępnia instrukcję

using,

która pozwala uprościć prawidłowe stosowanie

obiektów jednorazowego użycia. Przycocwny niżej kod pokazuje strukturę instrukcji

using-.

using ( FileSt ream f ileSt ream = new FileSt ream ( " SomeFile . txt " , FileMode . Open ) ) {

li j akieś p rzeks z tałcenia obiektu fileSt ream } A oto niektóre zagadnienia, które należy rozważyć, implementując wzór zwalniania: •

Kod klienta powinien być w stanie wywołać metodę Dispose w sposób powtarzalny, be'l żadnych zakłócających efektów.

482

C#

-

Księga przykładów

• W aplikacjach

• •

wielowątkowych ważne jest, by tylko jeden wątek na raz wykonywał metodę

Dispose. Zazwyczaj za zapewnienie synchronizacji wątków odpowiada kod klienta, ale można zdecydować się na implementację synchronizacji w metodzie Dispose. Metoda Dispose nie powinna zwracać wyjątków. Ponieważ metoda Dispose wykonuje całe niezbędne czyszczenie, nie ma potrzeby wywoły­ wania finalizatora obiektu. Metoda Dispose powinna wywołać metodę GC.SuppressFinalize, aby zabezpieczyć się przed wywołaniem finalizatora przez mechanizm sprzątający.



Implementacja finalizatora wywołującego metodę Dispose jako mechanizmu bezpieczeń­ srwa w przypadku, gdy kod klienta nie wywoła poprawnie metody Dispose. Jednakże należy unikać odwoływania się przez finalizator do zarządzanych obiektów, ponieważ nie można mieć pewności co do stanu obiektu.



Jeżeli klasa jednorazowego użycia rozszerza się na inną klasę tego samego rodzaju, metoda Dispose klasy potomnej musi wywołać metodę Dispose jej klasy bazowej . Kod potomny należy opakować w blok try i wywołać nadrzędną metodę Dispose w klauzuli finaliy, aby zagwarantować jej wykonanie.



Inne metody i właściwości klasy powinny wywołać wyjątek System. ObjectDisposedException, jeżeli kod klienta spróbuje wykonać metodę w zwolnionym j uż obiekcie.

Klasa DisposeExamp/.e w poniższym przykładzie demonstruje podstawową implementację wzorca zwalniania: u s ing System ;

li Implementacj a interfej su IDisposable public class DisposeExample : IDi sposable {

li P rywatne pole danych , sygnalizuj ące , czy obiekt został rzeczywiście li zwol niony bool i sDisposed = false ;

li P rywatne pole danych p rzechowu j ące uchwyt do nieza rządzanego zasobu p rivate IntPtr resou rceHandle ; 11 Kon st ruktor

public DisposeExample ( ) {

li Kod kon st rukto ra uzysku j e odsyłacz do nieza rządzanego zasobu . li resou rceHandle =

}

li Dest rukto r I Final ize r . Ponieważ D ispose wywoł u j e GC . Supp res sFinalize, li metoda ta j est wywoływana tyl ko p rzez p roces czysz czenia pamięc i , j eżeli li użyt kown ik obiektu nie wywołał Dispos e , j ak powinien . -DisposeExample ( ) {

li li li li

Wywołanie metody Dispose j ako p rzeciwieństwo duplikacj i kodu w celu wyc z y s z czenia nieza rządzanych za sobów . Należy u żyć ch ronionego p rzeciążenia D is pose i p rzekazał mu wa rtość " fa l s e " w celu zaznaczenia , że Dispose j est wywoływana p rzez p roces c z y s z czenia

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 483

li

pamięci , a nie p rzez kod użyt kowy .

Dispose ( false ) ; }

li li li

Publiczna implementa c j a metody IDispo sable . Di s pose , wywołana p rzez użyt kownika obiektu w celu dete rministycznego zwolnienia nieza rządzanych zasobów .

public void Dispose ( ) {

li li li

Wywołanie ch ronionego p rzeciążenia Dispose i p rzekazanie wa rtośc i " t rue" w c e l u zaznaczenia , że Dispose j es t wywoływane p rzez kod użyt kowy , a nie p rzez p roces c z y s zczenia pamięc i .

Dispose ( t rue ) ;

li li li

Ponieważ metoda Dispose wykonu j e całe niezbędne czyszczenie , należy s p rawdzić , że p roces czyszczen ia pamięci nie wywoła dest rukto ra klas y .

GC . Suppres s F inalize ( t his ) ; }

li li li

Ch ronione p rzeciążenie metody Dispose . A rgument sygnalizu j e , czy metoda j est wywoływana p rzez kod użyt kowy ( t rue ) , czy p rzez p roces czyszczenia pamięc i ( false ) .

p rotected v i rtual void D ispose ( bool d ispos ing ) {

li

Nie należy dwuk rotnie wywoływać D i s pose obiekt u !

if ( ! isDi s posed ) {

li li li

Sp rawdzenie , czy wywołanie pochodzi z kodu użyt kowego , czy z p rocesu c z y s z czenia pamię ci . W czasie zwalniania nie należy odwoływać się do innych za rządzanych obiektów .

if ( d is po s ing ) {

li

Metoda wywołana p rzez kod użyt kowy .

}

li li li li

Bez względ u , czy j est wywołany p rzez kod klienta , czy p rzez mechanizm sp rząta j ący , zwalnia wszyst kie nieza rządzane zasoby i ustawia wa rtości za rządzanych pól danych na n u l l . Clos e ( resou rceHandle ) ;

}

li

Sygnalizowanie , że obiekt został zwoln iony .

isDi sposed

=

t rue ;

}

li li

P rzed wykonaniem j akiej kolwiek funkc j i należy się upewnić , że wobec obiektu nie wykonano Dispose .

public void SomeMethod ( ) {

484

C#

-

Księga przykładów li Wywołanie wyj ątku , j eżeli obiekt został j uż zwolniony . if ( isDisposed ) { th row new Obj ectDisposedException ( " Di sposeExampl e " ) ; }

li Wykonanie metody . 11

·-

} public static void Main ( ) {

li Inst rukc j a u s ing gwa rantuj e , że metoda Dispose zostanie wywołana li nawet wtedy , gdy poj awi s ię wyj ątek . u s ing ( DisposeExample d = new DisposeExampl e ( ) ) {

li z ró b coś z d } } }

1 6.6 Implementowanie typu formatowalnego Problem Chcesz zaimplementować typ, którego można użyć w łańcuchach formatowanych i utworzyć różne reprezentacje zawartości w zależności od specyfikatorów formatu.

Rozwiązanie Zaimplementuj interfejs

System.!Formattabk.

Omówienie Poniższy fragment kodu demonstruje użycie specyfikatorów formatu w metodzie klasy

System. Consok.

WriteLirie

Kod w nawiasach klamrowych (w niniejszym przykładzie wytłuszczony)

stanowi specyfikatory formatu.

double a = 345678 . 5678 ; uint b

=

12000 ;

byte c = 254 ; Console . Writeline ( " a = {8} , b = { 1 } , and c

=

{2} " , a , b , c ) ;

Console . W riteline ( " a = {8 : c8} , b = { l : n4} , and c = {2 , 18 : x5} " , a , b , c ) ; Po uruchomieniu w systemie skonfigurowanym według regionalnych ustawień dla Wielkiej Brytanii (U.K.), kod ren utworzy pokazane niżej dane wyjściowe. Jak można zauważyć, zmiana specyfikacorów formatu w sposób znaczący zmienia format danych wyjściowych, nawet gdy same dane nie zostały zmienione.

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 485 a a

=

345678 . 5678 , b f345 , 679 , b

=

=

12000 , and c = 254

12 , 000 . 0000 , and c

=

000fe

Aby zapewnić wykorzystanie specyfikatorów formatu w typach projektanta, nalei.y zaimple­ mentować interfejs !Formattable. Deklaruje on pojedynczą metodę o nazwie ToString z nastę­ pującym podpisem: st ring ToSt ring ( st ring f o rmat , I F o rmat P rovide r f o rmat P rovide r ) ;

Argument format jest łańcuchem System.String zawierającym łańcuch formatu. Łańcuch taki jest częścią specyfikacora formatu, występującego po przecinku. Na przykład dla specyfikatora formatu {2, l O :x5 } z poprzedniego przykładu, „x5" jest łańcuchem formatu. Zawiera on instruk­ cje, których instancja !Formattable może użyć, gdy generuje reprezentację jego zawarcości. W dokumentacji .NET Framework dla /Formattable zawiera wymóg, by typy implementujące !Formattable akceptowały postać „G" (generał - ogólną), ale inne łańcuchy formatu są zalei.ne od implementacji. Argument format będzie miał wartość null, jeżeli specyfikator formatu nie zawiera komponentu formatu, na przykład {O} lub { l ,20} . Argument formatProvider jest odniesieniem do instancji System.!FormatProvider, która zapewnia dostęp do informacji o preferencjach kulturowych i regionalnych, i jest scosowany podczas generowania reprezentacji łańcucha obiektu !Formattable. Informacja ta zawiera takie dane, jak symbol waluty lub liczbę miejsc dziesiętnych. Domyś l nie formatProvider ma wartość null, co oznacza, że nalei.y użyć ustawień regionalnych biei.ącego wątku, dostępnych dzięki statycznej metodzie CurrentCulture klasy System. Gwbalization. Culturelnfo. Niektóre metody generujące formatowane łańcuchy, takie jak String.Format, umożliwiają wyspecyfikowanie alternatywnego !FormatProvider. System .NET Framework używa !Formattable przede wszystkim jako wsparcia dla formato­ wania typów wartości, ale opcja ta może być używana z dobrym rezultatem dla każdego typu. Poniższy przykład zawiera klasę o nazwie Person, która implementuje interfejs !Formattable. Klasa Person zawiera tytuł oraz imię i nazwisko osoby i wyświetli nazwę osoby w różnych forma­ tach, zależ.nie od dostarczonego łańcucha formatu. Klasa Person nie używa ustawień regional­ nych dostarczonych przez argument formatProvider. ,

using System ; public class Pe rson : I F o rmattable {

li Pola p rywatne p rzechowuj ące tytuły i inne s z czegóły o soby . p rivate st ring title ; p rivate st ring [ ] names ;

li Konst ruktor u żywany do określania tut ułów i nazwisk o sób . public Person ( st ring title , pa rams st ring [ ] name s ) { title;

this . t itle thi s . names

=

names ;

}

li Zastąpienie metody Obj ect . ToSt ring w celu zwrócenia nazwiska osoby p rzy li użyciu ogól nego f o rmat u . public ove r ride st ring ToSt ring ( ) { ret u rn ToSt ring ( " G " , null ) ; }

486

C#

-

Księga przykładów li Implementac j a metody I F o rmattable . ToSt ring , zwracaj ącej nazwisko w innym li formacie , zależnie od p rzekazanego łańcucha formatuj ącego . public st ring ToSt ring ( st ring fo rmat , I Fo rmat P rovider format P rovide r ) { st ring result

=

null ;

li Użycie formatu ogólnego , j eżeli żaden nie został podan y . i f ( fo rmat == nul l ) format = " G " ;

li Zawa rtość łańcucha formatuj ącego określa format , w j a kim zwracane li j est nazwisko . switch ( fo rmat . ToUppe r ( ) [ 0 ] ) { case ' S ' :

li Wyko rzys tanie k rótkiej f o rmy - inicj ał imienia i nazwis ko re sult = names [ 0 ] [ 0 ] + "

" + names [ names . Length - 1 ] ;

b reak ; case ' P ' :

li We rsj a fo rmalna - tytuł , inic j ały i nazwisko . if ( t itle ! = null && title . Length ! = 0 ) { result = title + " . " ; }

li Dodanie inicj ałów i nazwis ka for ( int count = 0 ; count

<

names . Length ; count++ ) {

if ( count I = ( names . Length - 1 ) ) { result += names [ count ] [ 0 ] +

"

. I

} else { result += names [ count ] ; } } b reak ; case ' I ' :

li Fo rma nieofic j alna - tylko pierwsze imię re sult

names [ 0 ] ;

b reak ; case ' G ' : defaul t :

li Fo rma ogólna ( domyślna ) - pierwsze imię i nazwis ko re sult = name s [ 0 ] + " " + names [ names . Lengt h - 1 ] ; b reak ; } retu rn resul t ; } }

Rozdział 1 6: Powszechnie używane interfejsy i wzorce

487

Poniższy kod demonstruje wykorzystanie możliwości formatowania klasy Penon:

li Two rzenie obiektu Pe rson , reprezentu j ącego człowieka o nazwi s ku M r . Richard li Glen David Pete rs . Pe rson pe rson

=

new Pe rson ( "M r " , " Richa rd " , " Glen " , " David " , " Peters " ) ;

li Wyświetlenie nazwy osoby p rzy użyciu różnych łańcuchów fo rmatuj ących . System . Console . W riteLine ( " De a r { 9 : G} , " , person ) ; System . Console . WriteLine ( " De a r {9 : P} , " , person ) ; System . Console . W riteLine ( " De a r {0 : I } , " , person ) ; System . Console . WriteLine ( " De a r { 0} , " , pe rson ) ; System . Console . WriteLine ( " Dea r { 9 : 5} , " , person ) ;

Po wykonaniu kod urworzy następujące dane wyjściowe: Dea r Richa rd Pete rs , Dear M r . R . G . D . Pete rs , Dear Richa rd , Dear Richa rd Pete r s , Dear R . Pete rs ,

1 6. 7 Implementacja niestandardowej klasy wyjątku Prob lem Chcesz urworzyć własną klasę wyjątku, tak by można było wykorzystać mechanizm obsługi wyjątków modułu rumime i obsługiwać wyjątki specyficzne dla aplikacji .

Rozwiązanie Urwórz serializowalną klasę, która rozszerza się na klasę System.App/icationException i imple­ mentuje konstruktory z następującymi podpisami: publ ic CustomException ( ) : base ( ) { } public CustomException ( st ring mes sage ) : base ( message ) { } public CustomException ( st ring mes sage , Exception inne r ) : base ( mes sage , inne r ) { }

Zaimplementuj wszystkie pola danych użytkownika wymagane przez wyjątek, w cym konscruk­ cory i właściwości niezbędne do manipulowania polami danych.

Omówienie Klasy wyjatków są unikatowe, ponieważ nie trzeba deklarować nowych klas, aby zaimplementować nową lub rozszerzoną funkcjonalność. Mechanizm obsługi wyjątków modułu runtime - dostępny

488

C#

-

Księga przykładów

za pośrednictwem instrukcji języka C# try, catch i finaUy, pracuje w oparciu o typ wywołanego wyjątku, a nie o pola funkcjonalne lub pola danych, implementowane przez wyjątek. W celu wywołania wyjątku należy użyć istniejącej klasy wyjątku z biblioteki klas .NET Fra­ mework (jeśli odpowiednia istnieje). Do użytecznych klas wyjątków należą: •

System.ArgumentNullException, gdy kod przekazuje argument o wartości nuU do metody zastosowanej przez projektanta, która nie wspiera argumentów null.



System.ArgumentOutOJRangeException, gdy kod przekazuje nadmiernie dużą lub małą war­ tość argumentu do metody projektanta.



System.FormatException, gdy kod próbuje przekazać do metody projektanta argument String, zawierający niewłaściwie sformatowane dane.

Jeżeli nie istnieje klasa wyjątku odpowiednia do potrzeb projektanta albo gdy aplikacja mogłaby zyskać na wykorzystaniu specyficznych dla niej wyjątków, można łatwo utworzyć własną klasę wyjątku. Aby zintegrować niestandardowy wyjątek z mechanizmem obsługi wyjątków modułu runtime, a jednocześnie zachować spójność ze wzorcem zaimplementowanym przez klasy wyjątku zdefiniowane w .NET Framework, należy: •

Nadać klasie wyjątku wyróżniającą nazwę zakorkzoną słowem Exception, taką jak TjpeMis­ matchException lub RecordNotFoundException.



Rozszerzyć klasę ApplicationException. Niestandardowa klasa wyjątku bezwzględnie musi rozszerzać klasę System. Exception w przeciwnym wypadku kompilator zgłosi błąd, gdy kod projektanta spróbuje wywołać wyjątek; klasa ApplicationException rozszerza Exception i jest zalecaną podstawą dla wszystkich klas wyjątku specyficznych dla aplikacji. -



Należy oznaczyć klasę wyjątku projektanta jako seafed (zapieczętowana), jeżeli inne klasy wyjątku nie mają jej rozszerzać.



Zaimplementować dodatkowe pola danych i właściwości, aby zapewnić wsparcie dla niecy­ powych informacji, których powinna dostarczyć klasa wyjątku.



Zaimplementować trzy konstruktory public o podpisach przedstawionych niżej i upewnić się, że wywołują one konstruktory klasy podstawowej. public CustomException ( ) : base ( ) { } public Cus tomException ( st ring mes sage ) : base ( mes sage ) { } public CustomException ( st ring message , Exception inne r ) : base ( mes sage , inne r ) { }



Zapewnić serializowalnośc klasy, tak by moduł runtime mógł przekazywać wyjątki przez granice domen aplikacji i przez granice systemu komputerowego. Zastosowanie atrybutu System.SerializableAttribute wystarcza dla klas wyjątków, nie implementujących niestandar­ dowych pól danych. Ponieważ jednak Exception implementuje interfejs System.Runtime. Serialization.!Serializabfe, jeśli klasa wyjątku projektanta deklaruje niestandardowe pola danych, należy zastąpić metodę !Serializabfe. GetObjectData klasy Exception, a ponadto zaimplementować konstruktor deserializacj i z podanym niżej podpisem. Jeżeli klasa wyjątku projektanta jest opieczętowana (seafed), należy oznaczyć konstruktora deserializacji jako pri­ vate; w innym przypadki należy oznaczyć go jako chroniony (protected). p rivate CustomException ( S e rializationlnfo info , St reamingContext context ) { }

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 489 Metoda GetObjectData i konstruktor deserializacji powinny wywoływać równoważną metodę klasy bazowej, aby umożliwić klasie bazowej poprawną serializację i deserializację danych. Więcej szczegółów dotyczących ustanawiania serializowalnych klas zawiera przepis 1 6. 1 . Przedstawiona niżej klasa CustomException jest niestandardową klasą wyjątku, który rozsze­ rza ApplicationException i deklaruje dwa niestandardowe pola danych: łańcuch o nazwie strin­ glnfa i zmienną boołowską o nazwie booleanlnfa. using System ; using System . Runtime . Se rialization ;

li Oznaczenie CustomException j ako s e rializowal nej . [ Se ria l i z ab le I public sealed class CustomException : Appl icat ionException {

li Niestanda rdowe pola danych klasy CustomExcept ion . p rivate st ring s t ringinf o ; p rivate bool boolean info ;

li T rzy standa rdowe kons t rukto ry i p roste wywołanie konst rukto ra klasy li bazowej ( System . Appl icationException ) . public CustomException ( ) : base ( ) { } public CustomException ( st ring message ) : base ( mes sage ) { } public CustomException ( s t ring mes sage , Exception inne r ) : base ( mes sage , inne r ) { }

li li li li li

Kon st ruktor dese rializacj i wymagany p rzez inte rfej s ISerialization . Ponieważ CustomException j es t opieczętowana , kon s t ru kto r ten j est p rywatny . Gdyby CustomException n ie była opieczętowana , konst ruktor powin ien zostać zadekla rowany j a ko ch roniony , aby wyp rowad zone klasy mogły go wywołać podczas dese rializacj i .

p rivate CustomExceptio n ( Serializationinfo info , St reamingCon text context ) : base ( info , context ) {

li Deserializowanie każdego n ie standa rdowego pola danych . st ringinfo = info . GetSt ring ( " St ri ng i n fo " ) ; boolean info

=

info . GetBoolean ( " Booleaninfo" ) ;

}

li Dodat kowe kon st ru kto ry umożliwiaj ące kodowi u stawianie niestanda rdowych li pól danych . public CustomException ( st ring message , st ring st ring!nfo , bool boolean ! n fo ) : this ( mes sage ) { t h i s . st ringinfo

=

st ringinfo ;

t h i s . boolean!nfo = booleaninfo ; } public C u stomException ( s t ring message , Exception inne r , st ring st ringinfo , bool booleani n f o ) : this ( mes sage , inne r ) { this . st ring! n fo t hi s . boolean info }

=

=

st ringinfo ; boolean!nfo ;

490

C#

-

Księga przykładów li Właściwości tylko do odczyt u , zapewniaj ące dostęp do n iestanda rdowych li pól danych . public st ring St ring!nfo { get { ret u rn st ring!nfo ; } } public bool Boolean!nfo { get { retu rn boolean! n f o ; } }

li li li li

Metoda GetObj ectData ( zadekla rowan a w inte rfej s ie !Se rial izabl e ) j est używana podczas se rial izac j i CustomException . Pon ieważ klasa ta dekla r u j e n iestanda rdowe pola danych , musi ona zastąpić implementacj ę GetObj ectData z klasy bazowej .

public ove r ride void GetObj ectData ( Se rializat ion info info , St reamingContext context ) {

li Serializowanie niestanda rdowych pól danych info . AddValue ( " St ring! n fo " , st ring!nfo ) ; info . AddValue ( " Booleaninfo" , boolean!nfo ) ;

li Wywołanie klasy bazowej w celu s e rializacj i j ej pól base . GetObj ectData ( info , context ) ; }

li Zastąpienie właściwości klasy bazowej Message w celu dołączenia li niestanda rdowych pól danych . public ove r ride st ring Mes sage { get { st ring mes sage = base . Mes sage ; if ( st ring! n fo ! = nul l ) { message += Envi ronment . NewLine st ring!nfo +

"

=

+

" + boolea n ! n fo ;

} ret u rn mes sage ; } } } W dużych aplikacjach implemenruje się zazwyczaj niewiele klas wyjątków. Sposób zorganiw­ wania własnych klas wyjątków i ustalenie, jak kod powinien ich używać, wymaga dogłębnego przemyślenia. Generalnie należy unikać tworzenia nowych klas wyjątków, chyba że kod ma specjalnie przechwycić ten wyjątek. W celu osiągnięcia większej szczegółowości informacyjnej należy użyć raczej pól danych, a nie nowych klas wyjątków. Ponadto należy w miarę możliwości unikać rozbudowanych pionowo hierarchii klas, na rzecz szerokich i płytkich hierarchii.

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 491

1 6.8 Implementacja niestandardowego argumentu zdarzenia Problem Kiedy nastąpi zdarzenie, chcesz przekazać specyficzny dla niego stan do programów obsługi zdarzeń.

Rozwiązanie Utwórz niestandardową klasę argumentu zdarzenia, wyprowadwną z klasy

System.EventArg.

Kiedy nastąpi zdarzenie, utwórz instancję tej klasy i przekaż ją do programów obsługi zdarzeń.

Omówienie Przy deklarowaniu własnych typów zdarzeń, często wymagane jest przekazanie stanu określo­ nego zdarzenia do nasłuchujących programów obsługi zdarzeń. Aby utworzyć niestandardową klasę argument u zdarzenia, zgodną ze wwrcem

Eventpattem zdefiniowanym w

.NET Frame­

work, należy: • Wyprowadzić niestandardową klasę argumentu zdarzenia z klasy

EventArgs.

Klasa ta nie

zawiera danych i jest używana do zdarzeń, które nie wymagają przekazywania stanu.

EventArgs, na przy­ DiskFuiiEventArgs lub Mail&ceivedEventArgs. Zaznaczyć klasę argumentu jako opieczętowaną (seakd), jeżeli nie ma być rozszerzana przez

• Nadać klasie argumentu zdarzenia znaczącą nazwę, zakońcwną słowem

kład



inne klasy argumentów zdarzeń. • Zaimplementować dodatkowe pola i właściwości danych, aby zapamiętać stan zdarzenia,

który ma być przekazany do programów obsługi zdarzeń. Najlepiej zapewnić niezmienność stanu zdarzenia, tak więc należy użyć pól danych zadeklarowanych jako

private readonly

(prywatne i tylko do odczytu) oraz właściwości public (publicznych), aby zapewnić dostęp do danych typu „tylko do odczytu". • Zaimplementować konstruktora public (publicznego), wspierającego początkową konfigu­

rację stanu zdarzenia. • Zapewnić klasie argumentów zdarzeń serializowalność, tak by moduł runtime mógł prze­

kierowywać jej instancje przez granice domen aplikacji i systemu komputerowego. Zasto­ sowanie atrybutu

System.SerializabkAttribute

jest zazwyczaj wystarczające. Jednakże jeśli

dana klasa ma specjalne wymagania w zakresie serializacji, należy także zaimplementować interfejs

System.Runtime.Serialization.!Serializabk (przepis 1 6. l

zawiera więcej szczegółów

na temat tworzenia klas serializowalnych). Poniższy wydruk pokazuje implementację klasy argumentu zdarzenia o nazwie

EventArgs. Teoretycznie,

MailReceived­

serwer pocztowy przekazuje instancje tej klasy do programów obsługi

492

C#

-

Księga przykładów

zdarzeń w reakcji na odebranie wiadomości e-mail. Klasa MaiLReceivedEventArgs zawiera infor­ mację o nadawcy i temacie otrzymanej poczty. using System ; [ Se rializable ) public sealed class MailReceivedEventArgs : EventArgs {

li P rywatne pola tyl ko do odczyt u , p r zechowuj ące stan zda rzenia , który li j est roz syłany do wszystkich p rog ramów obsługi zda rzeń . Klasa li MailReceivedEventArgs określi nadawcę i temat ot rzymanej wiadomości . p rivate readonly st ring f rom ; p rivate readonly st ring subj ect ;

li Kon st rukto r inicj u j e s tan zda rzenia public MailReceivedEventA rgs ( st ring f rom , st ring subj ect ) { t h i s . f rom

=

this . subj ect

f rom; =

subj ect ;

}

li Właściwości tylko do odczyt u , zapewniaj ące dostęp do stanu zda rzenia public st ring F rom { get { ret u rn f rom ; } } public st ring Subj ect { get { retu rn subj ect ; } } }

1 6.9 Implementacja wzorca pojedynczej instancji Problem Chcesz zapewnić istnienie tylko pojedynczej instancji danego typu w danym momencie i umoż­ liwić dostęp do niej wszystkim ełemenrom twojej aplikacji.

Rozwiązanie Zaimplementuj typ przy użyciu „Singleton Patrem" (wzorzec pojedynczy) poprzez: •

Implemenrację w typie pola private static, aby przechować odniesienie do pojedynczej instancji typu.



Implementację publicznie dostępnej właściwości statycznej w tym typie, aby zapewnić dostęp „tylko do odczytu" do pojedynczej instancji.



lmplemenrację konstruktora prywatnego, aby kod nie mógł utworzyć dodatkowych instan­ cji tego typu.

Omówienie Spośród wszystkich zidentyfikowanych wzorców, wzorzec pojedynczej instancj i (Singleton Par­ tem) jest prawdopodobnie najbardziej znany i najczęściej używany. Celem jego zastosowania

Rozdział

1 6:

Powszechnie używane interfejsy i wzorce 493

jest zagwarantowanie, że w danym momencie istnieje tylko jedna instancja typu, a także zapew­ nienie globalnego dostępu do funkcjonalności tej pojedynczej instancj i. Poniższy kod demon­ struje implementację wzorca pojedynczej instancji dla klasy o nazwie Singkton.Exampk

public class SingletonExample {

li Pole s tatyczne p rzechowu j ące odniesienie do poj edynczej instancj i p rivate static SingletonExample instan ce ;

li Statyczny kon st ruktor two rzący poj edynczą instancj ę . Inną alte rnatywą li j est posłużenie się opóźnioną inicj ac j ą właściwości I n stance . s tatic Singleton Example ( ) { instan ce = new Singleton Example ( ) ; }

li P rywatny kon st rukt o r , powst rzymuj ący kod p rzed tworzeniem dodat kowych li instacj i typu p rivate SingletonExample ( ) { }

li Właściwoś c publiczna , zapewniaj ąca dostęp do poj edynczej instancj i public s tatic SingletonExample I n stance { get { retu rn instance ; } }

li Publiczne metody zapewn iaj ące funkcj onalność poj edync zej instancj i public void SomeMethodl ( ) { public void SomeMethod2 ( ) {

I* . . *I I* . . *I

} }

}

Singkton.Exampk, należy uzyskać odniesienie do pojedyn­ lnstance, a następnie wywołać jej metody. Alternatywnie, można bezpośrednio wykonać pola pojedynczej instancji poprzez właściwość lnstance. Poniższy

Aby wywołać funkcjonalność klasy

czej instancji przy użyciu właściwości kod pokazuje oba podejścia.

li Uzys kanie odnies ienia do poj edynczej instancj i i wywołanie metod SingletonExample s = SingletonExample . Instance ; s . SomeMethod l ( ) ;

li Wykonanie funkcj onalności poj edynczej instan c j i bez odniesienia SingletonExample . I nstance . SomeMethod 2 ( ) ;

1 6.1 O Implementacja wzorca obserwatora Problem Chcesz zaimplementować wydajny mechanizm pozwalający obiektowi (podmiotowi) na sygna­ liwwanie zmian swojego stanu innym obiektom (obserwatorom).

494

C#

-

Księga przykładów

Rozwiązanie Zaimplementuj Observer Pattern (wzorzec obserwatora) przy użyciu typów delegowanych jako funkcji wskaźnikowych zależnych od typów danych oraz typów zdarz.eń, aby zarządzać powia­ damianiem zestawu obserwatorów.

Omówienie Tradycyj ne rozwiązanie wzorca obserwatora (Observer Pattern) stanowi implemencacja dwóch interfejsów: jednego reprezentującego obserwatora (!Observer) i drugiego reprezentującego podmiot (Subject). Obiekty implementujące !Observer rejestrują się wobec podmiotu, zazna­ czając, że mają przyjmować powiadomienia o ważnych zdarzeniach, takich jak zmiany stanu podmiotu. Podmiot jest odpowiedzialny za zarządzanie listą zarejestrowanych obserwatorów i powiadamianie ich w odpowiedzi na zdarzenia, które go dotyczą. Podmiot zazwyczaj powiada­ mia obserwatorów przy użyciu metody Notify, zadeklarowanej w interfejsie !Observer. Podmiot może przekazać dane obserwatorowi jako część metody Noti/J; alternatywnie obserwator może wywołać metodę zadeklarowaną w interfejsie !Subject w celu otrzymania dodatkowych danych o zdarzeniu. Wprawdzie projektant może utworzyć wzorzec obserwatora w języku C#, używając opi­ sanego podejścia, jednak wzorzec ten jest tak rozpowszechniony w nowoczesnych rozwiąza­ niach, że język C# i moduł .NET Framework zawierają typy zdarzeń i delegatów, upraszczające jego implementację. Użycie zdarzeń i delegatów oznacza, że nie trzeba deklarować interfejsów !Observer i !Subject. Ponadto nie jest konieczne konstruowanie logiki zarządzającej i powiada­ miającej zarejestrowanych obserwatorów - a jest to obszar, w którym najłatwiej o błąd kodo­ wania . . NET Framework tak często używa pewnej szczególnej implementacji wzorca obserwatora opartego o zdarzenia i delegata, że otrzymała ona własną nazwę Event pattern (wzorzec zdarze­ nia). Formaliści woleliby zapewne termin Event idiom, jednak pozostaniemy przy terminologii powszechnie stosowanej w dokumentacji firmy Microsoft. Należący do kodu przykładowego dla tego rozdziału plik ObserverExample.cs zawiera kompletną implementację wzorca zdarzenia. Przykład cen zawiera następujące typy: •

Klasę 7hermostat (przedmiot przykładu), która śledzi aktualną temperaturę i powiadamia obserwatorów, kiedy występują zmiany.



Klasę TemperatureChangeEventArgr, która jest implementacją użytkownika klasy System. EventArgr, używaną do przechowania danych o zmianach temperatury celem dystrybucji podczas powiadamiania obserwatorów.



Delegata TemperatureEventHandler, definiującego podpis metody, którą muszą zaimple­ mentować wszyscy obserwatorzy obiektu 7hermostat i którą obiekt 7hermostat wywołuje w przypadku zmiany temperatury.



Klasy TemperatureChangeObserver i klasy 7hermostat.

TemperatureAverageObserver, które są obserwatorami

Klasa TemperatureChangeEventArgs (występująca w poniższym przykładzie) jest wyprowadzona z klasy System.EventArgr. Klasa argumentu zdarzenia powinna zawierać wszystkie dane, które

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 495 podmiot ma przekazać do obserwatorów przy powiadamianiu ich o zdarzeniu. Jeżeli nie trzeba przekazać danych wraz z powiadomieniem o zdarzeniu, nie trzeba również definiować nowej klasy argumentu; wystarczy przekazać argument

null (zero)

przy rozpatrywaniu zdarzenia.

W przepisie 1 6.8 można znaleźć więcej szczegółów dotyczących implementacji niestandardo­ wych klas argumentów zdarzeń.

li li

Klasa a rgumentu zda rzenia , zawie raj ąca i n f o rmacj e o zda rzeniu zmiany temperat u ry . I n s t a n c j a tej klasy j est p rzekazywana z każdym zda rzeniem .

public class Tempe rat u reChangeEventA rgs : System . EventArgs {

li

P rywatne pola danych zawie raj ą sta ry i nowy odczyt tempe ratu ry

p rivate readonly int oldTempe rat u re , newTempe rat u re ;

li

Kon st ruktor pobie raj ący s t a rą i nową wa rtość tempe rat u ry

public Tempe ratu reChangeEventArg s ( int ol dTemp , int newTemp ) { oldTempe ratu re

oldTem p ;

newTempe rat u re = newTemp ; }

li

Właściwość tylko d o odczytu zapewnia dostęp d o wa rtości tempe ratu r

public int OldTemperat u re { get { ret u rn oldTemperat u re ; } } public int NewTempe ratu re { get { ret u rn newTemperat u re ; } } } Podany niżej kod pokazuje deklarację delegata

TemperatureEventHandkr. Wszyscy obserwato­

rzy muszą zaimplementować metodę w oparciu o tę deklarację (jej nazwa nie ma znaczenia),

void i pobiera dwa argumenty: obiekt 'Ihermostat jako pierwszy i obiekt Temperatu­ reChangeEventArgs jako drugi argument. Podczas powiadamiania argument 'Ihermostat odnosi się do obiektu 'Ihermostat, który wywołał zdarzenie, a argument TemperatureChangeEventArgs która zwraca

zawiera dane o starych i nowych wartościach temperatury.

li li

Delegat określaj ący podpi s , któ ry muszą z awierać wszystkie metody obsługi zda rzeń zmian tempe rat u ry

public delegate void Tempe ratu reEventHandle r ( The rmostat s , Tempe rat u reChangeEventA rgs e ) ; W tym wzorcu zdarzenia obserwatora obserwowanym obiektem jest klasa

'Ihermostat.

Teore­

tycznie, monitorujące urządzenie ustawia bieżącą temperaturę przez wywołanie właściwości

Temperature dla obiektu 'Jhermostat. Powoduje ro, że obiekt 'Ihermostat wyzwala zdarzenie Tem­ peratureChange i wysyła obiekt TemperatureChangeEventArgs do każdego obserwatora. Oto kod dla klasy 'Ihermostat: li li li li

Klasa rep rezentuj ąca t e rmostat , będący ź ródłem zda rzeń polegaj ących na zmianach temperat u ry . We wzo rcu obserwato ra obiekt t e rmostatu j es t " podmiotem " , któ rego nasłuchuj ą " ob s e rwato rzy " , oczekuj ący n a powiadomienia o zmianac h .

public class The rmostat {

li

Pole p rywatne p rzechowu j ące aktualną temperat u rę .

p rivate int temperatu re = e ;

496

C#

-

Księga przykładów li P rocedu ra obsługi zda rzeń , wyko rzystywana do za rządzania listą delegatów li obserwato rów i wyzwolenia zda rzenia , gdy nastąpi zmiana temperatu ry . public event Tempera t u reEventHandle r Tempe rat u reChange ;

li li li li li

Metoda c h roniona , sł użąca do wyzwolenia zda rzenia Tempe rat u reChange Pon ieważ zda rzenia mogą być wyzwalane tylko z wnęt rza zawie raj ącego j e typu , użycie metody ch ronionej pozwala n a niestanda rdowe zachowanie klas wyp rowadzonych p rzy równoczes nym zachowaniu możl iwo ś c i wyzwolenia zda rzenia w klasie bazowej .

virtual p rotected void RaiseTempe ratu reEvent ( Tempe ratu reChangeEventA rgs e) {

li Powiadomienie wszystkich obse rwato rów . Test dla zwrotu null pozwala li u stalić , czy z a rej est rowani są j acyś obserwato rzy . if ( Tempe rat u reChange ! = null ) { Tempe rat u reChange ( th i s , e ) ; } }

li Właściwość publ iczna , pobieraj ąca i u s tawiaj ąca bieżącą temperatu rę . li St rona „ u stawiaj ąca" j est odpowiedzialna za wyzwalanie zda rzeń zmiany li tempe ra t u ry w celu powiadamiania obserwato rów . public int Tempe rat u re { get { ret u rn temperat u re ; } set {

li Two rzenie nowego obiektu a rgumentu zda rzenia , zawi e raj ącego li sta rą i nową wa rtość temperat u ry . Tempe rat u reChangeEventArgs e

=

new Temperatu reChangeEventArg s ( temperat u re , value ) ;

li Akt ualizac j a wa rtości tempe rtat u ry . tempe ratu re

=

value ;

li Wyzwolenie zda rzenia zmiany tempe rat u ry . Ra iseTempe rat u reEvent ( e ) ; } } } Aby zademonstrować wzorzec obserwatora (Observer Paccern), przykład zawiera dwa różne cypy obserwatorów:

TemperatureAverageObserver i TemperatureChangeObserver. Obydwie klasy TemperatureAverageObserver utrzymuje licznik zda­

mają cę samą podstawową implementację.

rzeń zmian cemperacury oraz sumę wartości temperatury i wyświetla średnią cemperacurę przy wystąpieniu każdego zdarzenia.

TemperatureChangeObserver

wyświecla informację o zmianie

cemperacury za każdym razem, gdy następuje zdarzenie zmian temperatury.

TemperatureChangeObserver (należy sięgnąć do przy­ TemperatttreAverageObserver). Warto zauważyć, że konstruktor zawiera odniesienie do obiektu lhermostat, który obiekt TemperatttreChangeObserver ma obser­ Niżej podany wydruk pokazuje klasę

kładowego pliku dla kodu

wować. Przy tworzeniu instancji obserwatora należy przekazać mu odniesienie do podmiotu.

Rozdział 1 6: Powszechnie używane interfejsy i wzorce 497 Obserwaror powinien urworzyć instancję delegata, zawierającą odniesienie do metody zarzą­ dzania obserwatorem zdarzeń. Aby zostać zarejestrowanym jako obserwator, obiekt obserwatora musi dołączyć instancję swojego delegata do podmiotu, przy użyciu publicznego pola zdarzenia podmiotu. Gdy obiekt

Thermostat,

TemperatureChangeObserver zarejestruje

instancję swojego delegata w obiekcie

należy utrzymywać odniesienie do delegata tylko wtedy, gdy obserwacja ma być

póiniej przerwana. W innej sytuacji nie ma potrzeby utrzymywania odniesienia do podmiotu, ponieważ odniesienie do źródła zdarzenia jest zawarte w pierwszym argumencie za każdym razem, gdy obiekt

Thermostat wyzwala zdarzenie poprzez metodę TemperatureChange.

li Obse l"'łła to r obiektu The nno stat , wyświetlaj ący i n f o rmacj ę o zmianie li temperat u ry , gdy wystąpi zda rzenie zmiany . public class Temperatu reChangeObs e rve r {

li Konst rukto r pobie raj ący odniesienie do obiektu The rmostat , który ma być li obsel"'łłowany p rzez obiekt Tempe rat u reChangeObs e rve r . public Tempe rat u reChangeObse rve r (The nnostat t ) {

li Two rzenie nowej i n stancj i delegata Tempe ratu reEventHandl e r li i rej est rowanie j ej w e wskazanym obiekcie The rmostat . t . Tempe ratu reChange += new Temperatu reEventHandle r ( t h i s . Tempe ratu reChange ) ; }

li Metoda obsługi zda rzenia zmiany temperatu ry . public void Tempe ratu reChange ( The nnostat sende r , Tempe ratu reChangeEventArgs temp ) { System . Console . WriteLine ( " ChangeObse rve r : Old={0 } , New:{ l } , Change={2 } " , temp . OldTempe rat u re , temp . NewTempe rat u re , temp . NewTempe ratu re - temp . Ol dTemperatu re ) ; } } Klasa

Thermostat

definiuje metodę

programem. Po urworzeniu obiektu

Main (przedstawioną dalej), która steruje przykładowym Thermostat i dwóch różnych obiektów obserwatora, metoda

Main powtarzalnie monituje użytkownika o wprowadzenie temperatury. Za każdym razem po wpisaniu nowej temperarury obiekt Thermostat powiadamia klasy nasłuchujące, które wyświet­ lają informację na konsoli.

public static void Main ( ) {

li Two rzenie instanc j i The rmostat . Thennostat t = new The rmostat ( ) ;

li Two rzenie obse l"'łła to rów The rmo stat . new Tempe rat u reChangeObse rve r ( t ) ; new Tempe ratu reAve rageObserve r ( t ) ;

li Pętla pobie raj ąca odczyty tempe ratu ry od użyt kownika . Każda wa rtość li nie będąca liczbą cał kowitą powodu j e p rze l"'łła nie pęt l i .

498

C#

-

Księga przykładów do { System . Console . Write ( " \n\ rEnter c u r rent tempe ratu re : " ) ; t ry {

li P rzekonwertowanie danych wpisanych p rzez użytkownika na liczbę li cał kowitą i u stawienie bieżącej tempe ratu ry . t . Tempe ratu re = System . Int32 . Pa rse ( System . Console . ReadLine ( ) ) ; } catch ( Sys tem . Except ion ) {

li Wyko rzystanie wyj ątku j ako wa runku zakończenia . System . Console . Wri teLine ( "Te rminating Obse rverExample . " ) ; ret u rn ; } } while ( t rue ) ; }

Poni�zy wydruk pokazuje dane wyjściowe, wyprodukowane w przypadku urworzenia i urucho­ mienia pliku ObserverExample.cs. Dane wyjściowe projektanta są zaznaczone pogrubieniem. Enter c u r rent temperatu re : 58 ChangeObse rve r : Old=e , New=50 , Change=Se AverageObse rve r : Average=Se . ee Enter c u r rent temperat u re : 28 ChangeObse rve r : Old=50 , New=20 , Change= - 30 AverageObserve r : Average=35 . ee Enter c u r rent temperatu re : 48 ChangeObse rve r : Old=20 , New=40 , Change=20 AverageObse rve r : Average=36 . 67

17 Integracja z systemem Windows Najważniejszym

celem stworzenia platformy Microsoft

.NET Framework jest

możli­

wość wykorzystania jej w różnych systemach operacyjnych, poprawienie mobilności kodu i uproszczenie integracji międzyplatformowej. W momencie pisania tej książki dostępne były wersje .NET Framework dla takich systemów operacyjnych, jak Microsoft Windows, FreeBSD, Linux i Mac OS X. Jednakże większość tych implementacji nie jest do dziś kompletna i tym samym nie są one szeroko stosowane. Systemem operacyjnym, w którym .NET Framework jest najczęściej instalowany, jest Microsoft Windows. Przepisy w tym rozdziale koncentrują się na wykonywaniu zadań specyficznych dla systemu operacyjnego Windows: • Odczytywanie informacji o środowisku wykonawczym {przepisy 1 7 . 1 i 1 7 .2). • Zapisywanie w dzienniku zdarzeń Windows {przepis 1 7.3). • Dostęp do rejestru Windows (przepis 1 7 .4). • Tworzenie i instalowanie usług Windows {przepisy 1 7.5 i 1 7 .6). • Tworzenie skrótu w menu Start lub na pulpicie (przepis 1 7.7) .

Uwaga Większość funkcjonalności omawianej w niniejszym rozdziale podlega zabez­ pieczeniom realizowanym przez uprawnienia dostępu kodu i wymuszanym przez CLR (Common Language Runtime). Rozdział 13 zawiera informacje dotyczące zabezpieczeń dostępu dla kodu; ponadto dokumentacja SOK .NET Framework zawiera omówienie okre­ ślonych uprawnień niezbędnych do wykonania każdego pola.

500

C#

-

Księga przykładów

1 7 .1 Dostęp do informacji o środowisku wykonywczym Problem Chcesz uzyskać dostęp do informacji o środowisku, w którym wykonywana jest aplikacja.

Rozwiązanie Użyj pól klasy System.Environment.

Omówienie Klasa Environment zapewnia zestaw pól statycznych (static), których można użyć do uzyskania (a czasami również modyfikacj i) informacj i o środowisku, w którym działa aplikacja. Tabela 1 7- 1 przedstawia niektóre z najczęściej używanych pól Environment. Tabela 1 7-1

Powszechnie używane pola klasy Environment

Pole Właściwość Commandline

Opis

CurrentDirectory

Pobiera i ustawia łańcuch zawierający katalog bieżącej aplika­ cji. Początkowo ta właściwość będzie zawierała nazwę katalogu, w którym aplikacja została uruchomiona.

HasShutdownStarted

Pobiera zmienną boolowską (bool} wskazującą, czy moduł CLR rozpoczął zamykanie aplikacji, lub czy bieżąca domena aplikacji rozpoczęła zwalnianie.

MachineName OS�rsion

Pobiera łańcuch znaków (string), zawierający wiersz polecenia użyty do uruchomienia bieżącej aplikacji, w tym nazwę aplika­ cji (w przepisie 1 .5 znajduje się więcej szczegółów).

Pobiera łańcuch zawierający nazwę komputera. Pobiera obiekt System. OperatingSystem, dostarczający informa­ cję o platformie i wersji systemu operacyjnego. Więcej szczegó­ łów zawiera akapit poniżej tabeli.

SystemDirectory

Pobiera łańcuch, zawierający w pełni kwalifikowaną ścieżkę katalogu systemowego.

TickCount

Pobiera wartość int, reprezentującą liczbę milisekund, które upłynęły od momentu startu systemu.

UserDomainName

Pobiera łańcuch, zawierający nazwę domeny Windows, do któ­ rej należy dany użytkownik. W przypadku maszyny autono­ micznej wartość będzie raka sama, jak dla pola MachineName.

Rozdział 1 7: Integracja z systemem Windows Tabela 1 7-1

501

Powszechnie używane pola klasy Environment

Pole

Opis

Userlnteractive

Pobiera zmienną boolowską wskazującą, czy aplikacja działa w trybie interakcyjnym użytkownika. User!nteractive dostarczy fa/se, gdy aplikacja działa jako usługa lub jest aplikacją Web.

UserName

Pobiera łańcuch, zawierający nazwę użytkownika, który uru­ chomił bieżący wątek.

�r.rion

Pobiera obiekt System. Ver.rion, zawierający informację o wersji modułu CLR

Metoda ExpandEnvironmentVariabks Zastepuje nazwy zmiennych środowiska w łańcuchu (string) wartością zmiennej ; przepis 1 7 .2 zawiera więcej szczegółów.

GetCommandlineArgs

Zwraca tablicę łańcuchów, zawierającą wszystkie argumenty wiersza poleceń, użyte do wykonania bieżącej aplikacji, w tym jej nazwę; przepis 1 . 5 zawiera więcej szczegółów.

GetEnvironmentVariab/,e

Zwraca łańcuch, zawierający wartość określonej zmiennej śro­ dowiskowej; przepis 1 7.2 zawiera więcej szczegółów.

GetEnvironment Variabks

Zwraca słownik System. Co/lections.!Dictionary, zawierający wszystkie zmienne środowiska i ich wartości; przepis 1 7.2 zawiera więcej szczegółów.

GetFolderPath

Zwraca łańcuch, zawierający ścieżkę do specjalnego folderu systemu wskazanego przy użyciu typu wyliczeniowego System. Environment.Specia/Folder. Dotyczy to folderów zawierających bufor przeglądarki internetowej, cookie, historię, pultit oraz „ulubione"; kompletna lista tych wartości znajduje się w doku­ mentacji .NET Framework SOK.

GetlogicalDrives

Zwraca tablicę łańcuchów, zawierającą nazwy wszystkich logicznych napędów.

Obiekt OperatingSystem zwracany przez OS�r.rion zawiera dwie właściwości: Platform i �r­ sion. Właściwość Platform zwraca wartość typu wyliczeniowego System.Platform!D, identyfiku­ jącą bieżący system operacyjny; uprawnione wartości to Win32NT, Win32S, Win32Wirul.
C# Księga Przykładów (2006) - Jones Allen [wersja 11 MB] [Sharp]

Related documents

526 Pages • 184,357 Words • PDF • 11.1 MB

6 Pages • 2,393 Words • PDF • 64.3 KB

196 Pages • 59,525 Words • PDF • 1005 KB

559 Pages • 323,730 Words • PDF • 7.6 MB

1 Pages • 158 Words • PDF • 117.2 KB

327 Pages • 92,720 Words • PDF • 1.8 MB

9 Pages • 3,937 Words • PDF • 2.6 MB

337 Pages • 95,140 Words • PDF • 6.3 MB

4 Pages • 1,081 Words • PDF • 296.9 KB

135 Pages • 35,241 Words • PDF • 641.3 KB

26 Pages • 1,510 Words • PDF • 671.8 KB